Skip to content

feat(agent): adopt the stable request prefix - #136

Merged
wolfy-j merged 33 commits into
masterfrom
feat/adopt-stable-prefix
Nov 4, 2026
Merged

wolfy-j merged 33 commits into
masterfrom
feat/adopt-stable-prefix

Conversation

@wolfy-j

@wolfy-j wolfy-j commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Adopts the stable request prefix from the framework, so requests within a run extend each other byte for byte and thinking and the prompt cache survive across steps. Stacked on #135.

Changes

  • Recall and lifecycle notes are stored where the request sent them, before the step's action, and replayed in place.

  • The time_aware note is rendered once when a node starts and committed before its first request. A restart replays the same minute, and a checkpoint keeps the note after the input.

  • Each action stores prefix_fingerprint, and steps pass drop_stale_thinking, so thinking bound to another prefix is stripped before the request.

  • Conversation inputs fold in key order, so a restart rebuilds the same user message.

  • The first user message is folded once into an agent.input row and replayed, so a later input cannot change it.

  • A persisted agent or model selection wins over the agent_id and model inputs on restart, and a _control.config switch commits with the results that carry it.

  • A checkpoint removes tool results together with their calls. The summarizer receives them, and each later checkpoint summarizes the previous summary plus the rows after its cut.

  • A skipped checkpoint's audit row stays out of the prompt. Iteration warnings and _control.memory additions land after all results of the round.

Behaviour changes

  • Replaced test contract: agent_checkpoint_test asserted that structured tool results survive a cut. That left a tool_result with no tool_use on the wire. Results now survive only through the summary.
  • retained_result_count is gone from checkpoint refs, function args and marker metadata.
  • _control.memory.delete refuses an existing row the directive did not add. A row that is already gone is a no-op.
  • A node with no conversation input, note or history now fails with INPUT_VALIDATION_FAILED instead of sending an empty user text.
  • A text turn below min_iterations now gets a feedback row, so the next request does not end on an assistant turn.
  • The default summarizer's max_tokens is now max_memory_chars * 2 (a cap, clamped to the model's output limit), and a summary cut off by that cap is a failed checkpoint with no cut.
  • Stored runs with text between a call and its result fail on resume. The README's upgrade notes cover this.
  • A lifecycle message with a user or assistant role now fails the node with an error naming the phase, agent and role.
  • A system lifecycle message is a positional note and no longer joins the top-level system prompt.
  • An activation note stays in the history of later turns.
  • Stored runs lose earlier thinking and restart the cache once after upgrade. The README covers this.

Tests

src/node/agent/agent_stable_prefix_test.lua, agent_checkpoint_test.lua, node_context_test.lua, and new restart scenarios stable_prefix and round_* that kill the runtime and check captured requests or stored rows. Against a local integration of the framework changes: SQLite passes 1554 tests on the published framework and lint is clean. All 17 restart scenarios and PostgreSQL 1552 passed on the commit before the last two. No live provider was called.

Merge order

Requires Agent >=v0.5.10 and LLM >=v0.5.13, both released. Merge after #135 and before #134: #134 makes checkpoints fire on cached agents, which needs the checkpoint fix here.

The agent node built its tool caller with the sequential default, so the
calls of one model turn ran one after another; the session host already
runs them in parallel. The node selects the parallel strategy; results
stay keyed by call id and are assembled in the model's order.
A recovered action runs its unresolved calls through the same parallel
caller, and a stored error observation is reused rather than re-running
the failing call. The observation for a failed call stores the error's
message: the executor reports failures as error values, which only the
commit encoder turned into text.
The node registers a result listener on the tool caller and commits a
call's observation as soon as that call completes, so a restart re-runs
only the calls still in flight. Results carrying a control directive stay
with the after-batch pass, which applies directives in model order and
skips calls already committed. In a round with an exit call only argument
failures commit early, because a valid finish drops sibling observations.
A failed commit fails the round. Recovery reuses the tool call node a
previous run committed for an unresolved call.

The restart proof gains a parallel_tools scenario: the runtime is killed
after the fast call's observation commits while the slow call runs, and
the second runtime completes with the fast call run once and the slow
call re-run under its original id.

Requires the wippy/agent tool caller result listener.
A tool that reports failure as data leaves its table in the result beside
the error, and the node stored that table, so the model read a table
address instead of the error. An error now stores its message as text, and
the observation and the tool call node share one stringification.

Recovery resolves a call by an observation of the action's own iteration:
call ids are unique within one turn only, and an id an earlier turn used
marked the newer call resolved.

The recovery tests gate their interruption on the slow call having started
and prove cancellation by waiting past the tool's delay.
Call ids are unique within one turn only, so a provider reusing a delegate
call id in a later turn resumed the earlier turn's child. Each child records
its iteration and recovery matches on it. A child persisted without an
iteration still matches by call id, so an in-flight run keeps its child
rather than repeating the work.

