@code-yeongyu/senpi 2026.8.21 → 2026.8.22

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 (44) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/core/auth-storage.d.ts.map +1 -1
  3. package/dist/core/auth-storage.js +5 -4
  4. package/dist/core/auth-storage.js.map +1 -1
  5. package/dist/core/extensions/builtin/permission-system/index.d.ts.map +1 -1
  6. package/dist/core/extensions/builtin/permission-system/index.js +7 -11
  7. package/dist/core/extensions/builtin/permission-system/index.js.map +1 -1
  8. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  9. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  10. package/dist/modes/interactive/interactive-mode.js +79 -31
  11. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  12. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +40 -0
  13. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  14. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +8 -3
  15. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +15 -1
  16. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  17. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts +21 -0
  18. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.d.ts.map +1 -0
  19. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js +36 -0
  20. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/stream-retry.js.map +1 -0
  21. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts +10 -0
  22. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.d.ts.map +1 -1
  23. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/types.js.map +1 -1
  24. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +6 -0
  25. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  26. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +166 -26
  27. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  28. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  29. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  30. package/node_modules/@earendil-works/pi-ai/dist/providers/data/google.json +1 -1
  31. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  32. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  33. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  34. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  35. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  36. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  37. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  38. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  39. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  40. package/package.json +7 -7
  41. package/dist/modes/interactive/split-trailing-assistant-text.d.ts +0 -7
  42. package/dist/modes/interactive/split-trailing-assistant-text.d.ts.map +0 -1
  43. package/dist/modes/interactive/split-trailing-assistant-text.js +0 -19
  44. package/dist/modes/interactive/split-trailing-assistant-text.js.map +0 -1
