feat(agent): add single-step execution and mid-turn resume - #2
Open
moedash wants to merge 7 commits into
Open
Conversation
moedash
marked this pull request as ready for review
August 19, 2026 22:24
moedash
force-pushed
the
moe/step-and-resume
branch
2 times, most recently
from
August 19, 2026 22:31
2037d04 to
442ea6a
Compare
3 tasks
A call left open earlier in the history is normal, and a result appended at the tail for one of those pairs with the wrong call. Errored and aborted messages never reach the provider, so their calls need no result either.
A step is a turn, so it no longer opens a run of its own; the extension turn index kept resetting to zero. The caller needs the outcome to know whether to step again, instead of rebuilding the loop's termination rules.
A step skipped post-run handling, so a stepped run lost its retry and its compaction. The settled result no longer claims the tool failed, because the effect can have landed before the session stopped. The record is written before memory, so a failed write can't leave the two out of step.
Shorter locals and a hoisted role list read better than wrapped calls, and the formatter keeps them.
Driving a turn a step at a time needed two things step() could not do on its own: put a prompt in the transcript without running it, and settle what a stopped turn left behind without running to the end of the turn. resumeInterruptedTurn is now the second one plus that run, so the two paths cannot drift.
moedash
force-pushed
the
moe/step-and-resume
branch
from
September 7, 2026 23:39
989c52f to
5032328
Compare
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.
What changed?
Step-level:
runLoop's inner body moves intorunSingleTurn, andagentStepruns exactly one turn without adding a message.Agent.step()andAgentSession.step()expose it. A step is a turn, so it emits noagent_start, and the session still does its post-run handling each step, so retries and compaction keep working.Starting and recovering a turn without running it:
AgentSession.recordPrompt()builds the turn's messages the wayprompt()does and records them, with no model call.AgentSession.prepareStep()settles the tool calls a stopped turn left with no result (findDanglingToolCalls()finds them) and drops a trailing assistant message that holds no answer, returning whether the turn still has work.Resume:
AgentSession.resumeInterruptedTurn()isprepareStep()plus a run to the end of the turn, for a caller that wants the whole turn back in one call. It never re-adds the prompt.Why?
Driving pi from an external durable executor means checkpointing between steps: start a turn, advance it one step, and pick up a half-finished one, without ever running a whole turn.
prompt()runs toagent_end, so it could only do the first of those.After a hard kill during a tool call, the session ends with an assistant tool-call and no result. That payload is still valid,
transformMessageshandles it. What you can't do is finish that turn:continue()refuses a trailing assistant message, so the only way forward is a new prompt, which changes the conversation.Two details worth flagging. Only the trailing message counts, because an aborted tool batch leaves older calls unresolved on purpose and a result appended at the tail would attach to the wrong call. And a settled call reports the outcome as unknown, not failed, since a crash after the tool ran but before its result landed means the side effect may already have happened.
This can't be an extension. The guard that blocks the resume is in the core loop.
How did you test it?
npm run checkpasses../test.shhas pre-existingfindtool failures that need thefdbinary; they fail the same way onmain.End to end: ran pi under a Temporal worker, killed it with
kill -9mid tool call, and the retry finished the turn with the prompt not re-added. That run droveresumeInterruptedTurn();recordPrompt()andprepareStep()are covered by unit tests so far.