The match covers agent children only: the tool call node of a tool that
delegates through a control directive carries the same call id and was
taken for the child.
Store recall notes, the turn context note and lifecycle notes where each request
sent them, so a later request extends the previous one. Store the prefix
fingerprint with the action and strip thinking bound to another prefix before
each request. A user or assistant lifecycle message now fails the node, and a
system lifecycle message is a positional note instead of joining the top-level
system prompt. Requires Agent 0.5.10 and LLM 0.5.13.
A restart rebuilds the user message from the node inputs, and pairs order differs
between tables, so the same inputs could produce different request bytes.
…restarts

A checkpoint replaces its cut rows, calls and results together: results no longer
outlive their calls, and the summarizer receives them. A skipped checkpoint's audit
row stays out of the prompt. A persisted selection beats the agent and model inputs
on a restart, a control switch commits with its results, and the iteration warning
and memory additions follow every result of a round. The first user message is
pinned at the first commit, developer rows stay separate messages, and a memory
delete can only remove rows the memory directive added.
The stable_prefix restart scenario kills a runtime during the first request after a
committed agent switch, adds a late input, and compares the captured requests from
both runtimes.
A later checkpoint received every row since the node started, so its payload grew
without bound and could exceed the summarizer's own context. Each checkpoint now
summarizes the earlier summary and the rows after its cut. A summary with nothing
after it is left alone.
… restarts

The earlier summary reaches the default summarizer whole, with a merge instruction.
A round's memory rows, warning and pending controls commit before the node waits on
children, and additions behind a delegation wait in the node metadata. The cut stops
before an action with an unresolved call. A delete of a row already gone is a no-op.
A run stored before the input pin keeps its input model, and the run context reads
the pin. The round_children and round_delegate restart scenarios kill a runtime while
the round waits.
…by call

No checkpoint runs while an action has a call with no result, and the closed-row
recomputation is gone. Pending memory additions are keyed by call id, so a re-run call
replaces its own and a sibling's survive whether or not the re-run still delegates. A
summary the model cut off is a failed checkpoint, and the merge instruction states the
budget. The run context reads rows in request order under the same row identity as the
cut. A pinned run takes its model from the config, and a run stored before the pin from
its state.
…ical retries

The default max_tokens follows max_memory_chars, and every summary states what fits.
A failed checkpoint is not repeated for the same rows within a process. Only the newest
action can hold a checkpoint back. A resume stored before the pin keeps a config newer
than its state. The clamp counts characters and never splits one, and history dedupes
on a row's own id.
…ecovery

text_prefix walks bytes within a bound set by the cap. Table outputs reach the model as
JSON. A compaction requested in a round that ends through the finish tool commits with
the completion. A text turn below min_iterations gets feedback, and a node with no input
sends no empty user message or fails naming why. A round killed between its last result
and its closing commit is settled on recovery. The default max_tokens is twice the
budget, and the stated budget follows the model's output limit. Inline agents keep their
model on upgrade, and kept rows stay out of the run context's character budget.
Its prompt builder reads the database, and Postgres rejects an id that is not a UUID.
…reads

The default max_tokens follows the model's window and output limit, with a ceiling, and
the stated budget counts one token per character. An empty fold pins arena.prompt. A
strict checkpoint failure no longer discards a completing node's accepted output.
max_chars is a hard bound again: the summary and newest rows stay, and the input gets
what room is left. The settle payload matches the live one, a round with no calls
validates, a delegate with no input fails, and whitespace-only text is not sent.
…ary to the window

The default max_tokens leaves most of a small window to the summarizer's prompt, and the
stated budget is three quarters of it. max_chars accumulates from the oldest row again and
charges call arguments; the input and turn note take the room left. The settle payload
uses the validated call shape, delegate input has one predicate, and summaries and a
pinned arena prompt need visible characters.
…ults unique

A directive's child set commits before any yield directive waits, so recovery collects
the output. Artifact ids come from the call, so a re-run stores each once. A call that
returns several delegates fails the node, because each result would answer that one call.
The checker does not settle on the closure over the node double, so the test sets the id inline.
@wolfy-j
wolfy-j requested a review from skhaz November 4, 2026 18:21
Base automatically changed from feat/parallel-tool-calls to master November 4, 2026 20:39
@wolfy-j
wolfy-j marked this pull request as ready for review November 4, 2026 20:39
@wolfy-j
wolfy-j merged commit 5963871 into master Nov 4, 2026
8 of 15 checks passed
@wolfy-j
wolfy-j deleted the feat/adopt-stable-prefix branch November 4, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants