clx75
Joined 4 years ago
Joined 4 years ago
I wrote a Pi extension called "model-escalation" which lets the model request an automatic switch to a smarter model for the next turn if it thinks the problem it is struggling with is too complex or if the user explicitly asks it to do so.
With this extension in hand, I could split the work between Luna and Sol, resulting in a ~95%-5% split. I don't have to switch to Sol when review time comes, Luna does it automatically. Sol tells its remarks, then Luna gets the control back and continues the back&forth (if there are still complaints).
Then I turned what Deepseek did into an upfront policy.
This seems to work quite well generally: watch the agent as it works, and think what could be provided to make its life as easy as possible
I have been intrigued about Chinese models for a long time, but never tried anything except an early Qwen. My daily driver was Luna, with occasional elevation to Terra or Sol - all on 5.6 as I found the output of 6 jarringly incomprehensible.
I still had 95 USD on my OpenRouter account, so I thought I would give DeepSeek Flash 4.1 a try.
I think now I can understand the person who created that thread.
Here is the setup I settled on, after a lot of refactoring and calibration of the workflow:
I have a "plan" prompt which explains to the agent (Luna) that I want to have a discussion about an idea I have in mind. The agent should use the Socratic method to pull out all the necessary info from me using pointed questions. When the agent feels it has a complete picture, it should elevate to a higher level model (Sol), and ask its review. If Sol has complaints, Luna keeps asking me until the points raised by Sol are clarified. The review rounds with Sol continue until Sol says "ready to implement".
At this point Luna creates four files in a directory I specify:
1. PLAN.md: the implementation plan with all the details that came out from the design discussion 2. PHASES.md: implementation phases 3. DECISIONS.md: design decisions (this serves the same role as ADRs aka. architectural decision records) 4. DIARY.md: append-only log of what happened during implementation
Files 2-4 are written by the implementing agent. Luna only adds headers to explain the purpose of the files.
Once these files are there, I start up a new Pi session, switch to Deepseek 4.1 Flash and then point it to this plan folder.
And then it goes brrrrr... :-)
Here is what I implemented in the past week using this method:
1. A capture/analysis/decompilation pipeline for C64 demos: I ask the agent to start a selected demo through VICE, and say "on" or "off" when I want it to start/stop capture. The capture process uses VICE's builtin trace functionality: the trace log is piped into Clojure where it is saved into an optimized binary format (chunks of trace events). Once I have everything captured, I unleash my analysis pipeline which checks the write patterns of the executed code: what it wrote (VIC, SID, CIA, memory) and when (rasterline, cycle). Then it splits the execution trace into segments by type deduced from the write patterns (decruncher, loader, raster, music) and then decompiles each segment into both traditional ASM files and a s-exp based syntax which roundtrips back into the in-memory representation. Once I have the results of the analysis pipeline, I can tell the agent to analyze any demopart and it can explain how the effects are built down to the tiniest timing detail. As a first test, I asked it to create a clean-room implementation of Fairlight's legendary intro and two days later I had it, with everything in sexp assembly, including the music routine and its data. (The reason it took so long was that this was the first decomp and it provided a lot of feedback to the pipeline development.) Finally I had it write an assembler, linker and packager which creates a PRG from the assembled and linked artifact, then runs it through VICE. Done.
2. A DSP compiler which lets me compile sexp forms of a C-like DSP language into native DSP kernels running on the JVM, JS and WASM. Currently I have about 600 tests which verify that the behavior of the three targets matches that of a reference interpreter written in Clojure. I have a full numeric tower with i32, f32, f64, support for arrays and structs, typical control flow constructs, and a library of DSP building blocks like oscillators, filters, LFOs, delays and a chorus effect. By the time this is ready (end of the weekend?), I will be able to build synthesizers like Synth1, Surge XT or Vital in Clojure/Script, compile the DSP kernels in the browser, choosing between JS or WASM backends, driving the kernels via AudioWorklets and sequencing them from the host language.
There are two power tools at play here:
1. Deepseek Flash 4.1 2. Clojure and ClojureScript
Deepseek is a BEAST. It just goes on and delivers, phase after phase. What it needs: clear instructions - this is covered by the planning process quite well, but I still find it needs some steering from time to time (the right nudget of wisdom dropped into its thought stream at the right moment works wonders)
And Clojure... well. That article PG wrote about LISP (Beating the averages): what I witness now proves all of those claims.
The agent LOVES Clojure. Especially the REPL (not so much the parentheses).
Winning strategy: give it two tools: start_jvm and clojure_eval. Implement everything else it may need - tools to start up HTTP servers, compile ClojureScript, add dependencies, capture a C64 demo, run Playwright via spel, etc. - as lightweight Clojure namespaces which provide APIs to all these functionality. Let these namespaces be thin: the namespace and the functions therein shall have docstrings which tell the agent what the functions do, but the functions themselves shall be lightweight proxies to the real implementations in sub-namespaces. Then point the agent to these top-level API namespaces via your AGENTS.md. Bam, instant discoverability with minimal context usage. It will do EVERYTHING through clojure_eval and the round-trip time will be phenomenal, a perfect match to the raw speed of the agent.
...
Now all I need to figure out is what will I do when all my dreams become manifest in flesh.
https://github.com/cellux/pi-extensions/tree/master/extensio...
Note that this may not work for you standalone as it relies on my other agent-sandbox extension (which ensures all agent operations happen inside a sandbox container).
But point an LLM to its source code and it will extract the gist of it.
What I like most is how fast testing goes: it modifies test code on disk, asks clj-reload to reload all affected namespaces, then reruns the tests. It's also fun to see how it verifies its assumptions by evaluating short programs through the nREPL.
I asked it to organize the various subsystems inside my playground repo into Integrant systems and make it possible for me to say things like "restart the http subsystem".
It also understands shadow-cljs: I replicated the necessary parts of the shadow CLI tooling in Clojure; now I can compile, watch and serve any number of CLJS apps located in various namespaces from inside the same JVM. There is no need to touch the command line any more: I just instruct the agent to start a particular CLJS app and it's there.
Implementation: https://github.com/cellux/dotfiles/blob/master/.emacs.d/rb-t...
As Emacs has a built-in tree-sitter package, I implemented this same idea. I created gptel tools like tree_sitter_list_nodes, tree_sitter_get_nodes, tree_sitter_update_nodes, tree_sitter_insert_before_node and tree_sitter_insert_after_node. The "list" tool returns a list of AST nodes with first line number, first line content and node hash. The LLM can then use "get" to collect interesting nodes in their entirety and "update" to update a list of nodes identified by hash with new content (var/function bodies).
Worked like a charm.
This is an AST-walking interpreter for my personal LISP dialect written in C. Once it's ready, I would use it to implement a low-level, statically typed language (Schnell) as a Langsam library. The goal is to gain the ability to JIT-compile Schnell code (sexps of a statically typed language) from Langsam. Once this works, I would rewrite Langsam in Schnell so that it becomes a fast bytecode interpreter. With the faster Langsam (and the Schnell built into it) I could build a little OS called "Oben". The OS would first run on top of Linux, then I would attempt to bootstrap the entire stack on bare-metal. I already have a Forth dialect implemented in assembly language (Grund/Boden). The idea is to implement Langsam in Grund and then bootstrap the entire Grund -> Langsam -> Schnell -> Oben chain on something like the qemu q35, later on a Raspberry Pi Zero 2W and maybe even my own hardware (ie. an FPGA board like what Wirth et al. created for Project Oberon).
2. MTrak - https://github.com/cellux/mtrak
This is a TUI MIDI tracker written in Go. Not too user-friendly: one has to enter raw MIDI messages in hex into the tracks. Can be connected to synths like Fluidsynth or Surge XT via JACK MIDI. Unfortunately it takes a lot of CPU time, probably due to the use of BubbleTea (and no time spent on optimization).
3. Mixtape - https://github.com/cellux/mixtape
Beginnings of a programmable, non-realtime audio sample generator/manipulator written in Go with an OpenGL GUI. I was thinking about how people in the old times cut up the magnetic tape which contained the sound bites and rearranged them to build something new. What if I'd implement a data type called "tape" which is basically a piece of sound and then provide operators in a Forth-like language to create and manipulate such tapes. Each tape could be a sound and then these could be stitched together to form songs. Who knows maybe an entire song could be represented as a hierarchy of these tapes. Each sound or song section could be its own file (*.tape), these could be loaded from each other, maybe even caching the WAV generated from the code of a tape to speed things up when there is a huge hierarchy of tapes in a project. Lots of interesting ideas are brewing in this one.
I imagine two languages - Langsam and Schnell - intertwined in some sort of yin-yang fashion. Langsam is slow, dynamic, interpreted, Schnell is fast, static, compiled. Both would be LISPs. Schnell would be implemented as a library in Langsam. If you said (define (add x y) (+ x y)) in Langsam, you would get a Langsam function. If you said (s:define (add (x int) (y int)) (+ x y)) in Langsam, you would get a Langsam function which is a wrapper over a JIT-compiled Schnell function. If you invoke it, the wrapper takes care of the FFI, execution happens at C speed. Most of the complexity typical of a low-level compiled language could be moved into Langsam. I could have sophisticated type systems and C++ template like code generation implemented in a comfortable high level language.
This latter part I managed to partially implement in Clojure and it works (via LLVM), it would be just too much effort to get it completed.
As we understood it, Kopf lets you build an entire operator in Python, with the watch/update/cache/expansion logic all implemented in Python. But the first operator we wrote in it just didn't feel right. We had to talk to the K8S API from Python to do all the expansions. It was too complex. We also had aesthetic issues with the Kopf API.
Metacontroller gave us a small, Go binary which takes care of all the complex parts (watch/update/cache). Having to write only the expansion part in Python felt like a great simplification - especially now that we have Pydantic.
Other CRs are realized through imperative commands executed against a REST API. Prime example is KeycloakRealm and KeycloakClient which translate into API calls to Keycloak, or FSXFileSystem which needs Boto3 to talk to AWS (at least for now, until FSXFileSystem is also implemented in ACK).
For long-lived resources up-front (compile time?) expansion would be possible, we just don't know where to put the expansion code. Currently long-lived resource CRs are stored in Git, deployment is handled with Flux. When projects want an extra resource, we just commit it to Git under their project-resources folder. I guess we could somehow add an extra step here - running a script? - which would do the expansion and store the children in Git before merging desired state into the nonprod/prod branches, I'm just not clear on how to do this in a way that feels nice.
Currently the entire stack can be run on a developer's laptop, thanks to the magic of Tilt. In local dev it comes really handy that you can just change a CRs and the children are synced immediately.
Drawbacks we identified so far:
If we change the expansion logic, child resources of existing parents are (eventually) regenerated using the new logic. This can be a bad thing - for example jobs (which expand into Argo Workflows) should not change while they are running. Currently the only idea we have to mitigate this problem is storing the initial expansion into a ConfigMap and returning the original expansion from this "expansion cache" if it exists at later syncs.
Sometimes the Metacontroller plugin cannot be a pure function and executing the side effects introduces latency into the sync. This didn't cause any problems so far but maybe will as it goes against the Metacontroller design expressed in the docs.
Python is a memory hog, our biggest controllers can take ~200M.
Or at least this works wonderfully in Clojure via the nREPL.