Skip to content

feat(loop): add awaited mutator lifecycle callbacks - #32

Open
danielkov wants to merge 2 commits into
mainfrom
feat/loop-mutator-lifecycle
Open

danielkov wants to merge 2 commits into
mainfrom
feat/loop-mutator-lifecycle

Conversation

@danielkov

Copy link
Copy Markdown
Owner

Summary

Extend LoopMutator with awaited, protocol-neutral lifecycle callbacks for sessions, input, logical turns, model calls, progress, and resolved tool batches. Existing transcript mutators remain compatible through default no-op callbacks.

Motivation

Hosts need to intercept values before the loop consumes or commits them. Synchronous observer notifications cannot return effective inputs or responses, and public driver steps do not expose every logical-turn boundary.

Technical details

Editable payloads and consistent commits

Typed payloads separate mutable content and request options from immutable identity and accounting. Turn-start edits are staged and validated; model-request edits are inference-local. Model-response and final-turn edits reach the committed transcript, returned result, terminal notifications, and output capture consistently. Transcript replacements have an explicit observer notification.

Resumable lifecycle delivery

Logical-turn callbacks span model and tool continuations rather than repeating per inference. Driver-owned terminal state retains candidate edits and callback progress across interrupted finalization and explicit closure. Completed callbacks are not repeated; an interrupted callback may be retried. Original execution failures retain precedence over terminal notification failures.

Input admission and session closure

submit_input_async provides awaited admission, while synchronous submission retains queued batches until dispatch. Rejection removes only the rejected batch. close explicitly retires active work and delivers session-end callbacks; it is not tied to prompt completion or asynchronous work in Drop.

Individual and nested tool interception continues to use the executor boundary. Compaction and application-owned configuration or model-selection operations retain their own extension seams.

@danielkov
danielkov force-pushed the feat/loop-mutator-lifecycle branch from 22179a3 to 733994d Compare October 8, 2026 09:09
Narrow the LoopMutator extension to three awaited hooks that intercept
values before the loop consumes or commits them: on_session_start,
on_model_request, and on_model_response.

Remove the read-only lifecycle notifications (turn start/finish/end,
model progress/error, tool progress/batch, session end), queued input
admission and submit_input_async, explicit close, resumable terminal
delivery, transcript rewrite notifications, and the Clone derive on
LoopError. Read-only notifications belong in LoopObserver. This also
removes the expect-based state machines and behaviour that differed
depending on whether a mutator was registered.

Hooks fail closed: an error or invalid edit aborts the operation without
committing anything from it. LoopError::Cancelled from a hook finishes
the turn as cancelled, matching mutate and model-stream cancellation.

Hooks run inside the chat span, so gen_ai.input.messages and
gen_ai.output.messages capture the final edited messages and hook
latency counts toward inference time.

MutationPoint::AfterTurnEnded gets a doc fix only: it has always run
before the first inference of a new turn, after pending input is
appended.
@danielkov
danielkov force-pushed the feat/loop-mutator-lifecycle branch from 733994d to 9afba53 Compare October 8, 2026 09:10

This branch has not been deployed

No deployments
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