A mini code agent with Docker Agent - Part 8: a local agent to learn Rust
Over the previous seven steps, we saw various ways of using Docker Agent and discovered some of its basic features.
For this new step, I am going to put a few of those features together to build a local code agent that I will use to learn the Rust language — a kind of learning companion that you can then easily tweak to explore other programming languages (I currently use it for Swift and Python). This is a concrete example of an agent you can run on your own machine, without depending on an external server.
I did not keep everything: for instance, I don't need a skill or an MCP server (for now). But I do keep some tools and commands, and above all the RAG.
The setting: a cookbook, a workspace, and an agent that reads code
The [knowledge/](./knowledge/) corpus is the one from step 04: 16 Rust recipes that pin down certain idioms — syntax, ownership, Option/Result, iterators, collections, strings, traits, tests, concurrency, I/O, JSON, CLI, and the most frequent compiler errors with their fix.
[workspace/](./workspace/) will be the agent's working directory (when it generates source code, for example).
The new setting: lsp
cargo check answers "does this compile?". rust-analyzer answers "what is this symbol, where is it defined, who calls it, what is its exact signature?", symbol by symbol, without a full compilation.
We are going to give our agent an extra power: it will understand code even better. To do that, we are going to plug a Language Server Protocol (LSP) onto rust-analyzer. The agent will then be able to query the language server for information about the source code, such as a function's definition, the references to a variable, or the compilation diagnostics.
So in the agent definition, we add an **lsp toolset**. The LSP provides information about the structure and the symbols of the source code:
- type: lsp
command: rust-analyzer
file_types: [".rs"]
tools: # 6 tools out of 14 — see below
- lsp_document_symbols
- lsp_workspace_symbols
- lsp_references
- lsp_definition
- lsp_hover
- lsp_diagnostics
working_dir: "${env.PWD}/workspace"
lifecycle:
profile: resilient
startup_timeout: 10s
working_dir is not optional
working_dir is what docker-agent sends as the rootUri in the LSP initialize handshake. Without it, rust-analyzer does not load the crate, and the LSP tool calls will fail silently.
Why tools: only keeps 6 tools
The catalogue costs. This agent's full catalogue is close to thirty tools, which is roughly 1,100 tokens in names and descriptions alone, and the LSP accounts for 47% of that on its own (about fifteen tools). So it matters to keep only the tools you actually use, because let's not forget we are using a small model (Mellum2-instruct, 2.5 billion active parameters) and that the context size has a strong impact on how it behaves.
Context: the resource that runs out first
This is the real cost of "assembling", and it does not show up on the first prompt. Everything piles up in the window: the instruction, the definition of the seven toolsets, the name and description of every command, the history, and above all the result of every tool call. When the request no longer fits, the provider refuses it and the session stops dead.
Three ceilings
To keep the window from filling up too fast, you can cap the result of each tool and the content of old tool calls. You can also compact the history to make room for the answer that follows. And you can raise the model's total context window size (as far as it is able to accept).
agents:
root:
# Caps EVERY tool result as it arrives. "Middle-out" truncation:
# we keep the beginning and the end, and replace the middle with a marker.
max_tool_result_tokens: 1500
# Reclaims room from the OLD tool calls, without touching the recent ones.
max_old_tool_call_tokens: 4000
# Compact at 80% rather than 90%: the summary costs less, and there is
# still margin left for the answer that follows.
compaction_threshold: 0.8
⚠️ Both ceilings are at
0by default, meaning disabled. Nothing bounds a tool's output until you have asked for it. On an agent that hasshell,filesystemandlsp, this is the first setting to put in place, well before touching the compaction threshold. One chattycargo clippy, onedirectory_tree, onelsp_referenceson a popular symbol, and the window is full.
And on the engine side:
models:
mellum:
provider_opts:
context_size: 16384 # the TOTAL window
Without it, Model Runner applies its default value, and since docker-agent sizes its compaction budgets proportionally to context_size, an undeclared window amounts to letting compaction work "blind".
In the TUI, **/context** gives you the breakdown by category and **/compact** summarises right away. The full guide: Managing Context & Compaction.
A single command
To save context, the agent offers only one command, /rust, which takes an argument: the name of a Rust concept. The agent goes and searches the cookbook and explains that concept, citing the source files it used.
commands:
rust:
description: "Look a Rust topic up in the cookbook and explain: /rust <topic>"
instruction: >-
Search the cookbook about ${args[0]} and explain the topic only from what you find.
List the source files you used.
Everything else is asked in natural language. You lose no capability (you stop paying a toll on every turn for shortcuts you never use).
A small guardrail: allow_list
The allow_list puts everything the chapter owns out of reach — agent.yaml, AGENTS.md, knowledge/. We separate "what the agent works on" from "what configures the agent".
- type: filesystem
allow_list:
- ${env.PWD}/workspace # the agent writes ONLY there
That said, don't forget that an agent can be "clever", and that the ultimate guarantee is using a sandbox. The chapter ships an sbx kit to run the agent in a throwaway container, with everything the agent needs and nothing more.
The complete AGENTS.md and agent.yaml for this lesson
AGENTS.md:
# Agent: Rust learning companion
You help someone learn Rust. You work in the `workspace/` directory — that is the only place you may write.
## Non-negotiable
Search the team's Rust cookbook before you answer a Rust question or write Rust code: it pins the edition, the version and the idioms the team chose, and it wins over your own habits. If the cookbook is silent on a point, say so. Never invent a rule, and never cite a file that does not contain the answer.
## Method
- Answer what was asked, then stop. One compiling example beats a lecture.
- Name the cookbook file an idiom came from.
- Think before acting on anything non-trivial.
- For a multi-step task, use the todo list.
- Read files before concluding. Never assume their contents.
- Edit files with the filesystem tools. Do not paste code into the conversation.
- After every edit, run `cargo check --manifest-path <crate>/Cargo.toml` on the crate you touched — the `Cargo.toml` nearest above that file. That is the verification — an answer you have not compiled is a draft.
- Find a symbol BY NAME first — `lsp_document_symbols`, `lsp_workspace_symbols` — then `lsp_hover` or `lsp_definition` on the line they give you.
- Never try positions one by one. If a position returns nothing, look the symbol up by name.
- `lsp_diagnostics` answering "No diagnostics" is not a clean bill of health: confirm with `cargo check`.
- Run `cargo clippy` as well: it sees what the cookbook cannot, and the cookbook sees what it cannot.
- Use memory for durable facts — the user's name, what they are learning — not the current task.
agent.yaml:
agents:
root:
model: mellum
description: A local companion for learning Rust, backed by the team's cookbook.
instruction_file: AGENTS.md
max_tool_result_tokens: 1500
max_old_tool_call_tokens: 4000
compaction_threshold: 0.8
max_iterations: 40
toolsets:
- type: lsp
command: rust-analyzer
file_types: [".rs"]
tools:
- lsp_document_symbols
- lsp_workspace_symbols
- lsp_references
- lsp_definition
- lsp_hover
- lsp_diagnostics
working_dir: "${env.PWD}/workspace"
lifecycle:
profile: resilient
startup_timeout: 10s
- type: shell
- type: filesystem
allow_list:
- ${env.PWD}/workspace
- type: todo
- type: think
- type: memory
path: "./memory.db"
- type: rag
ref: rag_knowledge
commands:
rust:
description: "Look a Rust topic up in the cookbook and explain: /rust <topic>"
instruction: >-
Search the cookbook about ${args.join(" ")} and explain the topic only from what you find.
List the source files you used.
rag:
rag_knowledge:
tool:
# What the model reads to decide whether to search. Name the subject.
description: >
Search the team's Rust cookbook:
syntax, ownership and borrowing, Option and Result, error handling,
iterators, collections, strings, structs and enums,
traits and generics, testing, concurrency,
file I/O, JSON, CLI parsing,
and the most common compiler errors with their fixes.
docs:
- ./knowledge
strategies:
- type: chunked-embeddings
embedding_model: embedder
# A plain local SQLite file. Delete it to force a re-index.
database: ./knowledge-embeddings.db
# EmbeddingGemma produces 768-dimension vectors: this MUST match.
vector_dimensions: 768
similarity_metric: cosine_similarity
# The 0.5 default keeps only 1 chunk in 4 on some phrasings, stripping
# the supporting context. Measured in step 04; same corpus here.
threshold: 0.3
limit: 8
chunking:
size: 1200
overlap: 100
respect_word_boundaries: true
models:
mellum:
provider: dmr
model: huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
base_url: http://localhost:12434/engines/v1
#base_url: http://host.docker.internal:12434/engines/v1
provider_opts:
context_size: 16384
# A second, much smaller model — used ONLY to turn text into vectors.
embedder:
provider: dmr
model: huggingface.co/unsloth/embeddinggemma-300m-gguf:Q8_0
base_url: http://localhost:12434/engines/v1
# EmbeddingGemma's context is 2048 tokens: chunks must stay under it.
max_tokens: 2048
📝 You'll find all the configuration files over here: 08-coding-agent
Running Docker Agent
Of course you need the Mellum2-instruct and EmbeddingGemma models. You can download them from Hugging Face:
docker model pull huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
docker model pull huggingface.co/unsloth/embeddinggemma-300m-gguf:Q8_0
And the Rust toolchain must be installed, including rust-analyzer and clippy:
rustup component add rust-analyzer clippy
cd 08-coding-agent
# TUI
docker-agent run localhost.agent.yaml
# from a container or a sandbox:
docker-agent run agent.yaml
In a sandbox: the spec.yaml kit
The folder also comes with an sbx kit ([spec.yaml](./spec.yaml)), to run all of this in a throwaway sandbox:
sbx run docker-agent --kit . -- run agent.yaml
📝 The kit does one precise job: installing what the
agent.yamlrequires: the Rust toolchain, rust-analyzer, clippy, ...
A few prompts to learn Rust
Understanding a concept, with the team's conventions
"/rust Result":

