@code-yeongyu/senpi-ai 2026.10.25-4 → 2026.10.26-1

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.
Files changed (35) hide show
  1. package/dist/api/anthropic-messages.js +19 -2
  2. package/dist/api/azure-openai-responses.js +1 -1
  3. package/dist/api/bedrock-converse-stream.js +13 -1
  4. package/dist/api/cursor-agent.js +2 -2
  5. package/dist/api/devin-agent/stream-state.js +8 -0
  6. package/dist/api/devin-agent.js +1 -1
  7. package/dist/api/google-generative-ai.js +22 -10
  8. package/dist/api/google-vertex.js +22 -10
  9. package/dist/api/lazy.js +8 -2
  10. package/dist/api/mistral-conversations.js +1 -1
  11. package/dist/api/openai-codex-responses.js +1 -1
  12. package/dist/api/openai-completions.js +9 -4
  13. package/dist/api/openai-responses-shared.js +37 -3
  14. package/dist/api/openai-responses.js +1 -1
  15. package/dist/api/pi-messages.js +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/providers/data/.manifest.json +1 -1
  19. package/dist/providers/data/amazon-bedrock.json +1 -1
  20. package/dist/providers/data/opengateway.json +1 -1
  21. package/dist/providers/data/openrouter.json +1 -1
  22. package/dist/providers/faux.js +5 -1
  23. package/dist/utils/assistant-capture.d.ts +85 -0
  24. package/dist/utils/assistant-capture.js +290 -0
  25. package/dist/utils/event-stream.d.ts +15 -2
  26. package/dist/utils/event-stream.js +57 -8
  27. package/dist/utils/tool-argument-coercion.d.ts +2 -0
  28. package/dist/utils/tool-argument-coercion.js +74 -0
  29. package/dist/utils/tool-argument-validator.d.ts +20 -0
  30. package/dist/utils/tool-argument-validator.js +9 -0
  31. package/dist/utils/tool-schema-compat.d.ts +5 -0
  32. package/dist/utils/tool-schema-compat.js +5 -0
  33. package/dist/utils/validation.d.ts +1 -0
  34. package/dist/utils/validation.js +14 -75
  35. package/package.json +2 -2
@@ -5,6 +5,7 @@ import { readProviderDiagnostic } from "../provider-diagnostic.js";
5
5
  import { registerSessionResourceCleanup } from "../session-resources.js";
6
6
  import { isVideoMimeType } from "../types.js";
7
7
  import { combineAbortSignals } from "../utils/abort-signals.js";
8
+ import { pushAssistantProgress } from "../utils/assistant-capture.js";
8
9
  import { claudeCodeVersionTooOldHint, getClaudeCodeVersion, isClaudeCodeVersionTooOldError, recoverClaudeCodeVersion, } from "../utils/claude-code-version.js";
9
10
  import { splitDeferredTools } from "../utils/deferred-tools.js";
10
11
  import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
@@ -920,7 +921,7 @@ async function* iterateAnthropicEvents(response, signal) {
920
921
  }
921
922
  }
922
923
  export const stream = (model, context, options) => {
923
- const stream = new AssistantMessageEventStream();
924
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
924
925
  const normalizedContext = resolveTranscript(context, supportsMidConvoSystemMessages(model));
925
926
  const currentTools = getCurrentTools(normalizedContext.messages);
926
927
  (async () => {
@@ -1248,7 +1249,12 @@ export const stream = (model, context, options) => {
1248
1249
  index: event.index,
1249
1250
  };
1250
1251
  output.content.push(block);
1251
- // Native blocks are represented in output.content but have no dedicated stream event variant.
1252
+ pushAssistantProgress(stream, {
1253
+ type: "message_block_state",
1254
+ contentIndex: output.content.length - 1,
1255
+ phase: "insert",
1256
+ block,
1257
+ });
1252
1258
  }
1253
1259
  }
1254
1260
  else if (event.type === "content_block_delta") {
@@ -1304,6 +1310,11 @@ export const stream = (model, context, options) => {
1304
1310
  if (block && block.type === "thinking") {
1305
1311
  block.thinkingSignature = block.thinkingSignature || "";
1306
1312
  block.thinkingSignature += event.delta.signature;
1313
+ pushAssistantProgress(stream, {
1314
+ type: "message_block_metadata",
1315
+ contentIndex: index,
1316
+ thinkingSignatureDelta: event.delta.signature,
1317
+ });
1307
1318
  }
1308
1319
  }
1309
1320
  }
@@ -1346,6 +1357,12 @@ export const stream = (model, context, options) => {
1346
1357
  if (partialJson !== undefined && isRecord(block.raw)) {
1347
1358
  block.raw = { ...block.raw, input: parseStreamingJson(partialJson) };
1348
1359
  }
1360
+ pushAssistantProgress(stream, {
1361
+ type: "message_block_state",
1362
+ contentIndex: index,
1363
+ phase: "end",
1364
+ block,
1365
+ });
1349
1366
  }
1350
1367
  }
