Skip to content

feat(coding-agent): let an extension run the turn - #3

Open
moedash wants to merge 3 commits into
moe/step-and-resumefrom
moe/turn-executor
Open

feat(coding-agent): let an extension run the turn#3
moedash wants to merge 3 commits into
moe/step-and-resumefrom
moe/turn-executor

Conversation

@moedash

@moedash moedash commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

What changed?

pi.registerTurnExecutor(executor, options?). Pi hands the turn to the executor instead of running it, and the executor decides when to call turn.run(). run() is the same turn pi would have run, so the transcript, the events and the streaming do not change. An executor that returns without calling it holds the turn.

With resumeOnStart, opening a session also hands the executor a turn an earlier run left unfinished, which routes through resumeInterruptedTurn() and so through the executor like any other turn.

One executor wins, the first registration. None registered leaves pi running turns itself, which is every existing session.

Stacked on #2, which added prepareStep() and resumeInterruptedTurn().

Why?

Nothing outside pi can decide when a turn happens. prompt() runs the turn the moment it accepts the text, and an extension has no handle on the AgentSession to drive it instead: ExtensionContext gives a read-only session manager. So an extension that wants turns to go through a scheduler, or a durable executor, has to intercept input and run the work in a session of its own, which leaves the transcript you are looking at empty.

This is the seam that avoids that. The executor runs in this process against the live session, so what the user sees is pi's own turn.

resumeOnStart is the other half. A crash during a tool call leaves a call with no result and pi does not finish it, so the durability only shows up if something picks that turn up when the session opens again.

How did you test it?

  • Unit Tests
  • Staging
  • End to End Tests

npm run check passes. New tests cover the default (no executor registered), first-registration-wins, a wrapped turn producing the same transcript as an unwrapped one, a held turn staying unrun, resume-on-start finishing an interrupted turn without re-prompting, and a finished session being left alone.

pi runs a turn the moment a prompt is accepted, so nothing outside it can decide when the
turn happens, and an extension has no handle on the session to do it itself. registerTurnExecutor
hands the turn over instead. run() is the same turn pi would have run, so the transcript, the
events and the streaming do not change.
A crash during a tool call leaves a call with no result, and nothing finishes it. An executor
that asks for resumeOnStart gets that turn when the session is opened again, which is the half
of durable execution that only shows up after something has already gone wrong.
`_drive` asks the runner whether an extension took the turn over, and a double built before that call has no method to answer with.
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.

1 participant