@@ -31,11 +31,18 @@ import { buildMcpStateResult, buildNeutralHookResult, buildPiBashError, buildPiB
31
31
  import { AgentClientMessageSchema, AgentConversationTurnStructureSchema, AgentRunRequestSchema, AgentServerMessageSchema, AgentStoreConflictErrorSchema, AgentStoreConflictResultSchema, AssistantMessageSchema, BackgroundShellSpawnResultSchema, CanvasDiagnosticsErrorSchema, CanvasDiagnosticsResultSchema, ClientHeartbeatSchema, ComputerUseErrorSchema, ComputerUseResultSchema, ConversationActionSchema, ConversationSearchErrorSchema, ConversationSearchResultSchema, ConversationStateStructureSchema, ConversationStepSchema, ConversationTurnStructureSchema, DeleteErrorSchema, DeleteRejectedSchema, DeleteResultSchema, DeleteSuccessSchema, DiagnosticsErrorSchema, DiagnosticsRejectedSchema, DiagnosticsResultSchema, DiagnosticsSuccessSchema, ExecClientControlMessageSchema, ExecClientHeartbeatSchema, ExecClientMessageSchema, ExecClientStreamCloseSchema, ExecClientThrowSchema, FetchErrorSchema, FetchResultSchema, ForceBackgroundShellResultSchema, ForceBackgroundShellStatus, ForceBackgroundSubagentResultSchema, ForceBackgroundSubagentStatus, GetBlobResultSchema, GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, GrepContentMatchSchema, GrepContentResultSchema, GrepCountResultSchema, GrepErrorSchema, GrepFileCountSchema, GrepFileMatchSchema, GrepFilesResultSchema, GrepResultSchema, GrepSuccessSchema, GrepUnionResultSchema, KvClientMessageSchema, ListMcpResourcesExecResultSchema, ListMcpResourcesSuccessSchema, LsDirectoryTreeNode_FileSchema, LsDirectoryTreeNodeSchema, LsErrorSchema, LsRejectedSchema, LsResultSchema, LsSuccessSchema, McpAllowlistPrecheckResultSchema, McpApprovedSchema, McpArgsSchema, McpErrorSchema, McpImageContentSchema, McpRejectedSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolCallSchema, McpToolDefinitionSchema, McpToolErrorSchema, McpToolNotFoundSchema, McpToolResultContentItemSchema, McpToolResultSchema, ModelDetailsSchema, ReadErrorSchema, ReadMcpResourceExecResultSchema, ReadMcpResourceNotFoundSchema, ReadRejectedSchema, ReadResultSchema, ReadSuccessSchema, RecordScreenFailureSchema, RecordScreenResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, ResumeActionSchema, SelectedContextSchema, SelectedImageSchema, SetBlobResultSchema, ShellAllowlistPrecheckResultSchema, ShellFailureSchema, ShellRejectedSchema, ShellResultSchema, ShellStreamExitSchema, ShellStreamSchema, ShellStreamStartSchema, ShellStreamStderrSchema, ShellStreamStdoutSchema, ShellSuccessSchema, SmartModeClassifierErrorSchema, SmartModeClassifierResultSchema, SubagentAwaitNotFoundSchema, SubagentAwaitResultSchema, SubagentErrorSchema, SubagentResultSchema, ToolCallSchema, UserMessageActionSchema, UserMessageSchema, WebFetchAllowlistPrecheckResultSchema, WriteErrorSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, WriteSuccessSchema, } from "./cursor-agent/gen/agent_pb.js";
32
32
  import { composeShellCommand, omitUndefinedArgs, piLimit, piLsPath, piReadArgs, piTimeout, } from "./cursor-agent/pi-args.js";
33
33
  import { buildRequestedModel } from "./cursor-agent/reasoning-params.js";
34
+ import { CursorRetryableStreamError, cursorStreamRetryDelayMs, shouldRetryCursorStream, waitForCursorStreamRetry, } from "./cursor-agent/stream-retry.js";
34
35
  import { CURSOR_CONVERSATION_POISONED_MESSAGE, createConversationRotationStore, isZeroTokenResourceExhausted, resolveConversationRotationPersistPath, } from "./cursor-conversation-rotation.js";
35
36
  import { keepUsableCursorTaskArgs } from "./cursor-task-args.js";
36
37
  export const CURSOR_API_URL = "https://api2.cursor.sh";
37
38
  export const CURSOR_CLIENT_VERSION = "cli-2026.07.23-e383d2b";
38
39
  const EXEC_HEARTBEAT_INTERVAL_MS = 3000;
40
+ /** Maximum inbound silence before a Cursor turn without turnEnded is failed. */
41
+ export const CURSOR_STREAM_HEALTH_FAIL_THRESHOLD_MS = 30_000;
42
+ /** @deprecated Heartbeats and checkpoints count as inbound liveness without a separate deadline. */
43
+ export const CURSOR_STREAM_HEALTH_HEARTBEAT_ONLY_THRESHOLD_MS = CURSOR_STREAM_HEALTH_FAIL_THRESHOLD_MS * 3;
44
+ /** Maximum time allowed to drain exec handlers after turnEnded. */
45
+ export const CURSOR_TURN_END_DRAIN_TIMEOUT_MS = 5000;
39
46
  /**
40
47
  * HTTP/1 connection-specific headers that HTTP/2 forbids. Node's
41
48
  * `http2.request()` throws on these rather than dropping them.
@@ -221,8 +228,10 @@ export const stream = (model, context, options) => {
221
228
  let h2Client = null;
222
229
  let h2Request = null;
223
230
  let heartbeatTimer = null;
231
+ let streamHealthTimer = null;
224
232
  let h2Settled = false;
225
233
  let sawTurnEnded = false;
234
+ let turnEndDrainTimedOut = false;
226
235
  let endStreamError = null;
227
236
  // Reachable from the catch: a stream that dies mid-turn must still close
228
237
  // and pair the blocks it left open.
@@ -233,6 +242,10 @@ export const stream = (model, context, options) => {
233
242
  if (h2Settled)
234
243
  return;
235
244
  h2Settled = true;
245
+ if (streamHealthTimer) {
246
+ clearTimeout(streamHealthTimer);
247
+ streamHealthTimer = null;
248
+ }
236
249
  if (error !== undefined) {
237
250
  rejectH2(error);
238
251
  return;
@@ -252,19 +265,25 @@ export const stream = (model, context, options) => {
252
265
  let baseConversationId;
253
266
  let conversationId;
254
267
  let usageState;
255
- let retryPoisonedConversation = false;
268
+ let retryAttempt = false;
256
269
  let attempt = 0;
270
+ let streamRetries = 0;
271
+ let forceResumeAction = false;
272
+ let pinnedRequestedModel;
273
+ let pinnedModelDetails;
257
274
  do {
258
- retryPoisonedConversation = false;
275
+ retryAttempt = false;
259
276
  attempt += 1;
260
277
  h2Settled = false;
261
278
  sawTurnEnded = false;
279
+ turnEndDrainTimedOut = false;
262
280
  endStreamError = null;
263
281
  openBlockState = undefined;
264
282
  const h2Completion = new Promise((resolve, reject) => {
265
283
  resolveH2 = resolve;
266
284
  rejectH2 = reject;
267
285
  });
286
+ let attemptSawCheckpoint = false;
268
287
  try {
269
288
  const apiKey = options?.apiKey;
270
289
  if (!apiKey) {
@@ -275,11 +294,16 @@ export const stream = (model, context, options) => {
275
294
  const blobStore = conversationBlobStores.get(conversationId) ?? new Map();
276
295
  conversationBlobStores.set(conversationId, blobStore);
277
296
  const cachedState = conversationStateCache.get(conversationId);
278
- const { requestBytes, conversationState } = await buildGrpcRequest(model, context, options, {
297
+ const { requestBytes, conversationState, requestedModel, modelDetails } = await buildGrpcRequest(model, context, options, {
279
298
  conversationId,
280
299
  blobStore,
281
300
  conversationState: cachedState,
301
+ forceResumeAction,
302
+ pinnedRequestedModel,
303
+ pinnedModelDetails,
282
304
  });
305
+ pinnedRequestedModel ??= requestedModel;
306
+ pinnedModelDetails ??= modelDetails;
283
307
  conversationStateCache.set(conversationId, conversationState);
284
308
  const requestContextTools = buildMcpToolDefinitions(context.tools);
285
309
  const baseUrl = model.baseUrl || CURSOR_API_URL;
@@ -300,9 +324,28 @@ export const stream = (model, context, options) => {
300
324
  "x-cursor-client-type": "cli",
301
325
  "x-request-id": randomUUID(),
302
326
  };
303
- h2Client = http2.connect(baseUrl);
304
- h2Client.on("error", (error) => settleH2(mapH2TransportError(error, baseUrl)));
305
- h2Request = h2Client.request(requestHeaders);
327
+ const attemptH2Client = http2.connect(baseUrl);
328
+ h2Client = attemptH2Client;
329
+ attemptH2Client.on("error", (error) => {
330
+ if (h2Client !== attemptH2Client)
331
+ return;
332
+ const mapped = mapH2TransportError(error, baseUrl);
333
+ settleH2(sawTurnEnded
334
+ ? mapped
335
+ : new CursorRetryableStreamError(mapped instanceof Error ? mapped.message : String(mapped), "transport", { cause: mapped }));
336
+ });
337
+ attemptH2Client.on("goaway", () => {
338
+ if (h2Client === attemptH2Client && !h2Settled && !sawTurnEnded) {
339
+ settleH2(new CursorRetryableStreamError("Cursor HTTP/2 session received GOAWAY", "transport"));
340
+ h2Request?.close();
341
+ }
342
+ });
343
+ attemptH2Client.on("close", () => {
344
+ if (h2Client === attemptH2Client && !h2Settled && !sawTurnEnded && !endStreamError) {
345
+ settleH2(new CursorRetryableStreamError("Cursor HTTP/2 session closed", "transport"));
346
+ }
347
+ });
348
+ h2Request = attemptH2Client.request(requestHeaders);
306
349
  if (attempt === 1) {
307
350
  stream.push({ type: "start", partial: output });
308
351
  }
@@ -337,8 +380,55 @@ export const stream = (model, context, options) => {
337
380
  };
338
381
  openBlockState = state;
339
382
  const onConversationCheckpoint = (checkpoint) => {
383
+ attemptSawCheckpoint = true;
340
384
  conversationStateCache.set(conversationId, checkpoint);
341
385
  };
386
+ const healthFailThresholdMs = options?.streamHealthFailThresholdMs ?? CURSOR_STREAM_HEALTH_FAIL_THRESHOLD_MS;
387
+ let lastInboundFrameAt = Date.now();
388
+ let turnEndCompletionStarted = false;
389
+ const armStreamHealthTimer = () => {
390
+ if (streamHealthTimer)
391
+ clearTimeout(streamHealthTimer);
392
+ if (sawTurnEnded || h2Settled)
393
+ return;
394
+ const now = Date.now();
395
+ const deadline = lastInboundFrameAt + healthFailThresholdMs;
396
+ streamHealthTimer = setTimeout(() => {
397
+ streamHealthTimer = null;
398
+ if (sawTurnEnded || h2Settled)
399
+ return;
400
+ const stalledFor = Date.now() - lastInboundFrameAt;
401
+ if (stalledFor < healthFailThresholdMs) {
402
+ armStreamHealthTimer();
403
+ return;
404
+ }
405
+ settleH2(new CursorRetryableStreamError("Cursor stream ended before turnEnded: inbound stream stalled", "stall"));
406
+ h2Request?.close();
407
+ }, Math.max(0, deadline - now));
408
+ };
409
+ const completeAfterTurnEnded = async () => {
410
+ const drainTimeoutMs = options?.turnEndDrainTimeoutMs ?? CURSOR_TURN_END_DRAIN_TIMEOUT_MS;
411
+ let timeout;
412
+ const drained = await Promise.race([
413
+ drainInFlightDispatches().then(() => true),
414
+ new Promise((resolve) => {
415
+ timeout = setTimeout(() => resolve(false), drainTimeoutMs);
416
+ }),
417
+ ]);
418
+ if (timeout)
419
+ clearTimeout(timeout);
420
+ if (h2Settled)
421
+ return;
422
+ if (!drained) {
423
+ turnEndDrainTimedOut = true;
424
+ settleH2(new Error(`Cursor exec dispatches did not settle within ${drainTimeoutMs}ms after turnEnded`));
425
+ h2Request?.close();
426
+ return;
427
+ }
428
+ h2Request?.close();
429
+ settleH2();
430
+ };
431
+ armStreamHealthTimer();
342
432
  h2Request.on("data", (chunk) => {
343
433
  // Steady state drains fully per chunk; alias the fresh h2 chunk
344
434
  // instead of copying it through Buffer.concat.
@@ -360,8 +450,12 @@ export const stream = (model, context, options) => {
360
450
  }
361
451
  try {
362
452
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
363
- const isTurnEnded = serverMessage.message.case === "interactionUpdate" &&
364
- serverMessage.message.value.message?.case === "turnEnded";
453
+ const interactionUpdateCase = serverMessage.message.case === "interactionUpdate"
454
+ ? serverMessage.message.value.message?.case
455
+ : undefined;
456
+ const isTurnEnded = interactionUpdateCase === "turnEnded";
457
+ lastInboundFrameAt = Date.now();
458
+ armStreamHealthTimer();
365
459
  // Dispatch is fire-and-forget so the socket keeps draining
366
460
  // while a handler runs, but the promise is tracked: `done`
367
461
  // must not be pushed while an exec handler is still resolving,
@@ -372,10 +466,13 @@ export const stream = (model, context, options) => {
372
466
  });
373
467
  inFlightDispatches.add(dispatch);
374
468
  void dispatch.finally(() => inFlightDispatches.delete(dispatch));
375
- // Application completion is not protocol success; wait for a
376
- // clean HTTP/2 end.
377
- if (isTurnEnded) {
469
+ // turnEnded is the definitive application completion signal. Drain
470
+ // every dispatch it follows, then close our side of the stream so a
471
+ // server that keeps HTTP/2 open cannot hold the turn hostage.
472
+ if (isTurnEnded && !turnEndCompletionStarted) {
378
473
  sawTurnEnded = true;
474
+ turnEndCompletionStarted = true;
475
+ void completeAfterTurnEnded().catch((error) => settleH2(error));
379
476
  }
380
477
  }
381
478
  catch (e) {
@@ -401,10 +498,17 @@ export const stream = (model, context, options) => {
401
498
  }
402
499
  });
403
500
  h2Request.on("end", () => {
501
+ if (!sawTurnEnded && !endStreamError) {
502
+ settleH2(new CursorRetryableStreamError("Cursor stream ended before turnEnded", "clean-end"));
503
+ return;
504
+ }
404
505
  settleH2();
405
506
  });
406
507
  h2Request.on("error", (error) => {
407
- settleH2(mapH2TransportError(error, baseUrl));
508
+ const mapped = mapH2TransportError(error, baseUrl);
509
+ settleH2(sawTurnEnded
510
+ ? mapped
511
+ : new CursorRetryableStreamError(mapped instanceof Error ? mapped.message : String(mapped), "transport", { cause: mapped }));
408
512
  });
409
513
  if (options?.signal) {
410
514
  options.signal.addEventListener("abort", () => {
@@ -435,8 +539,43 @@ export const stream = (model, context, options) => {
435
539
  // Same reason as the success path: a handler still running would land
436
540
  // its real result after the turn finalized and be discarded — even
437
541
  // though the tool may already have run side effects. On abort the
438
- // drain returns immediately.
439
- await drainInFlightDispatches();
542
+ // drain returns immediately. A post-turn drain timeout is already the
543
+ // bound: do not wait forever a second time in the error path.
544
+ if (!turnEndDrainTimedOut)
545
+ await drainInFlightDispatches();
546
+ const shouldRetryStream = shouldRetryCursorStream({
547
+ error,
548
+ retries: streamRetries,
549
+ maxRetries: options?.streamStallMaxRetries ?? 10,
550
+ sawTurnEnded,
551
+ aborted: options?.signal?.aborted === true,
552
+ });
553
+ if (shouldRetryStream) {
554
+ if (openBlockState) {
555
+ // Resume responses continue from the server checkpoint. Close any
556
+ // locally open cards before resetting per-attempt bookkeeping; no
557
+ // speculative replay deduplication is attempted.
558
+ endCurrentTextBlock(output, stream, openBlockState);
559
+ endCurrentThinkingBlock(output, stream, openBlockState);
560
+ flushOpenToolCalls(output, stream, openBlockState);
561
+ }
562
+ forceResumeAction ||= attemptSawCheckpoint;
563
+ const retryDelayMs = cursorStreamRetryDelayMs({
564
+ attempt: streamRetries,
565
+ fixedDelayMs: options?.streamStallRetryDelayMs,
566
+ });
567
+ streamRetries += 1;
568
+ retryAttempt = true;
569
+ await waitForCursorStreamRetry(retryDelayMs, options?.signal);
570
+ if (options?.signal?.aborted) {
571
+ retryAttempt = false;
572
+ output.stopReason = "aborted";
573
+ output.errorMessage = "Request was aborted";
574
+ stream.push({ type: "error", reason: output.stopReason, error: output });
575
+ stream.end();
576
+ }
577
+ continue;
578
+ }
440
579
  // A stream that dies mid-turn leaves blocks open. Closing them here
441
580
  // settles their live cards and pairs the server-owned calls that
442
581
  // nothing else answers — an unpaired call is stripped from every
@@ -485,14 +624,14 @@ export const stream = (model, context, options) => {
485
624
  const blobs = conversationBlobStores.get(conversationId);
486
625
  if (blobs)
487
626
  conversationBlobStores.set(decision.wireId, blobs);
488
- retryPoisonedConversation = true;
627
+ retryAttempt = true;
489
628
  }
490
629
  else {
491
630
  message = CURSOR_CONVERSATION_POISONED_MESSAGE;
492
631
  }
493
632
  }
494
633
  }
495
- if (!retryPoisonedConversation) {
634
+ if (!retryAttempt) {
496
635
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
497
636
  output.errorMessage = message;
498
637
  stream.push({ type: "error", reason: output.stopReason, error: output });
@@ -509,7 +648,7 @@ export const stream = (model, context, options) => {
509
648
  h2Request = null;
510
649
  h2Client = null;
511
650
  }
512
- } while (retryPoisonedConversation);
651
+ } while (retryAttempt);
513
652
  })();
514
653
  return stream;
515
654
  };
@@ -3195,7 +3334,7 @@ async function buildGrpcRequest(model, context, options, state) {
3195
3334
  }
3196
3335
  }
3197
3336
  const action = create(ConversationActionSchema, {
3198
- action: userContent && (userText.trim().length > 0 || hasUserImages)
3337
+ action: !state.forceResumeAction && userContent && (userText.trim().length > 0 || hasUserImages)
3199
3338
  ? {
3200
3339
  case: "userMessageAction",
3201
3340
  value: create(UserMessageActionSchema, {
@@ -3244,15 +3383,16 @@ async function buildGrpcRequest(model, context, options, state) {
3244
3383
  rootPromptMessagesJson,
3245
3384
  turns,
3246
3385
  });
3247
- const requestedModel = buildRequestedModel(model, options?.thinkingSelection);
3386
+ const requestedModel = state.pinnedRequestedModel ?? buildRequestedModel(model, options?.thinkingSelection);
3248
3387
  const wireModelId = requestedModel.modelId;
3249
3388
  const cursorMaxMode = model.compat?.cursorMaxMode === true;
3250
- const modelDetails = create(ModelDetailsSchema, {
3251
- modelId: wireModelId,
3252
- displayModelId: model.id,
3253
- displayName: model.name,
3254
- ...(cursorMaxMode ? { maxMode: true } : undefined),
3255
- });
3389
+ const modelDetails = state.pinnedModelDetails ??
3390
+ create(ModelDetailsSchema, {
3391
+ modelId: wireModelId,
3392
+ displayModelId: model.id,
3393
+ displayName: model.name,
3394
+ ...(cursorMaxMode ? { maxMode: true } : undefined),
3395
+ });
3256
3396
  const runRequest = create(AgentRunRequestSchema, {
3257
3397
  conversationState,
3258
3398
  action,
@@ -3272,7 +3412,7 @@ async function buildGrpcRequest(model, context, options, state) {
3272
3412
  bytes: requestBytes.length,
3273
3413
  tools: context.tools?.length ?? 0,
3274
3414
  });
3275
- return { requestBytes, blobStore, conversationState };
3415
+ return { requestBytes, blobStore, conversationState, requestedModel, modelDetails };
3276
3416
  }
3277
3417
  function hasImages(content) {
3278
3418
  return content.some((item) => item.type === "image");