@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/tool-call-middleware/protocols/kimi-xtml/thinking-recovery-stream.js +37 -2
- package/dist/tool-call-middleware/recovery-event-stream.d.ts +12 -1
- package/dist/tool-call-middleware/recovery-event-stream.js +30 -4
- package/dist/tool-call-middleware/recovery-native-projection.d.ts +10 -0
- package/dist/tool-call-middleware/recovery-native-projection.js +36 -7
- package/dist/tool-call-middleware/recovery-stream-failure.js +3 -2
- package/dist/tool-call-middleware/recovery-stream-terminal.d.ts +6 -0
- package/dist/tool-call-middleware/recovery-stream-terminal.js +35 -17
- package/dist/tool-call-middleware/recovery-stream-wrapper.js +26 -12
- package/dist/tool-call-middleware/stream-wrapper.js +3 -1
- package/dist/utils/assistant-capture.d.ts +16 -0
- package/dist/utils/assistant-capture.js +51 -14
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
229
|
+
const thinking = projection;
|
|
230
|
+
outerStream.relaying(event, () => thinking.projectThinkingDelta(event.contentIndex, event.delta, event.partial));
|
|
220
231
|
break;
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|