1351
1368
  }
@@ -44,7 +44,7 @@ function formatAzureOpenAIError(error) {
44
44
  * Generate function for Azure OpenAI Responses API
45
45
  */
46
46
  export const stream = (model, context, options) => {
47
- const stream = new AssistantMessageEventStream();
47
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
48
48
  const normalizedContext = resolveTranscript(context, model.compat?.supportsMidConvoSystemMessages);
49
49
  // Start async processing
50
50
  (async () => {
@@ -3,6 +3,7 @@ import { NodeHttpHandler } from "@smithy/node-http-handler";
3
3
  import { HttpProxyAgent } from "http-proxy-agent";
4
4
  import { HttpsProxyAgent } from "https-proxy-agent";
5
5
  import { calculateCost } from "../models.js";
6
+ import { pushAssistantProgress } from "../utils/assistant-capture.js";
6
7
  import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
7
8
  import { normalizeProviderError } from "../utils/error-body.js";
8
9
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
@@ -24,7 +25,7 @@ const EMPTY_TEXT_PLACEHOLDER = "<empty>";
24
25
  /** Matches the placeholder the Anthropic API path uses for redacted thinking. */
25
26
  const REDACTED_THINKING_PLACEHOLDER = "[Reasoning redacted]";
26
27
  export const stream = (model, context, options = {}) => {
27
- const stream = new AssistantMessageEventStream();
28
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
28
29
  // Bedrock has no mid-conversation system messages; fold them into the leading prompt.
29
30
  const normalizedContext = collapseSystemMessages(context);
30
31
  (async () => {
@@ -500,12 +501,23 @@ function handleContentBlockDelta(event, blocks, output, stream) {
500
501
  if (delta.reasoningContent.signature && !thinkingBlock.redacted) {
501
502
  thinkingBlock.thinkingSignature =
502
503
  (thinkingBlock.thinkingSignature || "") + delta.reasoningContent.signature;
504
+ pushAssistantProgress(stream, {
505
+ type: "message_block_metadata",
506
+ contentIndex: thinkingIndex,
507
+ thinkingSignatureDelta: delta.reasoningContent.signature,
508
+ });
503
509
  }
504
510
  if (delta.reasoningContent.redactedContent?.length) {
505
511
  // Encrypted reasoning from non-Anthropic models on Bedrock (e.g. OpenAI GPT-5.6).
506
512
  // The payload is opaque, so keep it verbatim in `thinkingSignature` the way the
507
513
  // Anthropic path stores redacted thinking, and replay it on the next turn.
508
514
  if (!thinkingBlock.redacted) {
515
+ pushAssistantProgress(stream, {
516
+ type: "message_block_metadata",
517
+ contentIndex: thinkingIndex,
518
+ redacted: true,
519
+ ...(thinkingBlock.thinkingSignature ? { thinkingSignature: "" } : {}),
520
+ });
509
521
  thinkingBlock.redacted = true;
510
522
  thinkingBlock.thinkingSignature = "";
511
523
  thinkingBlock.thinking += REDACTED_THINKING_PLACEHOLDER;
@@ -475,7 +475,7 @@ export function mapH2TransportError(error, baseUrl) {
475
475
  return error;
476
476
  }
477
477
  export const stream = (model, context, options) => {
478
- const stream = new AssistantMessageEventStream();
478
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
479
479
  const requestView = toCursorRequestView(context);
480
480
  (async () => {
481
481
  const output = {
@@ -3061,7 +3061,7 @@ export function processInteractionUpdate(update, output, stream, state, usageSta
3061
3061
  target[kStreamingLastParseLen] = nextBuffer.length;
3062
3062
  }
3063
3063
  const idx = output.content.indexOf(target);
3064
- stream.push({ type: "toolcall_delta", contentIndex: idx, delta: chunk, partial: output });
3064
+ stream.push({ type: "toolcall_delta", contentIndex: idx, delta: chunk, partial: output }, { parsedThrough: target[kStreamingLastParseLen] ?? 0 });
3065
3065
  }
3066
3066
  }
3067
3067
  else if (updateCase === "toolCallCompleted") {
@@ -6,6 +6,7 @@
6
6
  * or closes them. This module owns the open-block state so the adapter emits
7
7
  * senpi's start/delta/end triples in the right order.
8
8
  */
9
+ import { pushAssistantProgress, pushAssistantSignature } from "../../utils/assistant-capture.js";
9
10
  import { parseStreamingJson } from "../../utils/json-parse.js";
10
11
  import { StopReason } from "./gen/cascade_pb.js";
11
12
  export function createDevinStreamState() {
@@ -41,6 +42,7 @@ function appendThinking(message, output, events, state) {
41
42
  if (block?.type !== "thinking")
42
43
  return;
43
44
  block.thinking += message.deltaThinking;
45
+ pushAssistantSignature(events, state.thinkingIndex, block, message.deltaSignature);
44
46
  if (message.deltaSignature)
45
47
  block.thinkingSignature = message.deltaSignature;
46
48
  events.push({
@@ -84,6 +86,12 @@ function appendToolCall(call, output, events, state) {
84
86
  const block = output.content[entry.contentIndex];
85
87
  if (block?.type !== "toolCall")
86
88
  return;
89
+ if (call.name && call.name !== block.name)
90
+ pushAssistantProgress(events, {
91
+ type: "message_block_metadata",
92
+ contentIndex: entry.contentIndex,
93
+ name: call.name,
94
+ });
87
95
  if (call.name)
88
96
  block.name = call.name;
89
97
  if (!call.argumentsJson)
@@ -20,7 +20,7 @@ import { AssignModelRequestSchema, AssignModelResponseSchema, GetChatMessageRequ
20
20
  import { applyDevinResponse, createDevinStreamState } from "./devin-agent/stream-state.js";
21
21
  import { buildDevinChatRequest, buildDevinRouterPrompt, DEVIN_ASSIGN_MODEL_PATH, DEVIN_CHAT_HEADERS, DEVIN_CHAT_MESSAGE_PATH, DEVIN_DEFAULT_BASE_URL, DEVIN_USER_JWT_PATH, decodeDevinFrames, devinCliMetadata, encodeDevinRequestFrame, postDevinUnary, readDevinTrailerError, } from "./devin-agent/wire.js";
22
22
  export const stream = (model, context, options) => {
23
- const events = new AssistantMessageEventStream();
23
+ const events = new AssistantMessageEventStream({ captureBlocks: true });
24
24
  void run(model, context, events, options);
25
25
  return events;
26
26
  };
@@ -1,5 +1,6 @@
1
1
  import { GoogleGenAI, } from "@google/genai";
2
2
  import { calculateCost, clampThinkingLevel } from "../models.js";
3
+ import { pushAssistantCompleteBlock, pushAssistantSignature } from "../utils/assistant-capture.js";
3
4
  import { formatProviderError, normalizeProviderError } from "../utils/error-body.js";
4
5
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
6
  import { providerHeadersToRecord } from "../utils/headers.js";
@@ -13,7 +14,7 @@ import { applyExtraBody, buildBaseOptions, GOOGLE_RESERVED_BODY_KEYS } from "./s
13
14
  // Counter for generating unique tool call IDs
14
15
  let toolCallCounter = 0;
15
16
  export const stream = (model, context, options) => {
16
- const stream = new AssistantMessageEventStream();
17
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
17
18
  const normalizedContext = collapseSystemMessages(context);
18
19
  (async () => {
19
20
  const output = {
@@ -50,10 +51,13 @@ export const stream = (model, context, options) => {
50
51
  const googleStream = await retryGoogleRequest(() => client.models.generateContentStream(params), options);
51
52
  stream.push({ type: "start", partial: output });
52
53
  let currentBlock = null;
54
+ let currentIndex = 0;
53
55
  let groundingMetadataEmitted = false;
54
56
  let urlContextMetadataEmitted = false;
55
57
  const blocks = output.content;
56
58
  const blockIndex = () => blocks.length - 1;
59
+ // blockIndex() drifts past the open block once a native part is appended; capture keeps the block's own index.
60
+ const pushOpen = (event) => stream.push(event, { contentIndex: currentIndex });
57
61
  for await (const chunk of googleStream) {
58
62
  await options?.onProviderStreamEvent?.(chunk, model);
59
63
  // @google/genai documents GenerateContentResponse.responseId as an output-only field
@@ -71,7 +75,7 @@ export const stream = (model, context, options) => {
71
75
  (!isThinking && currentBlock.type !== "text")) {
72
76
  if (currentBlock) {
73
77
  if (currentBlock.type === "text") {
74
- stream.push({
78
+ pushOpen({
75
79
  type: "text_end",
76
80
  contentIndex: blocks.length - 1,
77
81
  content: currentBlock.text,
@@ -79,7 +83,7 @@ export const stream = (model, context, options) => {
79
83
  });
80
84
  }
81
85
  else {
82
- stream.push({
86
+ pushOpen({
83
87
  type: "thinking_end",
84
88
  contentIndex: blockIndex(),
85
89
  content: currentBlock.thinking,
@@ -90,18 +94,21 @@ export const stream = (model, context, options) => {
90
94
  if (isThinking) {
91
95
  currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined };
92
96
  output.content.push(currentBlock);
97
+ currentIndex = blockIndex();
93
98
  stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
94
99
  }
95
100
  else {
96
101
  currentBlock = { type: "text", text: "" };
97
102
  output.content.push(currentBlock);
103
+ currentIndex = blockIndex();
98
104
  stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
99
105
  }
100
106
  }
101
107
  if (currentBlock.type === "thinking") {
102
108
  currentBlock.thinking += part.text;
109
+ pushAssistantSignature(stream, currentIndex, currentBlock, part.thoughtSignature);
103
110
  currentBlock.thinkingSignature = retainThoughtSignature(currentBlock.thinkingSignature, part.thoughtSignature);
104
- stream.push({
111
+ pushOpen({
105
112
  type: "thinking_delta",
106
113
  contentIndex: blockIndex(),
107
114
  delta: part.text,
@@ -110,8 +117,9 @@ export const stream = (model, context, options) => {
110
117
  }
111
118
  else {
112
119
  currentBlock.text += part.text;
120
+ pushAssistantSignature(stream, currentIndex, currentBlock, part.thoughtSignature);
113
121
  currentBlock.textSignature = retainThoughtSignature(currentBlock.textSignature, part.thoughtSignature);
114
- stream.push({
122
+ pushOpen({
115
123
  type: "text_delta",
116
124
  contentIndex: blockIndex(),
117
125
  delta: part.text,
@@ -123,7 +131,7 @@ export const stream = (model, context, options) => {
123
131
  handledAsKnownPart = true;
124
132
  if (currentBlock) {
125
133
  if (currentBlock.type === "text") {
126
- stream.push({
134
+ pushOpen({
127
135
  type: "text_end",
128
136
  contentIndex: blockIndex(),
129
137
  content: currentBlock.text,
@@ -131,7 +139,7 @@ export const stream = (model, context, options) => {
131
139
  });
132
140
  }
133
141
  else {
134
- stream.push({
142
+ pushOpen({
135
143
  type: "thinking_end",
136
144
  contentIndex: blockIndex(),
137
145
  content: currentBlock.thinking,
@@ -155,16 +163,18 @@ export const stream = (model, context, options) => {
155
163
  };
156
164
  output.content.push(toolCall);
157
165
  stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
166
+ // The adapter never parses its own spelling: the complete parsed seed stays authoritative.
158
167
  stream.push({
159
168
  type: "toolcall_delta",
160
169
  contentIndex: blockIndex(),
161
170
  delta: JSON.stringify(toolCall.arguments),
162
171
  partial: output,
163
- });
172
+ }, { parsedThrough: 0 });
164
173
  stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
165
174
  }
166
175
  if (!handledAsKnownPart) {
167
176
  output.content.push(toProviderNativeContent(part));
177
+ pushAssistantCompleteBlock(stream, blockIndex(), blocks[blockIndex()]);
168
178
  }
169
179
  }
170
180
  }
@@ -174,6 +184,7 @@ export const stream = (model, context, options) => {
174
184
  subtype: "groundingMetadata",
175
185
  raw: candidate.groundingMetadata,
176
186
  });
187
+ pushAssistantCompleteBlock(stream, blockIndex(), blocks[blockIndex()]);
177
188
  groundingMetadataEmitted = true;
178
189
  }
179
190
  if (candidate?.urlContextMetadata && !urlContextMetadataEmitted) {
@@ -182,6 +193,7 @@ export const stream = (model, context, options) => {
182
193
  subtype: "urlContextMetadata",
183
194
  raw: candidate.urlContextMetadata,
184
195
  });
196
+ pushAssistantCompleteBlock(stream, blockIndex(), blocks[blockIndex()]);
185
197
  urlContextMetadataEmitted = true;
186
198
  }
187
199
  if (candidate?.finishReason) {
@@ -213,7 +225,7 @@ export const stream = (model, context, options) => {
213
225
  }
214
226
  if (currentBlock) {
215
227
  if (currentBlock.type === "text") {
216
- stream.push({
228
+ pushOpen({
217
229
  type: "text_end",
218
230
  contentIndex: blockIndex(),
219
231
  content: currentBlock.text,
@@ -221,7 +233,7 @@ export const stream = (model, context, options) => {
221
233
  });
222
234
  }
223
235
  else {
224
- stream.push({
236
+ pushOpen({
225
237
  type: "thinking_end",
226
238
  contentIndex: blockIndex(),
227
239
  content: currentBlock.thinking,
@@ -1,5 +1,6 @@
1
1
  import { GoogleGenAI, ResourceScope, } from "@google/genai";
2
2
  import { calculateCost, clampThinkingLevel } from "../models.js";
3
+ import { pushAssistantCompleteBlock, pushAssistantSignature } from "../utils/assistant-capture.js";
3
4
  import { formatProviderError, normalizeProviderError } from "../utils/error-body.js";
4
5
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
6
  import { providerHeadersToRecord } from "../utils/headers.js";
@@ -17,7 +18,7 @@ const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
17
18
  // Counter for generating unique tool call IDs
18
19
  let toolCallCounter = 0;
19
20
  export const stream = (model, context, options) => {
20
- const stream = new AssistantMessageEventStream();
21
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
21
22
  const normalizedContext = collapseSystemMessages(context);
22
23
  (async () => {
23
24
  const output = {
@@ -55,10 +56,13 @@ export const stream = (model, context, options) => {
55
56
  const googleStream = await retryGoogleRequest(() => client.models.generateContentStream(params), options);
56
57
  stream.push({ type: "start", partial: output });
57
58
  let currentBlock = null;
59
+ let currentIndex = 0;
58
60
  let groundingMetadataEmitted = false;
59
61
  let urlContextMetadataEmitted = false;
60
62
  const blocks = output.content;
61
63
  const blockIndex = () => blocks.length - 1;
64
+ // blockIndex() drifts past the open block once a native part is appended; capture keeps the block's own index.
65
+ const pushOpen = (event) => stream.push(event, { contentIndex: currentIndex });
62
66
  for await (const chunk of googleStream) {
63
67
  await options?.onProviderStreamEvent?.(chunk, model);
64
68
  // Vertex uses the same @google/genai GenerateContentResponse type as Gemini.
@@ -76,7 +80,7 @@ export const stream = (model, context, options) => {
76
80
  (!isThinking && currentBlock.type !== "text")) {
77
81
  if (currentBlock) {
78
82
  if (currentBlock.type === "text") {
79
- stream.push({
83
+ pushOpen({
80
84
  type: "text_end",
81
85
  contentIndex: blocks.length - 1,
82
86
  content: currentBlock.text,
@@ -84,7 +88,7 @@ export const stream = (model, context, options) => {
84
88
  });
85
89
  }
86
90
  else {
87
- stream.push({
91
+ pushOpen({
88
92
  type: "thinking_end",
89
93
  contentIndex: blockIndex(),
90
94
  content: currentBlock.thinking,
@@ -95,18 +99,21 @@ export const stream = (model, context, options) => {
95
99
  if (isThinking) {
96
100
  currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined };
97
101
  output.content.push(currentBlock);
102
+ currentIndex = blockIndex();
98
103
  stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
99
104
  }
100
105
  else {
101
106
  currentBlock = { type: "text", text: "" };
102
107
  output.content.push(currentBlock);
108
+ currentIndex = blockIndex();
103
109
  stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
104
110
  }
105
111
  }
106
112
  if (currentBlock.type === "thinking") {
107
113
  currentBlock.thinking += part.text;
114
+ pushAssistantSignature(stream, currentIndex, currentBlock, part.thoughtSignature);
108
115
  currentBlock.thinkingSignature = retainThoughtSignature(currentBlock.thinkingSignature, part.thoughtSignature);
109
- stream.push({
116
+ pushOpen({
110
117
  type: "thinking_delta",
111
118
  contentIndex: blockIndex(),
112
119
  delta: part.text,
@@ -115,8 +122,9 @@ export const stream = (model, context, options) => {
115
122
  }
116
123
  else {
117
124
  currentBlock.text += part.text;
125
+ pushAssistantSignature(stream, currentIndex, currentBlock, part.thoughtSignature);
118
126
  currentBlock.textSignature = retainThoughtSignature(currentBlock.textSignature, part.thoughtSignature);
119
- stream.push({
127
+ pushOpen({
120
128
  type: "text_delta",
121
129
  contentIndex: blockIndex(),
122
130
  delta: part.text,
@@ -128,7 +136,7 @@ export const stream = (model, context, options) => {
128
136
  handledAsKnownPart = true;
129
137
  if (currentBlock) {
130
138
  if (currentBlock.type === "text") {
131
- stream.push({
139
+ pushOpen({
132
140
  type: "text_end",
133
141
  contentIndex: blockIndex(),
134
142
  content: currentBlock.text,
@@ -136,7 +144,7 @@ export const stream = (model, context, options) => {
136
144
  });
137
145
  }
138
146
  else {
139
- stream.push({
147
+ pushOpen({
140
148
  type: "thinking_end",
141
149
  contentIndex: blockIndex(),
142
150
  content: currentBlock.thinking,
@@ -159,16 +167,18 @@ export const stream = (model, context, options) => {
159
167
  };
160
168
  output.content.push(toolCall);
161
169
  stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
170
+ // The adapter never parses its own spelling: the complete parsed seed stays authoritative.
162
171
  stream.push({
163
172
  type: "toolcall_delta",
164
173
  contentIndex: blockIndex(),
165
174
  delta: JSON.stringify(toolCall.arguments),
166
175
  partial: output,
167
- });
176
+ }, { parsedThrough: 0 });
168
177
  stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
169
178
  }
170
179
  if (!handledAsKnownPart) {
171
180
  output.content.push(toProviderNativeContent(part));
181
+ pushAssistantCompleteBlock(stream, blockIndex(), blocks[blockIndex()]);
172
182
  }
173
183
  }
174
184
  }
@@ -178,6 +188,7 @@ export const stream = (model, context, options) => {
178
188
  subtype: "groundingMetadata",
179
189
  raw: candidate.groundingMetadata,
180
190
  });
191
+ pushAssistantCompleteBlock(stream, blockIndex(), blocks[blockIndex()]);
181
192
  groundingMetadataEmitted = true;
182
193
  }
183
194
  if (candidate?.urlContextMetadata && !urlContextMetadataEmitted) {
@@ -186,6 +197,7 @@ export const stream = (model, context, options) => {
186
197
  subtype: "urlContextMetadata",
187
198
  raw: candidate.urlContextMetadata,
188
199
  });
200
+ pushAssistantCompleteBlock(stream, blockIndex(), blocks[blockIndex()]);
189
201
  urlContextMetadataEmitted = true;
190
202
  }
191
203
  if (candidate?.finishReason) {
@@ -217,7 +229,7 @@ export const stream = (model, context, options) => {
217
229
  }
218
230
  if (currentBlock) {
219
231
  if (currentBlock.type === "text") {
220
- stream.push({
232
+ pushOpen({
221
233
  type: "text_end",
222
234
  contentIndex: blockIndex(),
223
235
  content: currentBlock.text,
@@ -225,7 +237,7 @@ export const stream = (model, context, options) => {
225
237
  });
226
238
  }
227
239
  else {
228
- stream.push({
240
+ pushOpen({
229
241
  type: "thinking_end",
230
242
  contentIndex: blockIndex(),
231
243
  content: currentBlock.thinking,
package/dist/api/lazy.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { assistantEvents, pushAssistantProgress, setAssistantReader } from "../utils/assistant-capture.js";
1
2
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
2
3
  import { isOAuthRefreshUnavailableError } from "../utils/oauth-refresh-error.js";
3
4
  import { describeReplacedInstall } from "../utils/provider-failure-description.js";
@@ -39,9 +40,13 @@ function hasResult(source) {
39
40
  class LazyAssistantMessageEventStream extends AssistantMessageEventStream {
40
41
  setCancellationHandler(handler) {
41
42
  this.cancellationHandler = handler;
43
+ // The forwarder carries records, so this wrapper is carrier-aware and keeps its cancellation for them.
44
+ setAssistantReader(this, (project) => this.withCancellation(this.mapIterator(project)));
42
45
  }
43
46
  [Symbol.asyncIterator]() {
44
- const iterator = super[Symbol.asyncIterator]();
47
+ return this.withCancellation(super[Symbol.asyncIterator]());
48
+ }
49
+ withCancellation(iterator) {
45
50
  return {
46
51
  next: () => iterator.next(),
47
52
  return: async () => {
@@ -68,7 +73,8 @@ async function forwardStream(target, source, iterator) {
68
73
  export function lazyStream(model, setup) {
69
74
  const outer = new LazyAssistantMessageEventStream();
70
75
  const inner = setup();
71
- const iterator = inner.then((source) => source[Symbol.asyncIterator]());
76
+ // Progress crosses as it is read, so the forwarder loop runs exactly the turns it runs for events alone.
77
+ const iterator = inner.then((source) => assistantEvents(source, (progress) => pushAssistantProgress(outer, progress))[Symbol.asyncIterator]());
72
78
  let cancellation;
73
79
  outer.setCancellationHandler(() => {
74
80
  cancellation ??= iterator.then(async (sourceIterator) => {
@@ -16,7 +16,7 @@ const MAX_MISTRAL_ERROR_BODY_CHARS = 4000;
16
16
  * Stream responses from the native Mistral Chat Completions endpoint.
17
17
  */
18
18
  export const stream = (model, context, options) => {
19
- const stream = new AssistantMessageEventStream();
19
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
20
20
  const normalizedContext = resolveTranscript(context, model.compat?.supportsMidConvoSystemMessages);
21
21
  (async () => {
22
22
  const output = createOutput(model);
@@ -142,7 +142,7 @@ function copyBytesToArrayBuffer(bytes) {
142
142
  // Main Stream Function
143
143
  // ============================================================================
144
144
  export const stream = (model, context, options) => {
145
- const stream = new AssistantMessageEventStream();
145
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
146
146
  const normalizedContext = resolveTranscript(context, model.compat?.supportsMidConvoSystemMessages);
147
147
  (async () => {
148
148
  const output = {
@@ -318,7 +318,7 @@ function resolveCacheRetention(cacheRetention, env) {
318
318
  return "short";
319
319
  }
320
320
  export const stream = (model, context, options) => {
321
- const stream = new AssistantMessageEventStream();
321
+ const stream = new AssistantMessageEventStream({ captureBlocks: true });
322
322
  const normalizedContext = resolveTranscript(context, getCompat(model).supportsMidConvoSystemMessages);
323
323
  (async () => {
324
324
  const output = {
@@ -589,19 +589,24 @@ export const stream = (model, context, options) => {
589
589
  for (const block of blocks) {
590
590
  const contentIndex = getContentIndex(block);
591
591
  if (block.type === "text") {
592
- stream.push({ type: "text_start", contentIndex, partial: output });
592
+ stream.push({ type: "text_start", contentIndex, partial: output }, { offset: 0 });
593
593
  for (const delta of deferredTextDeltas) {
594
594
  stream.push({ type: "text_delta", contentIndex, delta, partial: output });
595
595
  }
596
596
  }
597
597
  else if (block.type === "thinking") {
598
- stream.push({ type: "thinking_start", contentIndex, partial: output });
598
+ stream.push({ type: "thinking_start", contentIndex, partial: output }, { offset: 0 });
599
599
  for (const delta of deferredThinkingDeltas) {
600
600
  stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
601
601
  }
602
602
  }
603
603
  else {
604
- stream.push({ type: "toolcall_start", contentIndex, partial: output });
604
+ const rawArgumentPrefix = (deferredToolCallDeltas.get(block) ?? []).join("");
605
+ stream.push({ type: "toolcall_start", contentIndex, partial: output }, {
606
+ rawArgumentPrefix,
607
+ parsedThrough: rawArgumentPrefix.length,
608
+ offset: 0,
609
+ });
605
610
  for (const delta of deferredToolCallDeltas.get(block) ?? []) {
606
611
  stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
607
612
  }
@@ -1,5 +1,6 @@
1
1
  import { CONTEXT_PROVENANCE_FIELD, contextProvenanceFingerprint, getContextProvenance, } from "../context-provenance.js";
2
2
  import { calculateCost, supportsConfigurationUpdate } from "../models.js";
3
+ import { enableAssistantCapture, pushAssistantProgress } from "../utils/assistant-capture.js";
3
4
  import { splitDeferredTools } from "../utils/deferred-tools.js";
4
5
  import { shortHash } from "../utils/hash.js";
5
6
  import { parseStreamingJson } from "../utils/json-parse.js";
@@ -581,6 +582,7 @@ function reconcileNativeImageGenerationCall(item) {
581
582
  };
582
583
  }
583
584
  export async function processResponsesStream(openaiStream, output, stream, model, options) {
585
+ enableAssistantCapture(stream);
584
586
  let sawTerminalResponseEvent = false;
585
587
  let nativeImageBase64Chars = 0;
586
588
  const outputSlots = new Map();
@@ -644,7 +646,7 @@ export async function processResponsesStream(openaiStream, output, stream, model
644
646
  contentIndex: output.content.length - 1,
645
647
  };
646
648
  outputSlots.set(outputIndex, slot);
647
- stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output });
649
+ stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output }, { rawArgumentPrefix: block.partialJson });
648
650
  return slot;
649
651
  }
650
652
  if (isResponseCustomToolCallItem(item)) {
@@ -686,6 +688,12 @@ export async function processResponsesStream(openaiStream, output, stream, model
686
688
  reconcileNativeImageSlot(outputIndex, slot, imageItem);
687
689
  output.content.push(block);
688
690
  outputSlots.set(outputIndex, slot);
691
+ pushAssistantProgress(stream, {
692
+ type: "message_block_state",
693
+ contentIndex: slot.contentIndex,
694
+ phase: "insert",
695
+ block,
696
+ });
689
697
  return slot;
690
698
  };
691
699
  const getOrCreateSlot = (outputIndex, item) => {
@@ -732,10 +740,16 @@ export async function processResponsesStream(openaiStream, output, stream, model
732
740
  if (!imageItem)
733
741
  continue;
734
742
  const existingSlot = getSlot(outputIndex, "providerNative");
743
+ const slot = existingSlot ?? createSlot(outputIndex, outputItem);
735
744
  if (existingSlot)
736
745
  reconcileNativeImageSlot(outputIndex, existingSlot, imageItem);
737
- else
738
- createSlot(outputIndex, outputItem);
746
+ if (slot)
747
+ pushAssistantProgress(stream, {
748
+ type: "message_block_state",
749
+ contentIndex: slot.contentIndex,
750
+ phase: "end",
751
+ block: slot.block,
752
+ });
739
753
  outputSlots.delete(outputIndex);
740
754
  }
741
755
  };
@@ -897,6 +911,14 @@ export async function processResponsesStream(openaiStream, output, stream, model
897
911
  if (delta.length > 0)
898
912
  pushToolCallDelta(slot, delta);
899
913
  }
914
+ else {
915
+ pushAssistantProgress(stream, {
916
+ type: "message_tool_arguments_reset",
917
+ contentIndex: slot.contentIndex,
918
+ rawArgumentPrefix: event.arguments,
919
+ parsedThrough: event.arguments.length,
920
+ });
921
+ }
900
922
  }
901
923
  else if (event.type === "response.custom_tool_call_input.delta") {
902
924
  const slot = getSlot(event.output_index, "toolCall");
@@ -917,6 +939,12 @@ export async function processResponsesStream(openaiStream, output, stream, model
917
939
  const imageItem = readNativeImageGenerationCall(item);
918
940
  if (imageItem && slot?.type === "providerNative") {
919
941
  reconcileNativeImageSlot(event.output_index, slot, imageItem);
942
+ pushAssistantProgress(stream, {
943
+ type: "message_block_state",
944
+ contentIndex: slot.contentIndex,
945
+ phase: "end",
946
+ block: slot.block,
947
+ });
920
948
  finalizedNativeImageOutputIndexes.add(event.output_index);
921
949
  outputSlots.delete(event.output_index);
922
950
  }
@@ -990,6 +1018,12 @@ export async function processResponsesStream(openaiStream, output, stream, model
990
1018
  else if (slot?.type === "providerNative") {
991
1019
  slot.block.subtype = item.type;
992
1020
  slot.block.raw = item;
1021
+ pushAssistantProgress(stream, {
1022
+ type: "message_block_state",
1023
+ contentIndex: slot.contentIndex,
1024
+ phase: "end",
1025
+ block: slot.block,
1026
+ });
993
1027
  outputSlots.delete(event.output_index);
994
1028
  }
995
1029
  }