Skip to content

fix(transport,loop): Stop records turn.aborted; keep non-streamed tool narration - #19

Merged
thatswiftdev merged 2 commits into
mainfrom
wt/abort-and-tool-text
Oct 23, 2026
Merged

thatswiftdev merged 2 commits into
mainfrom
wt/abort-and-tool-text

Conversation

@thatswiftdev

Copy link
Copy Markdown
Contributor

Two fixes found while porting the Lionidas agent chat (Lionidas #131).

1. A Stop is recorded as turn.aborted, not turn.failed

Before this change, the conversation transport recorded every caught error as turn.fail, including an explicit Stop (POST …/abort) or a client disconnect. Readers therefore showed "turn N failed: aborted by caller". turn.aborted was only ever written on the ACP path.

  • When the loop error's cause is caller-abort, the transport now finalizes with turn.abort {reason}. This applies to both the authority path and the sink-only path.
  • ExecutionAuthority.finalizeOwned accepts kind: 'turn.abort' (+ reason) and counts turn.aborted as a written terminal.
  • Unchanged: the response envelope (code: 'ABORTED'), the ledger settle code, session accounting (an abort still sets the session to error, per the existing regression guard), and the queue drain after the turn.

2. Text written beside tool calls by a non-streaming provider is kept

A non-streaming provider (openai-compat) returns its narration ("Looking at the case library first.") only as message content next to tool_calls. Nothing streamed, so no delta or block reached the durable log, and the text was lost from replays.

  • The loop now emits that text once as a delta when a hop streamed nothing and has tool calls. That produces block start/delta/end before the calls, which is exactly what a streaming provider produces.
  • Unchanged: streamed hops (no duplication) and final answers (their text still rides turn.completed.answer).

Tests

  • tsc clean; 316 passed (313 existing + 3 new), 5 skipped.
  • New coverage: Stop → turn.aborted {reason: 'aborted by caller'} on both paths; a provider failure stays turn.failed; non-streamed narration lands once and before its tool call; streamed narration is not duplicated.
  • 10 source mutations were tried and all 10 were caught.

Lionidas re-pins to this branch in a follow-up PR. Lionidas already renders turn.aborted as "stopped", so it needs no code change.

…l narration

- conversation transport: an explicit Stop / client disconnect (LoopError
  cause 'caller-abort') finalizes with turn.abort {reason} instead of
  turn.fail, on both the authority and sink-only paths. Envelope code,
  ledger settle code and session accounting are unchanged.
- ExecutionAuthority.finalizeOwned accepts kind 'turn.abort' (+ reason)
  and counts turn.aborted as a written terminal.
- loop: a hop that streamed nothing but returns text beside its tool calls
  emits that text once as a delta (block start/delta/end before the
  calls) - previously a non-streaming provider's narration never reached
  the durable log. Streamed hops and final answers are unchanged.

Tests: Stop -> turn.aborted on both paths, provider failure stays
turn.failed, non-streamed narration lands once and before its call,
streamed narration is not duplicated (10 source mutations, all caught).
@thatswiftdev
thatswiftdev merged commit 00b688b into main Oct 23, 2026
1 check passed
@thatswiftdev
thatswiftdev deleted the wt/abort-and-tool-text branch October 23, 2026 06:24
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