milk — specification¶
Overview¶
milk lets you switch between a primary inference agent and a configurable escalation agent mid-workflow, maintaining full session context across the switch. Any backend can serve either role — see docs/providers.md for the full catalog and the one routing constraint milk expects you to honor (escalation should be smarter/pricier than primary).
The primary use case is code assistance and shell automation for a single user.
This page is the architecture/CLI reference. For task-oriented docs, start at docs/getting-started.md; see also docs/workflows.md, docs/tooling.md, docs/operations.md, and docs/eval.md.
Architecture¶
Components¶
milk [prompt | flags]
│
▼
┌─────────────────────────────────────────────────────┐
│ Session State Machine │
│ ROUTING → LOCAL | ESCALATION | ESCALATION_WAITING │
└──────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────┐
│ Router │
│ 1. Explicit flags │ --escalate, --primary
│ 2. Session state check │ ESCALATION_WAITING → bypass
│ 3. Rules layer │ heuristics + weighted scorer
│ 4. Primary model │ primary model self-classification
│ 5. Default: try local │
└────────┬─────────────────┘
│
┌────┴──────────┐
▼ ▼
PRIMARY ESCALATION
agent agent
(any AgentConfig) (any AgentConfig)
local / bedrock / local / bedrock /
claude-cli / … claude-cli / …
Routing mechanics, session states, and the native /workflow engine are covered in docs/workflows.md.
Project layout¶
cmd/milk/
main.go Cobra root command (single-prompt mode); builds TurnRunner instances
repl.go bubbletea TUI — transcript viewport, textarea, status bar, /agent switch
runner.go TurnRunner interface + three implementations (localRunner, cliRunner, subprocessRunner)
dispatch.go runPrimary / runEscalation — role-specific session bookkeeping shared by single-shot and TUI
interactive.go slash commands, tab completion, prompt label helpers
ansi.go ANSI colour helpers and activity spinner
panel_memory.go right-side memory panel (/panel memory)
panel_workflow.go workflow progress panel (/panel workflow)
workflow_cmd.go /workflow slash command + interactive wizard
workflow_runner.go workflowTurnRunner adapter (wraps TurnRunner with per-role session tracking)
internal/
workflow/ workflow engine: Workflow interface, TurnRunner adapter interface, State/verdict/turn
definitions (dev.yaml, pair.yaml, swarm.yaml); interpreter (interp/)
config/ config loading and defaults (~/.milk/config.json)
session/ session state machine + JSON store (~/.milk/sessions/)
router/ routing: rules layer → weighted scorer → primary-model classifier
agent/local/ OpenAI-compat HTTP client; Bedrock Converse native path; SigV4/Bearer/token_cmd
auth transports; tool loop; streaming tool-format FSM detector
agent/claude/ claude CLI subprocess driver; stream-json parser; permission-prompt protocol
agent/subprocess/ generic subprocess agent: NDJSON stream protocol, tag interception
(<milk:need:>, <milk:percept:>, <milk:escalate:>)
agent/aider/ aider-cli provider (wraps subprocess agent)
agent/smolagent/ subprocess provider (wraps subprocess agent)
escalation/ context builders: static instruction block + dynamic summary sent to escalation agent
memory/ Percept store; NREM decay/prune/promote consolidation (~/.milk/memory/)
obs/ OpenTelemetry file exporters (~/.milk/otel/)
claudesettings/ ~/.claude/settings.json reader (allowed tools, directories, AWS refresh command)
oversight/ remote oversight interface (Telegram notifier)
tags/ milk tag constants (<milk:need:>, <milk:percept:>, <milk:escalate:>)
Agent dispatch layers¶
TurnRunner.Execute() provider-specific inference (one of three implementations)
│
runPrimary / runEscalation role-specific session bookkeeping (dispatch.go)
│
run() / runTurn() single-shot or TUI entry point; builds runners, drives router
Any agent can be exposed as a callable tool to any other agent (Agent-as-Tool) — see docs/tooling.md.
Claude CLI escalation mechanics¶
Implementation detail for the claude-cli provider specifically — see docs/providers.md — Claude Code CLI for configuration, and docs/workflows.md — Context handoff for the cross-provider handoff format.
- AWS credential injection: when
aws_auth_refresh: true, milk reads theawsAuthRefreshcommand from~/.claude/settings.json, runs it before each turn for fresh STS credentials, and injects them as explicitAWS_*env vars into the subprocess. Conflicting inherited vars (AWS_BEARER_TOKEN_BEDROCK,ANTHROPIC_DEFAULT_*_MODEL,AWS_PROFILE, etc.) are stripped to prevent wrong-account overrides. See ADR 23. - First escalation turn:
claude --print --output-format stream-json \ --session-id <new-uuid> \ --append-system-prompt-file <static-ctx> \ --append-system-prompt-file <dynamic-ctx> \ -- "<user prompt>" - Subsequent turns (
--resume):claude --print --output-format stream-json \ --resume <escalation-session-id> \ --append-system-prompt-file <dynamic-ctx-if-changed> \ -- "<user prompt>" session_idis extracted from the first NDJSON message and persisted to the milk session file.
Permission prompt flow¶
milk passes --permission-prompt-tool stdio on every invocation. When Claude wants to use a tool that isn't pre-approved, it emits a control_request NDJSON event and pauses; milk intercepts it and, in TUI mode, routes a blocking prompt through the bubbletea message queue (ADR-0015):
- The agent goroutine calls
tuiInputReader.readLine(prompt)and blocks on a channel. - The TUI appends the prompt to the transcript and switches key events to
handlePermKey. - The user types
y/nand Enter; Ctrl-C sendsn. - milk writes a
control_responseJSON to Claude's stdin; the agent goroutine unblocks.
The prompt shows the tool name, key arguments, and — for workingDir blocks — the restricted path. The session stays alive throughout; no --resume round-trip needed.
dangerously_skip_permissions bypasses this flow entirely (auto-approve all); /skip-permissions on|off overrides it per session without restarting. allowed_tools/add_dirs pre-approve specific tools/directories without ever prompting.
Pre-flight "Stream closed" failures¶
A second failure class sits before the permission-prompt handler: Claude Code's directory-trust pre-flight check. When it fires, the tool returns "Stream closed" — no control_request event, no prompt, the turn ends silently with partial output. milk handles this in two layers:
- Baseline pre-approval:
Bash,Read,Write,Editare always merged into--allowedToolson every invocation;/tmpis always added to the trusted directory list alongsidecwd. Eliminates the failure for most fresh-workspace first turns. - Post-turn detection and retry: milk's stream parser tracks
type:"user"NDJSON messages; atool_resultblock whose content is"Stream closed"is correlated back to the tool name via a per-turn registry and recorded. After the turn, milk presents each failed tool, offers a tool/directory grant, persists it to~/.claude/settings.json, and retries via--resume. See ADR-0035.
CLI Interface¶
Usage¶
milk # interactive REPL mode
milk [flags] <prompt> # single-prompt mode
Interactive mode¶
milk with no prompt argument starts a REPL built on charmbracelet/bubbletea. The input prompt uses ❯ as the prefix. The status bar reflects the current routing state and active agent.
Slash commands: /escalate, /primary, /new, /drop, /list, /paste, /skip-permissions, /agent, /colorize, /think, /need, /workflow, /config, /open, /update, /help, /exit
Memory commands: /learn <statement>, /memory [global|session|<pattern>], /memory show <pattern or #id>, /forget <pattern or #id>, /export [json|<path>] — see docs/operations.md — Memory.
Panel commands: /panel memory, /panel workflow, /panel tasks — see docs/operations.md and docs/workflows.md.
/skip-permissions toggles dangerously_skip_permissions for the current session: on makes the escalation agent auto-approve all tool uses; off (default) re-enables per-tool prompting. Alone, shows current state. A red warning banner appears at startup if already on via config.
/agent manages agent backends at runtime:
| Subcommand | Action |
|---|---|
/agent |
Show active primary and escalation backends |
/agent list |
List all configured backends; active marked with * |
/agent switch <name> as primary\|escalation |
Switch role to the named backend (prompts if args missing) |
/agent add |
Add a backend via interactive wizard |
/agent add name=… url=… model=… [provider=…] [api_key=…] [aws_region=…] |
Add inline |
/agent tool list [<agent>\|global] |
List tool-agents (effective merged for primary by default) |
/agent tool enable\|disable <tool> [for <agent>\|global] |
Enable/disable a tool-agent entry |
/agent tool add <tool> description=<desc> [for <agent>\|global] |
Add a new tool-agent entry |
/agent tool remove <tool> [for <agent>\|global] |
Remove a tool-agent entry |
New backends are appended to agents in ~/.milk/config.json immediately; /agent switch assigns a role in the current session.
/colorize controls transcript syntax and Markdown rendering:
| Subcommand | Action |
|---|---|
/colorize |
Show current mode |
/colorize off |
Disable all colorization |
/colorize fenced |
Highlight fenced code blocks only |
/colorize balanced |
Fenced blocks + inline Markdown (default) |
/colorize full |
Full glamour Markdown render — experimental |
Persisted to config immediately, effective on next render.
/think controls reasoning/thinking token visibility: /think (show current), /think on (show inline), /think off (show [thinking…] placeholder). Retroactive — both transcript variants are maintained in parallel during streaming, so toggling is instant. Default via show_reasoning in config (default true). Applies to both primary-model <think> blocks and Claude extended thinking.
/need sets the current session goal (/need <one-sentence goal>); the primary agent calls this automatically when the user states a new objective. Shown in the memory panel and injected into escalation context.
/workflow runs a named multi-agent pipeline — see docs/workflows.md — The native /workflow engine.
/config manages configuration:
| Subcommand | Action |
|---|---|
/config |
Print current config JSON in the transcript |
/config init |
Run the interactive setup wizard |
/config open |
Open ~/.milk/config.json in the configured editor |
Same commands on the CLI as milk config, milk config init, milk config open. Editor selection uses config_editors (see below).
milk otel manages observability settings — see docs/operations.md — Observability.
/open opens any file in the configured editor: /open <path> (or /open @<path>, @ stripped automatically). The agent can also open files via the open_file tool.
/update checks GitHub for newer milk releases, compares versions, and (with confirmation) downloads and installs the appropriate binary for the current platform.
Multi-line input: Shift+Enter/Alt+Enter inserts a newline; Enter submits. Bracketed paste is handled transparently.
Clipboard binary paste: when a bracketed paste yields empty text, milk probes the system clipboard (xclip/wl-paste) for non-text content (e.g. image/png); found content is saved to a temp file and staged as a pending attachment, same as /attach. No special key combination beyond a normal paste gesture.
Keyboard: Up/Down navigates input history (single-line mode); Ctrl-C clears a pending force-mode flag or exits; Ctrl-D exits.
Flags¶
| Flag | Description |
|---|---|
--escalate |
Force route to escalation agent for this turn |
--primary |
Force route to primary agent for this turn; breaks ESCALATION_WAITING state |
--new |
Start a new session (old sessions for cwd untouched) |
--session <name> |
Target session by name (resume or create) |
--continue |
Alias for default resume behavior (explicit) |
--list |
List sessions for current cwd |
--list --all |
List all sessions across all directories |
--drop |
Delete current session |
Configuration¶
~/.milk/config.json — see docs/providers.md for the full agents/provider field reference, docs/workflows.md for the rules block, and docs/tooling.md for agent_tools/mcp_servers. This section covers the remaining root-level behavior fields.
{
"agent": "local",
"agents": [ { "name": "local", "url": "http://localhost:8080", "model": "qwen2.5-coder", "provider": "local" } ],
"escalation_agent": "claude",
"default_route": "local",
"colorization": "balanced",
"show_reasoning": true,
"sticky_escalation": true,
"experimental_lazy_history_management": false,
"aws_auth_refresh": false
}
agent names the active primary backend; empty defaults to the first non-claude-cli entry. escalation_agent selects which entry handles escalated turns (default "claude") — a built-in claude-cli entry named "claude" is always available even if unlisted.
colorization field¶
See /colorize above for the four modes and their behavior.
show_reasoning field¶
Default visibility of thinking/reasoning tokens; see /think above.
config_editors field¶
Ordered list of editor commands tried by /config open and /open; first found on $PATH wins. Env vars ($EDITOR, $VISUAL) are expanded before lookup. Default: ["$EDITOR", "$VISUAL", "nano", "vim", "vi"].
"config_editors": ["code --wait", "$EDITOR", "nano"]
experimental_lazy_history_management field¶
When true, agent context is built lazily: only the current agent's own turns and user turns directed at it are included verbatim; contiguous blocks of other-agent turns collapse into a placeholder (e.g. [escalation]: (14 turns omitted)), retrievable on-demand via get_session_context. Significantly reduces context size for sessions with heavy cross-agent activity. User turns always show [user to <agent>] labels regardless. Default: false.
Streaming¶
Both agents stream output in real time: SSE from OpenAI-compat APIs (stream: true), AWS binary event-stream from Bedrock Converse, or NDJSON from Claude's --output-format stream-json. milk relays tokens to stdout as they arrive.
Backlog¶
- Planning mode (offline, no LLM execution)
- Demotion from escalation back to primary mid-session
- Web UI / TUI
- MCP stdio transport for local tools ✓ (done)
- Multi-user / daemon mode