Repository navigation
feat(agent): adopt the stable request prefix - #136
Merged
Merged
Conversation
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.
…n the accepted answer
…y when a round settles
skhaz
approved these changes
Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_awarenote 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 passdrop_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.inputrow and replayed, so a later input cannot change it.A persisted agent or model selection wins over the
agent_idandmodelinputs on restart, and a_control.configswitch 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.memoryadditions land after all results of the round.Behaviour changes
agent_checkpoint_testasserted that structured tool results survive a cut. That left atool_resultwith notool_useon the wire. Results now survive only through the summary.retained_result_countis gone from checkpoint refs, function args and marker metadata._control.memory.deleterefuses an existing row the directive did not add. A row that is already gone is a no-op.INPUT_VALIDATION_FAILEDinstead of sending an empty user text.min_iterationsnow gets a feedback row, so the next request does not end on an assistant turn.max_tokensis nowmax_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.userorassistantrole now fails the node with an error naming the phase, agent and role.systemlifecycle message is a positional note and no longer joins the top-level system prompt.Tests
src/node/agent/agent_stable_prefix_test.lua,agent_checkpoint_test.lua,node_context_test.lua, and new restart scenariosstable_prefixandround_*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.10and 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.