"What is the difference between String and &str, and which one should a function take? Answer from the cookbook.":

"What is the difference between an Option and a Result, and when do I use each? Answer from the cookbook and list the files you used.":

Decoding a compiler error
By far the most rewarding use when you are starting out.
"I get: errorE0502: cannot borrow v as mutable because it is also borrowed as immutable. Explain what causes this and how to fix it, from the cookbook.":

Writing code, then reading it with the LSP
"Create a crate in workspace/temperature with cargo, then write a public function converting Celsius to Fahrenheit, following the cookbook. Run cargo check until it passes.":


"Use lsp_document_symbols on workspace/temperature/src/main.rs and list every item it defines with the line it is on.":

"Read workspace/temperature/src/main.rs and explain it to me function by function. Change nothing.":

Improving your own code
"Rewrite this loop as an iterator chain and explain why it is better, without editing any file: for i in 0..v.len() { total += vi; }":

Key takeaways
By leaning on a cookbook, and on the tools the Rust toolchain offers, you can build a local code agent that helps you learn a programming language. The agent can read the code, understand the errors, and give explanations grounded in the cookbook's conventions and in the ones the tools enforce. It is a practical tool for learning and practising Rust on your own. And of course, it is easily adaptable to other programming languages. But it matters to manage the context and to cap the tool results so the context window does not fill up too fast, especially with a small model.
Written by

No comments yet. Be the first to comment!