ADR 0031 — Local Agent Percept Writing, Dynamic Consumer Hints, and CurrentNeed Early-Exit Fallback¶
Date: 2026-05-30
Status¶
Accepted — partially supersedes ADR-0022
Context¶
ADR-0022 established bidirectional memory for the escalation (Claude) agent: it reads percepts via
[Remembered facts] injection and writes percepts via nonce-tagged stream interception. Two gaps
remained:
-
Local agent had no write path. Important facts surfaced during local-agent turns (reasoning traces, discovered constraints, user preferences) were lost unless the user explicitly called
record_memory. The write path was escalation-only. -
Consumer hints were hardcoded.
MemoryInstructiontold agents to use@local:or@claude:prefixes, but agent names are configurable (agents[].namein~/.milk/config.json). A deployment with a primary agent namedllamaand escalation agent nameddeepseekwould never match the hardcoded prefixes, so all percepts fell through asConsumerAllregardless of the intended target. -
CurrentNeedmissed theisRepeatedPromptearly-exit path. When the local agent detects that the user has repeated a prompt it just answered, it exits before any streaming begins —onNeedis never called.Session.CurrentNeedremained stale (or blank for a new session), so the escalation agent received no current-need context on handoff.
Decision¶
Local agent percept writing. runLocal now calls agent.WithTagCallbacks before handing off to
agent.Run. The same TagWriter / PerceptWriter stream-interception pattern used for the Claude
agent is applied: <milk:need:NONCE> tags update Session.CurrentNeed; <milk:percept:NONCE> tags
are recorded with ProducerLocal. Consumer routing follows the same switch on consumerHint:
primary-targeted percepts → ConsumerLocal; escalation-targeted percepts → ConsumerEscalation;
unknown/no hint → ConsumerAll (shared between both agents).
Dynamic consumer hint names. MemoryInstruction(nonce, primaryName, escalationName string) now
embeds the actual configured agent names in the instruction text. The @local: / @claude:
examples are replaced with @<primaryName>: / @<escalationName>:. ConsumerHintFrom and
consumerHintFrom now accept a names ...string variadic rather than a hardcoded list, so the
parsers match whatever names are passed in at runtime.
WithTagCallbacks (local agent) and WithOnPercept (Claude agent) both accept primaryName and
escalationName, store them in agentNames []string, and forward them to PerceptWriter /
perceptWriter respectively.
CurrentNeed early-exit fallback. In the isRepeatedPrompt escalation branch of runLocal,
after the repeat is detected but before handing off to the escalation agent, the code now sets
sess.CurrentNeed = prompt if CurrentNeed is still empty. This ensures the escalation agent
always has at minimum the literal prompt as its current-need context, even when no <milk:need>
tag was emitted during the (skipped) streaming phase.
Consequences¶
- Both agents can now contribute facts to the shared memory store. Local-agent percepts arrive with
ProducerLocalattribution; escalation-agent percepts withProducerEscalation. - Consumer hint routing is now configuration-aware. Deployments with non-default agent names see
correct
@<name>:prefix matching. - The
CurrentNeedfield is reliable on all escalation paths, includingisRepeatedPromptearly exits. - The local agent's percept nonce is generated fresh per turn (same
claude.GenerateNonce()call), consistent with the escalation agent's per-turn nonce.