How to Configure Self-Improvement
๐ง Tools ยท ๐ Prompt ยท ๐ฌ Conversation
Two independent loops let the agent improve on a task without a person in the loop:
| Loop | What it does | Pieces |
|---|---|---|
| Attempts | On a known task, the agent commits candidate answers; a scorer grades each against a rubric, and only a candidate that beats the best so far is accepted | rubric context source, rubric sources, an attempt store, a scorer, the attempts_tools tools source, optionally the attempt_plateau and attempt_transcript_recorder interjection sources |
| Refinement | The agent's memories, skills, prompt notes and sub-agents are edited from its own trajectory, on its request or at harness checkpoints | the refiner, the refine_tools tools source, the refine interjection source, writable stores |
Both are off by default. The only extra model calls are short-lived helpers (a judge, a refiner, a gate) that never act on the task, and each runs only when configured. They are agent runs: name an agent from agent_setups.json for each, ideally a lean one with tools and memory off, or leave the identifier unset to run a fresh copy of the current agent.
Attemptsโ
On a turn whose user message matches a rubric, the agent gets two tools and a ## Attempts prompt section telling it not to answer directly but to work in attempts: state a hypothesis, produce the full candidate, commit it. Turns that match no rubric are answered normally, with neither tools nor prompt.
{
"agent_configuration": {
"tools_provider_configuration": { "direct_only_tools": ["commit_attempt", "read_attempt"] },
"rubric_context_source_configuration": { "enabled": true },
"disk_rubric_source_configuration": { "enabled": true, "rubrics_directories": ["rubrics"] },
"file_attempt_store_configuration": { "enabled": true, "directory": "attempts" },
"llm_judge_scorer_configuration": { "enabled": true, "judge_agent_identifier": "judge", "repeats": 3 },
"attempts_tools_source_configuration": { "enabled": true, "min_improvement": 0.5, "max_attempts": 5 },
"interjections_provider_configuration": { "enabled": true },
"attempt_plateau_interjection_source_configuration": { "enabled": true },
"attempt_transcript_recorder_interjection_source_configuration": { "enabled": true }
}
}
The attempts gate needs all of: the rubric context source, at least one rubric source, one attempt store, one scorer and the attempts_tools source. Without an enabled store or a matching rubric, every piece stays inert.
Rubricsโ
A rubric names a task, says which user messages are that task, and holds the reference answer. The reference never enters the agent's transcript; only the scorer sees it.
| Field | Type | Required | Description |
|---|---|---|---|
name | str | yes | Task name; keys the lineage of attempts |
match | str | yes | Case-insensitive substring of the latest user message, or a regular expression as re:<pattern> |
reference | str | yes | The reference answer. For exact_match, one accepted answer per line |
task | str | no | What the question asks, given to the judge |
criteria | str | no | Extra grading criteria, given to the judge |
pass_threshold | float | no | Overrides the scorer's pass_threshold for this task |
metadata | dict | no | Structured data a scorer or harness needs beyond the reference, such as the options of a multiple-choice question |
The rubric context source matches the latest user message against the rubrics of every enabled rubric source; the first match wins. It adds nothing to the prompt.
za agents capabilities context-sources configure rubric
| Config key | Setting | Type | Default | Description |
|---|---|---|---|---|
rubric_context_source_configuration | enabled | bool | false | Match the user message against the rubrics and select the task for the turn |
disk_rubric_source_configuration | enabled | bool | false | Load rubrics from Markdown files |
rubrics_directories | list[str] | ["rubrics"] | Directories of <name>.md rubric files | |
in_memory_rubric_source_configuration | enabled | bool | false | Use the rubrics given inline |
rubrics | list[Rubric] | [] | Rubric objects with the fields above |
A rubric file is named after the task. Its YAML frontmatter holds match and the optional fields; its body is the reference answer:
---
match: "re:boiling point of (pure )?water"
task: The boiling point of water at sea level.
criteria: The value must be in degrees Celsius.
pass_threshold: 8
---
100 ยฐC
in_memory_rubric_source_configuration takes the same rubrics as JSON, which is how a harness that controls the request passes the reference for one turn.
Toolsโ
attempts_tools (CLI name attempts-tools):
| Tool | Description |
|---|---|
commit_attempt | Score a complete candidate (hypothesis, artifact, optional notes and parent_id) and record it. Returns the attempt id, verdict, score, the best score before it, the scorer's details and the attempts remaining |
read_attempt | Read an earlier attempt by attempt_id: hypothesis, score, verdict, notes, scorer details and full artifact |
A candidate is accepted when the scorer passes it and its score is at least the best accepted score plus min_improvement; otherwise it is rejected and kept with its score. Its parent defaults to the best accepted attempt. The prompt section lists the lineage so far. Nothing forces the model to commit or to answer with the best accepted artifact: the instructions are the only lever.
Committing is the model's decision, so keep both tools out of the Python REPL with direct_only_tools.
za agents capabilities tool-sets configure attempts-tools
attempts_tools_source_configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Offer the tools and the ## Attempts prompt on turns that matched a rubric |
scorer | str or null | null | Scorer to grade with: exact_match or llm_judge. null uses the first enabled |
min_improvement | float | 0.0 | How much a candidate must beat the best accepted score by. 0 accepts ties, right for deterministic scorers; for llm_judge, raise it to the judge's observed spread |
max_attempts | int or null | null | Commits allowed per task, rejected ones included. null is unlimited. With a pass/fail scorer and few possible answers, a budget stops the agent from enumerating answers instead of reasoning |
digest_limit | int | 20 | Most recent attempts listed in the prompt |
instructions | str or null | null | Replace the built-in ## Attempts instructions |
defer_tools | bool or null | null | Load the tools on demand, see Tool search |
Scorersโ
The scorer runs only inside commit_attempt, against the turn's rubric.
| Scorer | Config key | Passes when |
|---|---|---|
exact_match | exact_match_scorer_configuration | The artifact equals one line of the reference (or, with contains, includes one). Score 1 or 0 |
llm_judge | llm_judge_scorer_configuration | The mean of repeats judgements, each 0 to 10, is at least the pass threshold |
exact_match_scorer_configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Switch the scorer on |
contains | bool | false | Pass when a reference line appears anywhere in the artifact |
normalize | bool | true | Casefold and collapse whitespace before comparing |
llm_judge_scorer_configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Switch the scorer on |
judge_agent_identifier | str or null | null | Agent that grades, once per judgement in a fresh context; its llm_configuration_name picks the grading model. null runs a fresh copy of the current agent |
repeats | int | 3 | Independent judgements to average, run in parallel |
pass_threshold | float | 7.0 | Mean score at or above which a candidate passes, unless the rubric sets its own |
The agent gets the mean, the spread over the repeats and the threshold. The judge's reasons are stored with the attempt and never returned to the agent, because a grader explaining a rejection tends to paraphrase the reference.
Attempt Storesโ
The first enabled store holds the lineage of every task.
| Store | Config key | Settings | Keeps attempts |
|---|---|---|---|
file | file_attempt_store_configuration | enabled (false), directory ("attempts") | On disk: <directory>/<rubric name>/index.jsonl, and per attempt a folder with attempt.json, artifact.txt and, with the transcript recorder on, transcript.json |
in_memory | in_memory_attempt_store_configuration | enabled (false) | In process memory, lost on restart |
Artifacts are plain files, so a read_only_mounts entry can expose the attempts directory to the REPL.
The attempt_plateau interjection source tells the agent when its attempts stop improving, and attempt_transcript_recorder saves how each attempt came about.
Refinementโ
The refiner edits the agent's harness: a lean agent reads the current memories, skills, instruction texts and sub-agents plus the evidence, and proposes at most max_edits typed edits, which are written to the stores. What it writes is in the system prompt from the next turn on. It is told never to store task answers, scores or references, and never sees a rubric.
It runs two ways:
| Trigger | Configured with | Evidence |
|---|---|---|
| The agent asks | refine_tools_source_configuration | The agent's one-sentence observation |
| The harness checks in | refine_interjection_source_configuration | The transcript so far and the gate agent's instructions |
{
"agent_configuration": {
"tools_provider_configuration": { "direct_only_tools": ["request_refinement"] },
"refiner_configuration": { "refiner_agent_identifier": "refiner", "max_edits": 3 },
"refine_tools_source_configuration": { "enabled": true },
"memory_provider_configuration": { "enabled": true },
"file_memory_store_configuration": { "enabled": true, "file_path": "memories/agent_memory.json" },
"disk_skills_source_configuration": { "enabled": true, "skills_directories": ["skills/"] },
"disk_instruction_source_configuration": { "enabled": true, "instructions_directories": ["instructions"] },
"instructions_provider_configuration": { "writable_source": "disk_instructions" }
}
}
Writable Targetsโ
Each edit kind goes to one provider's writable source. A kind whose provider is off or has nowhere to write is not offered; with no kind available, refine_tools and the refine interjection source turn themselves off.
| Edit kind | Written to | Setting | Default |
|---|---|---|---|
memory: a durable fact | Memory store | memory_provider_configuration.writable_source | First active store |
skill: a repeatable procedure | Skills source | skills_provider_configuration.writable_source | First active source. Set a source that can write skills, such as disk |
prompt_note: a narrow behavioural rule | Instruction source | instructions_provider_configuration.writable_source | First active source that can write: disk_instructions, which writes <name>.md into its first instructions_directories entry |
sub_agent: a specialist the agent can delegate to | Delegable agents source | agent_delegation_provider_configuration.writable_source | First active source that can write. The SDK ships none |
To let the refiner add sub-agents, subclass WritableDelegableAgentsSource (zav.agents_sdk.adapters.agent_delegation): available_tools names the tools sources a new sub-agent may be given, create_agent(name, description, instructions, tools) stores it, and get_agents returns it as a DelegableAgent on the next build.
The request_refinement Toolโ
refine_tools (CLI name refine-tools) adds a ## Refinement prompt section and one tool:
| Tool | Description |
|---|---|
request_refinement | Run the refiner on the agent's observation (a repeated failure or a reusable tactic). Returns what was kept |
Keep it in direct_only_tools so a script in the REPL cannot call it.
za agents capabilities tool-sets configure refine-tools
refine_tools_source_configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Offer request_refinement and the ## Refinement prompt |
defer_tools | bool or null | null | Load the tool on demand, see Tool search |
The Refinerโ
refiner_configuration is shared by both triggers. It has no enabled flag: the triggers switch it on.
| Setting | Type | Default | Description |
|---|---|---|---|
refiner_agent_identifier | str or null | null | Agent that proposes the edits. null runs a fresh copy of the current agent |
max_edits | int | 3 | Edits kept from one refinement; the rest are dropped |
max_transcript_chars | int | 24000 | Characters from the end of the rendered transcript given to the refiner |
ledger_path | str or null | "refinements.jsonl" | File recording every edit. null turns the ledger off |
instructions | str or null | null | Replace the built-in refiner instructions; {kinds} is replaced by the allowed edit kinds |
The refiner must reply with a JSON array of edits, each with kind, name (skills, prompt notes, sub-agents), description (skills, sub-agents), content, tools (sub-agents), trigger and intended_effect.
The Refinements Ledgerโ
Every proposed edit is appended to ledger_path as one JSON line, applied or not. A failed edit is recorded, never raised.
| Field | Description |
|---|---|
id, at | Record id and UTC timestamp |
mode | requested (the tool) or auto (the interjection source) |
focus | The agent's observation or the gate's instructions |
kind, name, description, content, tools | The edit |
trigger, intended_effect | The evidence and what the edit should change |
result | What was written (memory <id>, skill <name>, prompt note <name>, sub-agent <name>) or error: <reason> |
Seeing What the Model Sawโ
The system prompt is rebuilt every turn from the stores and is not part of the transcript, so saved attempt transcripts do not contain it. A tracing_configuration on the agent setup records every model call, system prompt included, with the judge, reviewer, gate and refiner runs nested in the same trace. The file vendor writes one JSON file per trace:
{ "tracing_configuration": { "vendor": "file", "vendor_configuration": { "file": { "directory": "traces" } } } }