@code-yeongyu/senpi-ai 2026.10.26-1 → 2026.10.26-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,48 @@
1
+ import { assistantEvents, carryAssistantCapture, dropAssistantCapture, pushAssistantProgress, } from "../../../utils/assistant-capture.js";
1
2
  import { AssistantMessageEventStream as AssistantMessageEventStreamImpl } from "../../../utils/event-stream.js";
2
3
  import { recoverKimiXtmlThinking } from "./thinking-recovery.js";
3
4
  export function wrapStreamWithKimiThinkingRecovery(innerStream) {
4
5
  const outerStream = new AssistantMessageEventStreamImpl();
6
+ let partial;
7
+ // Ended thinking index -> whether the terminal rewrite inserts a recovered response block after it.
8
+ const splitsByThinkingIndex = new Map();
9
+ // Blocks that lost a record once lose all later ones, so a block never carries records without its start.
10
+ const droppedIndexes = new Set();
11
+ // Live events keep source indices; only the terminal is rewritten. A record (event capture or control) crosses only
12
+ // while its source index names an existing block that no earlier split can move; otherwise it is dropped, never
13
+ // guessed.
14
+ const keepsIndex = (contentIndex) => {
15
+ if (!partial || contentIndex >= partial.content.length)
16
+ return false;
17
+ if (droppedIndexes.has(contentIndex))
18
+ return false;
19
+ for (let index = 0; index < contentIndex; index += 1) {
20
+ if (partial.content[index]?.type === "thinking" && splitsByThinkingIndex.get(index) !== false) {
21
+ droppedIndexes.add(contentIndex);
22
+ return false;
23
+ }
24
+ }
25
+ return true;
26
+ };
5
27
  void (async () => {
6
28
  try {
7
- for await (const event of innerStream) {
29
+ // Every event but the terminal crosses unchanged: same event objects, same indices, controls in order.
30
+ for await (const event of assistantEvents(innerStream, (progress) => {
31
+ if (keepsIndex(progress.contentIndex))
32
+ pushAssistantProgress(outerStream, progress);
33
+ })) {
34
+ if ("partial" in event)
35
+ partial = event.partial;
36
+ // The same event object crosses, so public bytes stay identical; only its record is withheld.
37
+ if ("contentIndex" in event && !keepsIndex(event.contentIndex))
38
+ dropAssistantCapture(event);
39
+ if (event.type === "thinking_end") {
40
+ const alone = { ...event.partial, content: [{ type: "thinking", thinking: event.content }] };
41
+ splitsByThinkingIndex.set(event.contentIndex, recoverKimiXtmlThinking(alone).content.length > 1);
42
+ }
8
43
  if (event.type === "done") {
9
44
  const message = recoverKimiXtmlThinking(event.message);
10
- outerStream.push({ type: "done", reason: event.reason, message });
45
+ outerStream.push(carryAssistantCapture(event, { type: "done", reason: event.reason, message }, message));
11
46
  outerStream.end();
12
47
  return;
13
48
  }
@@ -1,4 +1,5 @@
1
1
  import type { AssistantMessageEvent } from "../types.ts";
2
+ import { type CaptureHint } from "../utils/assistant-capture.ts";
2
3
  import { AssistantMessageEventStream } from "../utils/event-stream.ts";
3
4
  type CancellationHandler = () => void | Promise<void>;
4
5
  /** Recovery events carry immutable snapshots and one awaitable cancellation lifecycle. */
@@ -6,10 +7,20 @@ export declare class RecoveryAssistantMessageEventStream extends AssistantMessag
6
7
  private cancellationHandler;
7
8
  private cancellationPromise;
8
9
  private sourceClosed;
10
+ private relaySource;
11
+ private relayed;
12
+ constructor();
9
13
  setCancellationHandler(handler: CancellationHandler): void;
10
14
  markSourceClosed(): boolean;
11
- push(event: AssistantMessageEvent): void;
15
+ /**
16
+ * Runs a projection step that copies `source` 1:1 to a new index. The copy is built from source blocks read after
17
+ * later deltas landed, so its capture relays the source's emission-time record instead of freezing the copy. Only
18
+ * the step's first push of the source's type is that copy; any later one records nothing.
19
+ */
20
+ relaying<T>(source: AssistantMessageEvent, project: () => T): T;
21
+ push(event: AssistantMessageEvent, hint?: CaptureHint): void;
12
22
  [Symbol.asyncIterator](): AsyncIterator<AssistantMessageEvent>;
23
+ private withCancellation;
13
24
  }
14
25
  export {};
15
26
  //# sourceMappingURL=recovery-event-stream.d.ts.map
@@ -1,10 +1,13 @@
1
+ import { carryAssistantCapture, getAssistantCapture, setAssistantReader, } from "../utils/assistant-capture.js";
1
2
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
2
3
  import { snapshotRecoveryEvent } from "./recovery-message-snapshot.js";
3
4
  /** Recovery events carry immutable snapshots and one awaitable cancellation lifecycle. */
4
5
  export class RecoveryAssistantMessageEventStream extends AssistantMessageEventStream {
5
6
  constructor() {
6
- super(...arguments);
7
+ super({ captureBlocks: true });
7
8
  this.sourceClosed = false;
9
+ this.relayed = false;
10
+ setAssistantReader(this, (project) => this.withCancellation(this.mapIterator(project)));
8
11
  }
9
12
  setCancellationHandler(handler) {
10
13
  this.cancellationHandler = handler;
@@ -15,11 +18,34 @@ export class RecoveryAssistantMessageEventStream extends AssistantMessageEventSt
15
18
  this.sourceClosed = true;
16
19
  return true;
17
20
  }
18
- push(event) {
19
- super.push(snapshotRecoveryEvent(event));
21
+ /**
22
+ * Runs a projection step that copies `source` 1:1 to a new index. The copy is built from source blocks read after
23
+ * later deltas landed, so its capture relays the source's emission-time record instead of freezing the copy. Only
24
+ * the step's first push of the source's type is that copy; any later one records nothing.
25
+ */
26
+ relaying(source, project) {
27
+ this.relaySource = source;
28
+ this.relayed = false;
29
+ try {
30
+ return project();
31
+ }
32
+ finally {
33
+ this.relaySource = undefined;
34
+ }
35
+ }
36
+ push(event, hint) {
37
+ const source = this.relaySource;
38
+ let relay = hint;
39
+ if (source?.type === event.type) {
40
+ relay = { ...hint, carried: this.relayed ? null : (getAssistantCapture(source) ?? null) };
41
+ this.relayed = true;
42
+ }
43
+ super.push(carryAssistantCapture(event, snapshotRecoveryEvent(event)), relay);
20
44
  }
21
45
  [Symbol.asyncIterator]() {
22
- const iterator = super[Symbol.asyncIterator]();
46
+ return this.withCancellation(super[Symbol.asyncIterator]());
47
+ }
48
+ withCancellation(iterator) {
23
49
  return {
24
50
  next: () => iterator.next(),
25
51
  return: async () => {
@@ -1,10 +1,13 @@
1
1
  import type { AssistantMessage, AssistantMessageEventStream, ToolCall } from "../types.ts";
2
+ import { type AssistantProgress } from "../utils/assistant-capture.ts";
2
3
  import type { StreamParserEvent, ToolCallFormat } from "./types.ts";
3
4
  /** Maintains source-index order and native/recovered ID ownership for recovery streams. */
4
5
  export declare class RecoveryNativeProjection {
5
6
  private readonly stream;
6
7
  private readonly message;
7
8
  private readonly rangesByInnerIndex;
9
+ private readonly copiedInnerIndexes;
10
+ private readonly pendingProgress;
8
11
  private readonly nativeLifecycleByInnerIndex;
9
12
  private readonly reservedIds;
10
13
  private readonly recoveredIds;
@@ -18,12 +21,19 @@ export declare class RecoveryNativeProjection {
18
21
  synchronizeRemaining(source: AssistantMessage): boolean;
19
22
  startText(innerIndex: number, outerIndex: number): boolean;
20
23
  recordProjectedBlock(innerIndex: number, outerIndex: number): boolean;
24
+ /**
25
+ * Relays a source control to the outer block that copies its source block. Controls of a block the projection
26
+ * rebuilds (prose split into text and recovered calls) describe no outer block and are dropped; controls of a
27
+ * block not projected yet wait for it.
28
+ */
29
+ relayProgress(progress: AssistantProgress): void;
21
30
  extendText(innerIndex: number): void;
22
31
  projectNativeStart(source: AssistantMessage, innerIndex: number): "projected" | "collision" | "invalid";
23
32
  projectNativeDelta(source: AssistantMessage, innerIndex: number, delta: string): boolean;
24
33
  projectNativeEnd(innerIndex: number, toolCall: ToolCall): boolean;
25
34
  assignRecoveredIds(events: readonly StreamParserEvent[]): StreamParserEvent[];
26
35
  private appendUnannounced;
36
+ /** Flushes the controls held for `innerIndex`; a copied block with a start event records its range after that start. */
27
37
  private recordRange;
28
38
  private allocateRecoveredId;
29
39
  }
@@ -1,3 +1,4 @@
1
+ import { pushAssistantProgress } from "../utils/assistant-capture.js";
1
2
  function cloneContentBlock(block) {
2
3
  if (block.type === "toolCall")
3
4
  return { ...block, arguments: { ...block.arguments } };
@@ -10,6 +11,8 @@ function cloneToolCall(toolCall) {
10
11
  export class RecoveryNativeProjection {
11
12
  constructor(stream, message, protocol = "antml") {
12
13
  this.rangesByInnerIndex = new Map();
14
+ this.copiedInnerIndexes = new Set();
15
+ this.pendingProgress = new Map();
13
16
  this.nativeLifecycleByInnerIndex = new Map();
14
17
  this.reservedIds = new Set();
15
18
  this.recoveredIds = new Set();
@@ -45,12 +48,31 @@ export class RecoveryNativeProjection {
45
48
  return true;
46
49
  }
47
50
  startText(innerIndex, outerIndex) {
48
- if (!this.recordRange(innerIndex, { start: outerIndex, end: this.message.content.length }))
51
+ if (!this.recordRange(innerIndex, { start: outerIndex, end: this.message.content.length }, false))
49
52
  return false;
50
53
  return true;
51
54
  }
52
55
  recordProjectedBlock(innerIndex, outerIndex) {
53
- return this.recordRange(innerIndex, { start: outerIndex, end: outerIndex + 1 });
56
+ return this.recordRange(innerIndex, { start: outerIndex, end: outerIndex + 1 }, true);
57
+ }
58
+ /**
59
+ * Relays a source control to the outer block that copies its source block. Controls of a block the projection
60
+ * rebuilds (prose split into text and recovered calls) describe no outer block and are dropped; controls of a
61
+ * block not projected yet wait for it.
62
+ */
63
+ relayProgress(progress) {
64
+ const range = this.rangesByInnerIndex.get(progress.contentIndex);
65
+ if (!range) {
66
+ const pending = this.pendingProgress.get(progress.contentIndex);
67
+ if (pending)
68
+ pending.push(progress);
69
+ else
70
+ this.pendingProgress.set(progress.contentIndex, [progress]);
71
+ return;
72
+ }
73
+ if (!this.copiedInnerIndexes.has(progress.contentIndex))
74
+ return;
75
+ pushAssistantProgress(this.stream, range.start === progress.contentIndex ? progress : Object.freeze({ ...progress, contentIndex: range.start }));
54
76
  }
55
77
  extendText(innerIndex) {
56
78
  const range = this.rangesByInnerIndex.get(innerIndex);
@@ -68,11 +90,11 @@ export class RecoveryNativeProjection {
68
90
  this.reservedIds.add(block.id);
69
91
  const outerIndex = this.message.content.length;
70
92
  this.message.content.push(cloneContentBlock(block));
71
- if (!this.recordRange(innerIndex, { start: outerIndex, end: outerIndex + 1 }))
72
- return "invalid";
73
93
  this.nativeLifecycleByInnerIndex.set(innerIndex, "started");
74
94
  this.stream.push({ type: "toolcall_start", contentIndex: outerIndex, partial: this.message });
75
- return "projected";
95
+ // Recorded after the start, as a thinking copy is, so controls held for the call follow its start event. The
96
+ // guards above already rule out both reasons recordRange refuses.
97
+ return this.recordRange(innerIndex, { start: outerIndex, end: outerIndex + 1 }, true) ? "projected" : "invalid";
76
98
  }
77
99
  projectNativeDelta(source, innerIndex, delta) {
78
100
  if (this.nativeLifecycleByInnerIndex.get(innerIndex) !== "started")
@@ -125,13 +147,20 @@ export class RecoveryNativeProjection {
125
147
  return false;
126
148
  const outerIndex = this.message.content.length;
127
149
  this.message.content.push(cloneContentBlock(block));
128
- return this.recordRange(innerIndex, { start: outerIndex, end: outerIndex + 1 });
150
+ return this.recordRange(innerIndex, { start: outerIndex, end: outerIndex + 1 }, true);
129
151
  }
130
- recordRange(innerIndex, range) {
152
+ /** Flushes the controls held for `innerIndex`; a copied block with a start event records its range after that start. */
153
+ recordRange(innerIndex, range, copiesSource) {
131
154
  if (this.rangesByInnerIndex.has(innerIndex) || innerIndex <= this.highestProjectedInnerIndex)
132
155
  return false;
133
156
  this.rangesByInnerIndex.set(innerIndex, range);
134
157
  this.highestProjectedInnerIndex = innerIndex;
158
+ if (copiesSource)
159
+ this.copiedInnerIndexes.add(innerIndex);
160
+ const pending = this.pendingProgress.get(innerIndex) ?? [];
161
+ this.pendingProgress.delete(innerIndex);
162
+ for (const progress of pending)
163
+ this.relayProgress(progress);
135
164
  return true;
136
165
  }
137
166
  allocateRecoveredId() {
@@ -1,3 +1,4 @@
1
+ import { removeToolCalls, rewriteHint } from "./recovery-stream-terminal.js";
1
2
  const failureDetails = {
2
3
  collision: {
3
4
  diagnosticType: "text_tool_call_recovery_collision",
@@ -20,7 +21,7 @@ export function terminateRecoveryStreamForFailure(stream, projection, options) {
20
21
  const details = failureDetails[failure];
21
22
  projection.sync(source);
22
23
  const message = projection.finalize(source, false);
23
- message.content = message.content.filter((block) => block.type !== "toolCall");
24
+ const rewrittenFrom = removeToolCalls(message);
24
25
  message.stopReason = "error";
25
26
  message.errorMessage = details.errorMessage;
26
27
  message.diagnostics = [
@@ -31,7 +32,7 @@ export function terminateRecoveryStreamForFailure(stream, projection, options) {
31
32
  details: { protocol, status: details.status },
32
33
  },
33
34
  ];
34
- stream.push({ type: "error", reason: "error", error: message });
35
+ stream.push({ type: "error", reason: "error", error: message }, rewriteHint(rewrittenFrom));
35
36
  stream.end(message);
36
37
  }
37
38
  //# sourceMappingURL=recovery-stream-failure.js.map
@@ -1,5 +1,11 @@
1
1
  import type { AssistantMessage, AssistantMessageEventStream, StopReason } from "../types.ts";
2
2
  import type { StreamMessageProjection } from "./stream-wrapper-shared.ts";
3
+ /** Removes the tool calls `removes` selects and returns the first index whose block vanished or moved, if any. */
4
+ export declare function removeToolCalls(message: AssistantMessage, removes?: (index: number) => boolean): number | undefined;
5
+ /** The capture hint that marks a terminal whose content no longer matches the announced blocks from an index on. */
6
+ export declare function rewriteHint(rewrittenFrom: number | undefined): {
7
+ rewrittenFrom: number;
8
+ } | undefined;
3
9
  /** Emits one terminal event after recovery projection state has been flushed. */
4
10
  export declare class RecoveryStreamTerminal {
5
11
  private emitted;
@@ -2,12 +2,28 @@ import { parseServerFallbackReceipt, SERVER_FALLBACK_ABORTED_DIAGNOSTIC } from "
2
2
  function errorText(error) {
3
3
  return error instanceof Error ? error.message : String(error);
4
4
  }
5
+ /** Removes the tool calls `removes` selects and returns the first index whose block vanished or moved, if any. */
6
+ export function removeToolCalls(message, removes = () => true) {
7
+ const from = message.content.findIndex((block, index) => block.type === "toolCall" && removes(index));
8
+ if (from < 0)
9
+ return undefined;
10
+ message.content = message.content.filter((block, index) => block.type !== "toolCall" || !removes(index));
11
+ return from;
12
+ }
13
+ function supersededBoundary(message) {
14
+ return message.content.findLastIndex((block) => block.type === "providerNative" && parseServerFallbackReceipt(block.raw) !== undefined);
15
+ }
16
+ /** A server fallback receipt supersedes every client tool call before it. */
5
17
  function discardSupersededTools(message) {
6
- const boundary = message.content.findLastIndex((block) => block.type === "providerNative" && parseServerFallbackReceipt(block.raw) !== undefined);
7
- if (boundary < 0)
8
- return false;
9
- message.content = message.content.filter((block, index) => index > boundary || block.type !== "toolCall");
10
- return true;
18
+ const boundary = supersededBoundary(message);
19
+ return removeToolCalls(message, (index) => index < boundary);
20
+ }
21
+ function firstIndex(left, right) {
22
+ return left === undefined ? right : right === undefined ? left : Math.min(left, right);
23
+ }
24
+ /** The capture hint that marks a terminal whose content no longer matches the announced blocks from an index on. */
25
+ export function rewriteHint(rewrittenFrom) {
26
+ return rewrittenFrom === undefined ? undefined : { rewrittenFrom };
11
27
  }
12
28
  /** Emits one terminal event after recovery projection state has been flushed. */
13
29
  export class RecoveryStreamTerminal {
@@ -20,36 +36,38 @@ export class RecoveryStreamTerminal {
20
36
  return;
21
37
  projection.finalizeDanglingToolCalls();
22
38
  const message = projection.finalize(source, sawToolCall);
23
- const superseded = discardSupersededTools(message);
39
+ const superseded = supersededBoundary(message) >= 0;
40
+ const rewrittenFrom = discardSupersededTools(message);
24
41
  const recovered = (!superseded && sawToolCall) || message.content.some((block) => block.type === "toolCall");
25
42
  const finalReason = recovered && (reason === "stop" || reason === "length") ? "toolUse" : reason;
26
43
  message.stopReason = finalReason;
27
- this.emit({ type: "done", reason: finalReason, message });
44
+ this.emit({ type: "done", reason: finalReason, message }, rewrittenFrom);
28
45
  }
29
46
  sourceError(projection, source, sawToolCall, reason) {
30
47
  if (this.emitted)
31
48
  return;
49
+ // The forwarded source holds source indices, not the projected ones the stream announced: no index is provable.
32
50
  if (source.diagnostics?.some((entry) => entry.type === SERVER_FALLBACK_ABORTED_DIAGNOSTIC)) {
33
- this.emit({ type: "error", reason, error: source });
51
+ this.emit({ type: "error", reason, error: source }, 0);
34
52
  return;
35
53
  }
36
- discardSupersededTools(projection.message);
54
+ const rewrittenFrom = discardSupersededTools(projection.message);
37
55
  projection.finalizeDanglingToolCalls();
38
56
  const message = projection.finalize(source, sawToolCall);
39
57
  if (reason === "aborted") {
40
- message.content = message.content.filter((block) => block.type !== "toolCall");
58
+ const aborted = removeToolCalls(message);
41
59
  message.stopReason = "aborted";
42
60
  message.errorMessage ??= "Request was aborted";
43
- this.emit({ type: "error", reason: "aborted", error: message });
61
+ this.emit({ type: "error", reason: "aborted", error: message }, firstIndex(rewrittenFrom, aborted));
44
62
  return;
45
63
  }
46
64
  if (projection.hasFinalizedToolCallContent()) {
47
65
  message.stopReason = "toolUse";
48
- this.emit({ type: "done", reason: "toolUse", message });
66
+ this.emit({ type: "done", reason: "toolUse", message }, rewrittenFrom);
49
67
  return;
50
68
  }
51
69
  message.stopReason = "error";
52
- this.emit({ type: "error", reason: "error", error: message });
70
+ this.emit({ type: "error", reason: "error", error: message }, rewrittenFrom);
53
71
  }
54
72
  iteratorFailure(projection, error) {
55
73
  if (this.emitted)
@@ -87,16 +105,16 @@ export class RecoveryStreamTerminal {
87
105
  }
88
106
  projection.finalizeDanglingToolCalls();
89
107
  const message = projection.finalize(projection.message, false);
90
- message.content = message.content.filter((block) => block.type !== "toolCall");
108
+ const rewrittenFrom = removeToolCalls(message);
91
109
  message.stopReason = "error";
92
110
  message.errorMessage = "Assistant message stream consumption was cancelled";
93
- this.emit({ type: "error", reason: "error", error: message });
111
+ this.emit({ type: "error", reason: "error", error: message }, rewrittenFrom);
94
112
  }
95
- emit(event) {
113
+ emit(event, rewrittenFrom) {
96
114
  if (this.emitted)
97
115
  return;
98
116
  this.emitted = true;
99
- this.stream.push(event);
117
+ this.stream.push(event, rewriteHint(rewrittenFrom));
100
118
  this.stream.end();
101
119
  }
102
120
  }
@@ -1,3 +1,4 @@
1
+ import { assistantEvents } from "../utils/assistant-capture.js";
1
2
  import { createAntmlInvokeRecoveryStreamParser } from "./protocols/antml/recovery-stream.js";
2
3
  import { RecoveryContentLifecycle } from "./recovery-content-lifecycle.js";
3
4
  import { RecoveryAssistantMessageEventStream } from "./recovery-event-stream.js";
@@ -12,13 +13,16 @@ export function wrapStreamWithInvokeRecovery(innerStream, tools, options = {}) {
12
13
  const createParser = resolvedOptions.createParser ?? createAntmlInvokeRecoveryStreamParser;
13
14
  const protocol = resolvedOptions.protocol ?? "antml";
14
15
  void (async () => {
15
- const innerIterator = innerStream[Symbol.asyncIterator]();
16
- const innerEvents = { [Symbol.asyncIterator]: () => innerIterator };
17
16
  let projection = null;
18
17
  let nativeProjection = null;
19
18
  let textProjection = null;
20
19
  let sawToolCall = false;
21
20
  let cancellationRequested = false;
21
+ const innerIterator = assistantEvents(innerStream, (progress) => {
22
+ if (!cancellationRequested)
23
+ nativeProjection?.relayProgress(progress);
24
+ })[Symbol.asyncIterator]();
25
+ const innerEvents = { [Symbol.asyncIterator]: () => innerIterator };
22
26
  const contentLifecycle = new RecoveryContentLifecycle();
23
27
  const terminal = new RecoveryStreamTerminal(outerStream);
24
28
  const finishText = () => {
@@ -167,7 +171,8 @@ export function wrapStreamWithInvokeRecovery(innerStream, tools, options = {}) {
167
171
  return;
168
172
  if (!prepareContentEvent(event.partial, event.contentIndex) || !nativeProjection)
169
173
  return;
170
- const status = nativeProjection.projectNativeStart(event.partial, event.contentIndex);
174
+ const native = nativeProjection;
175
+ const status = outerStream.relaying(event, () => native.projectNativeStart(event.partial, event.contentIndex));
171
176
  if (status !== "projected") {
172
177
  terminateForFailure(event.partial, status === "collision" ? "collision" : "invalid_native_event_order");
173
178
  return;
@@ -176,34 +181,39 @@ export function wrapStreamWithInvokeRecovery(innerStream, tools, options = {}) {
176
181
  sawToolCall = true;
177
182
  break;
178
183
  }
179
- case "toolcall_delta":
184
+ case "toolcall_delta": {
180
185
  if (!isActive(event.partial, event.contentIndex, "toolCall", "invalid_native_event_order"))
181
186
  return;
182
187
  if (!prepareContentEvent(event.partial, event.contentIndex) || !nativeProjection)
183
188
  return;
184
- if (!nativeProjection.projectNativeDelta(event.partial, event.contentIndex, event.delta)) {
189
+ const native = nativeProjection;
190
+ if (!outerStream.relaying(event, () => native.projectNativeDelta(event.partial, event.contentIndex, event.delta))) {
185
191
  terminateForFailure(event.partial, "invalid_native_event_order");
186
192
  return;
187
193
  }
188
194
  break;
189
- case "toolcall_end":
195
+ }
196
+ case "toolcall_end": {
190
197
  if (!isActive(event.partial, event.contentIndex, "toolCall", "invalid_native_event_order"))
191
198
  return;
192
199
  if (!prepareContentEvent(event.partial, event.contentIndex) || !nativeProjection)
193
200
  return;
194
- if (!nativeProjection.projectNativeEnd(event.contentIndex, event.toolCall)) {
201
+ const native = nativeProjection;
202
+ if (!outerStream.relaying(event, () => native.projectNativeEnd(event.contentIndex, event.toolCall))) {
195
203
  terminateForFailure(event.partial, "invalid_native_event_order");
196
204
  return;
197
205
  }
198
206
  contentLifecycle.end(event.contentIndex, "toolCall");
199
207
  sawToolCall = true;
200
208
  break;
209
+ }
201
210
  case "thinking_start": {
202
211
  if (!canStart(event.partial, event.contentIndex, "thinking"))
203
212
  return;
204
213
  if (!prepareContentEvent(event.partial, event.contentIndex) || !projection || !nativeProjection)
205
214
  return;
206
- const outerIndex = projection.startThinking(event.contentIndex, event.partial);
215
+ const thinking = projection;
216
+ const outerIndex = outerStream.relaying(event, () => thinking.startThinking(event.contentIndex, event.partial));
207
217
  if (!nativeProjection.recordProjectedBlock(event.contentIndex, outerIndex)) {
208
218
  terminateForFailure(event.partial, "invalid_native_event_order");
209
219
  return;
@@ -211,21 +221,25 @@ export function wrapStreamWithInvokeRecovery(innerStream, tools, options = {}) {
211
221
  contentLifecycle.start(event.contentIndex, "thinking");
212
222
  break;
213
223
  }
214
- case "thinking_delta":
224
+ case "thinking_delta": {
215
225
  if (!isActive(event.partial, event.contentIndex, "thinking"))
216
226
  return;
217
227
  if (!prepareContentEvent(event.partial, event.contentIndex) || !projection)
218
228
  return;
219
- projection.projectThinkingDelta(event.contentIndex, event.delta, event.partial);
229
+ const thinking = projection;
230
+ outerStream.relaying(event, () => thinking.projectThinkingDelta(event.contentIndex, event.delta, event.partial));
220
231
  break;
221
- case "thinking_end":
232
+ }
233
+ case "thinking_end": {
222
234
  if (!isActive(event.partial, event.contentIndex, "thinking"))
223
235
  return;
224
236
  if (!prepareContentEvent(event.partial, event.contentIndex) || !projection)
225
237
  return;
226
- projection.finishThinking(event.contentIndex, event.content, event.partial);
238
+ const thinking = projection;
239
+ outerStream.relaying(event, () => thinking.finishThinking(event.contentIndex, event.content, event.partial));
227
240
  contentLifecycle.end(event.contentIndex, "thinking");
228
241
  break;
242
+ }
229
243
  }
230
244
  }
231
245
  if (cancellationRequested || !outerStream.markSourceClosed())
@@ -1,7 +1,9 @@
1
1
  import { AssistantMessageEventStream as AssistantMessageEventStreamImpl } from "../utils/event-stream.js";
2
2
  import { StreamMessageProjection } from "./stream-wrapper-shared.js";
3
3
  export function wrapStreamWithToolCallMiddleware(innerStream, protocol, tools) {
4
- const outerStream = new AssistantMessageEventStreamImpl();
4
+ // Every outer block is rebuilt from parser output and own state, never copied from a source block read late, so
5
+ // the outer stream captures its own pushes and no source record is forwarded.
6
+ const outerStream = new AssistantMessageEventStreamImpl({ captureBlocks: true });
5
7
  const parser = protocol.createStreamParser(tools);
6
8
  void (async () => {
7
9
  let projection = null;
@@ -43,6 +43,13 @@ export interface AssistantCapture {
43
43
  readonly parsedThrough?: number;
44
44
  /** The block's own index where the event's public `contentIndex` names another block; internal readers prefer it. */
45
45
  readonly contentIndex?: number;
46
+ /**
47
+ * First block index whose earlier records no longer name the terminal's blocks (block index/identity only); readers
48
+ * MUST drop every block record at or above it for that message; absent = no rewrite. The terminal message always
49
+ * wins for CONTENT: readers replace each block's content from the terminal (or the block's end event), never
50
+ * append-only from live records.
51
+ */
52
+ readonly rewrittenFrom?: number;
46
53
  }
47
54
  /** While held, every capture-enabled stream whose first push comes after it builds capture records. */
48
55
  export declare function subscribeAssistantCapture(): () => void;
@@ -60,6 +67,7 @@ export declare function pushAssistantCompleteBlock(owner: object, contentIndex:
60
67
  export declare function assistantRecords(source: AsyncIterable<AssistantMessageEvent>): AsyncIterable<AssistantRecord>;
61
68
  /** Events only; each progress record goes to `onProgress` as it is read, so a forwarder spends no turn on it. */
62
69
  export declare function assistantEvents(source: AsyncIterable<AssistantMessageEvent>, onProgress: (progress: AssistantProgress) => void): AsyncIterable<AssistantMessageEvent>;
70
+ /** Internal readers address a block by `capture.contentIndex ?? event.contentIndex`; the public index can drift (Google/Vertex). */
63
71
  export declare function getAssistantCapture(event: object): AssistantCapture | undefined;
64
72
  export declare function isAssistantProgress(event: AssistantRecord): event is AssistantProgress;
65
73
  export interface CaptureHint {
@@ -69,6 +77,14 @@ export interface CaptureHint {
69
77
  readonly offset?: number;
70
78
  /** The open block's own index when the adapter's public `contentIndex` has drifted from it. */
71
79
  readonly contentIndex?: number;
80
+ /**
81
+ * Set by a wrapper that re-emits a source event 1:1 at its own index while reading the source later than it was
82
+ * emitted: block facts come from this source capture, usage and metadata from the relaying stream. `null` records
83
+ * nothing, because the source captured nothing.
84
+ */
85
+ readonly carried?: AssistantCapture | null;
86
+ /** A terminal's `AssistantCapture.rewrittenFrom`. */
87
+ readonly rewrittenFrom?: number;
72
88
  }
73
89
  export interface CapturePosition {
74
90
  offset: number;