tinker-agent 1.5.1 → 1.7.0

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 (52) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/README.md +15 -7
  3. package/package.json +8 -7
  4. package/src/agent/assistant-text-delta.ts +10 -0
  5. package/src/agent/loop.ts +116 -22
  6. package/src/agent/runtime-session.ts +248 -1
  7. package/src/cli/command-line.ts +9 -1
  8. package/src/cli/config.ts +17 -4
  9. package/src/cli/main.ts +1 -0
  10. package/src/cli/public-cli-contract.ts +4 -0
  11. package/src/cli/public-config-contract.ts +25 -1
  12. package/src/cli/run-runner.ts +5 -0
  13. package/src/cli/runner-dependencies.ts +4 -1
  14. package/src/cli/tui-runner.tsx +17 -2
  15. package/src/events/bash-result-detail.ts +13 -6
  16. package/src/events/observation-text-log.ts +26 -1
  17. package/src/events/stdout-event-printer.ts +18 -2
  18. package/src/events/types.ts +14 -2
  19. package/src/model/fake-model-client.ts +177 -0
  20. package/src/model/model-client.ts +3 -0
  21. package/src/model/openai-chat-model-client.ts +54 -15
  22. package/src/model/openai-chat-stream.ts +95 -72
  23. package/src/observation/observation-builder.ts +54 -6
  24. package/src/session/session-catalog.ts +17 -11
  25. package/src/session/session-store.ts +2 -0
  26. package/src/tools/bash-guard.ts +131 -0
  27. package/src/tools/bash-task.ts +129 -90
  28. package/src/tools/bash.ts +75 -13
  29. package/src/tools/delete.ts +182 -0
  30. package/src/tools/edit.ts +68 -9
  31. package/src/tools/registry.ts +49 -3
  32. package/src/tools/shell-process.ts +296 -0
  33. package/src/tools/task-input.ts +229 -0
  34. package/src/tools/task-output-tool.ts +4 -1
  35. package/src/tools/terminal-screen.ts +105 -0
  36. package/src/tools/turn-undo-manager.ts +794 -0
  37. package/src/tools/types.ts +45 -0
  38. package/src/tools/write.ts +65 -14
  39. package/src/tui/app.tsx +161 -45
  40. package/src/tui/assistant-markdown-section-framer.ts +135 -0
  41. package/src/tui/components/background-tasks.tsx +3 -2
  42. package/src/tui/components/bash-confirmation.tsx +27 -0
  43. package/src/tui/components/context-status.tsx +11 -1
  44. package/src/tui/components/footer.tsx +8 -5
  45. package/src/tui/components/prompt-input.tsx +13 -1
  46. package/src/tui/components/resume-session-picker.tsx +292 -46
  47. package/src/tui/components/timeline.tsx +10 -0
  48. package/src/tui/context-format.ts +17 -0
  49. package/src/tui/event-store.ts +76 -4
  50. package/src/tui/slash-commands.ts +28 -0
  51. package/src/tui/tui-projection-store.ts +246 -7
  52. package/src/tui/tui-session-controller.ts +19 -1
@@ -39,6 +39,27 @@ export function renderObservationLogEvent(event: AgentEvent): string | undefined
39
39
  return renderAssistantProgress(event);
40
40
  case "tool.observation":
41
41
  return renderToolObservation(event);
42
+ case "tool.confirmation.requested":
43
+ return [
44
+ "## Bash confirmation requested",
45
+ "",
46
+ `Reason: ${event.data.reason}`,
47
+ "",
48
+ event.data.command,
49
+ "",
50
+ "---",
51
+ "",
52
+ ].join("\n");
53
+ case "tool.confirmation.resolved":
54
+ return [
55
+ "## Bash confirmation resolved",
56
+ "",
57
+ `Decision: ${event.data.decision}`,
58
+ `Duration: ${event.data.durationMs} ms`,
59
+ "",
60
+ "---",
61
+ "",
62
+ ].join("\n");
42
63
  case "turn.finished":
43
64
  return renderTurnFinished(event.data);
44
65
  case "turn.cancelled":
@@ -158,7 +179,11 @@ function toolCallSummary(call: ToolCall): string {
158
179
  .join("\n");
159
180
  }
160
181
 
161
- if (call.name === "TaskOutput" || call.name === "TaskStop") {
182
+ if (
183
+ call.name === "TaskOutput" ||
184
+ call.name === "TaskInput" ||
185
+ call.name === "TaskStop"
186
+ ) {
162
187
  const taskId = stringProperty(args, "task_id");
163
188
  return [
164
189
  `Call ID: ${call.toolCallId}`,
@@ -117,6 +117,16 @@ export class StdoutEventPrinter implements EventSink {
117
117
  `${formatToolLine("tool.finished", event.data.call).trimEnd()} ok=${event.data.ok}\n`,
118
118
  );
119
119
  break;
120
+ case "tool.confirmation.requested":
121
+ this.stdout.write(
122
+ `tool.confirmation.requested toolCallId=${event.toolCallId} reason=${JSON.stringify(event.data.reason)} command=${JSON.stringify(event.data.command)}\n`,
123
+ );
124
+ break;
125
+ case "tool.confirmation.resolved":
126
+ this.stdout.write(
127
+ `tool.confirmation.resolved toolCallId=${event.toolCallId} decision=${event.data.decision} durationMs=${event.data.durationMs}\n`,
128
+ );
129
+ break;
120
130
  case "mcp.server.connected":
121
131
  this.stdout.write(
122
132
  `mcp.server.connected name=${event.data.serverName} tools=${event.data.toolCount}\n`,
@@ -192,6 +202,7 @@ function formatToolRawResult(call: ToolCall, raw: ToolRawResult): string[] {
192
202
  return optionalLine(formatDiff(call, raw));
193
203
  case "bash":
194
204
  case "task_output":
205
+ case "task_input":
195
206
  return optionalLine(formatBashResult(call, raw));
196
207
  case "task_list":
197
208
  case "task_stop":
@@ -199,6 +210,7 @@ function formatToolRawResult(call: ToolCall, raw: ToolRawResult): string[] {
199
210
  case "skill":
200
211
  return [formatSkillResult(raw)];
201
212
  case "read":
213
+ case "delete":
202
214
  case "glob":
203
215
  case "grep":
204
216
  case "web_search":
@@ -267,7 +279,7 @@ function formatDiff(call: ToolCall, raw: ToolRawResult): string | undefined {
267
279
  }
268
280
 
269
281
  function formatBashResult(_call: ToolCall, raw: ToolRawResult): string | undefined {
270
- if (raw.kind !== "bash" && raw.kind !== "task_output") {
282
+ if (raw.kind !== "bash" && raw.kind !== "task_output" && raw.kind !== "task_input") {
271
283
  return undefined;
272
284
  }
273
285
 
@@ -337,7 +349,11 @@ function formatToolLine(prefix: string, call: ToolCall): string {
337
349
  return `${prefix} name=${call.name}\n`;
338
350
  }
339
351
 
340
- if (call.name === "TaskOutput" || call.name === "TaskStop") {
352
+ if (
353
+ call.name === "TaskOutput" ||
354
+ call.name === "TaskInput" ||
355
+ call.name === "TaskStop"
356
+ ) {
341
357
  const taskId = toolTaskId(call);
342
358
  return `${prefix} name=${call.name}${taskId === undefined ? "" : ` task=${taskId}`}\n`;
343
359
  }
@@ -285,8 +285,8 @@ export type TurnFinishedData = {
285
285
  };
286
286
 
287
287
  export type ModelRequestAttemptData = {
288
- attemptNumber: 1 | 2;
289
- maxAttempts: 2;
288
+ attemptNumber: number;
289
+ maxAttempts: number;
290
290
  };
291
291
 
292
292
  export type ModelRequestFailureCode = ProviderResponseErrorCode;
@@ -294,6 +294,7 @@ export type ModelRequestFailureCode = ProviderResponseErrorCode;
294
294
  export type ModelRequestFailedData = ModelRequestAttemptData & {
295
295
  code: ModelRequestFailureCode;
296
296
  retryDisposition: "scheduled" | "not_retryable" | "exhausted";
297
+ retryDelayMs?: number;
297
298
  provider: string;
298
299
  model: string;
299
300
  error: string;
@@ -328,6 +329,16 @@ export type AgentEventDataMap = {
328
329
  "tool.raw_result": { call: ToolCall; raw: ToolRawResult };
329
330
  "tool.finished": { call: ToolCall; ok: boolean };
330
331
  "tool.observation": { call: ToolCall; observation: ToolObservation };
332
+ "tool.confirmation.requested": {
333
+ command: string;
334
+ reason: string;
335
+ };
336
+ "tool.confirmation.resolved": {
337
+ command: string;
338
+ reason: string;
339
+ decision: "allow" | "deny" | "cancelled";
340
+ durationMs: number;
341
+ };
331
342
  "agent.iteration.finished": {
332
343
  outcome: "continue" | "completed";
333
344
  toolCallCount: number;
@@ -419,6 +430,7 @@ export type AgentEventInput =
419
430
  | ToolEventInput<
420
431
  "tool.started" | "tool.raw_result" | "tool.finished" | "tool.observation"
421
432
  >
433
+ | ToolEventInput<"tool.confirmation.requested" | "tool.confirmation.resolved">
422
434
  | ToolEventInput<
423
435
  "bash.task.backgrounded" | "bash.task.stopping" | "bash.task.finished"
424
436
  >;
@@ -229,6 +229,9 @@ export class FakeModelClient implements ModelClient {
229
229
  if (this.mode === "pty-static-history") {
230
230
  return this.ptyStaticHistory(input, prepared, options);
231
231
  }
232
+ if (this.mode === "pty-incremental-output") {
233
+ return this.ptyIncrementalOutput(input, prepared, options);
234
+ }
232
235
  if (this.mode === "pty-resume-layout") {
233
236
  return this.ptyResumeLayout(input, prepared, options);
234
237
  }
@@ -238,9 +241,15 @@ export class FakeModelClient implements ModelClient {
238
241
  if (this.mode === "pty-tool-chain") {
239
242
  return this.ptyToolChain(input, prepared, options);
240
243
  }
244
+ if (this.mode === "pty-turn-undo") {
245
+ return this.ptyTurnUndo(input, prepared, options);
246
+ }
241
247
  if (this.mode === "pty-background-task") {
242
248
  return this.ptyBackgroundTask(input, prepared, options);
243
249
  }
250
+ if (this.mode === "pty-interactive-terminal") {
251
+ return this.ptyInteractiveTerminal(input, prepared, options);
252
+ }
244
253
  if (this.mode === "pty-resume") {
245
254
  return this.ptyResume(input, prepared, options);
246
255
  }
@@ -361,6 +370,39 @@ export class FakeModelClient implements ModelClient {
361
370
  return textOutput(prepared, "PTY_STATIC_LIVE_DONE");
362
371
  }
363
372
 
373
+ private async ptyIncrementalOutput(
374
+ input: ModelRequestInput,
375
+ prepared: PreparedModelRequest,
376
+ options: ModelRequestOptions,
377
+ ): Promise<ModelRequestOutput> {
378
+ const prompt = lastUserMessage(input.messages);
379
+ if (prompt !== "PTY_INCREMENTAL_OUTPUT") {
380
+ throw new Error(
381
+ `Unexpected pty-incremental-output prompt: ${JSON.stringify(prompt)}.`,
382
+ );
383
+ }
384
+
385
+ const chunks = [
386
+ "## PTY incremental first\nPTY_INCREMENTAL_EARLY_SENTINEL\n\n## PTY incre",
387
+ "mental second\n",
388
+ "PTY_INCREMENTAL_SECOND_BODY\n\n## PTY incremental final\n",
389
+ "PTY_INCREMENTAL_FINAL_SENTINEL",
390
+ ] as const;
391
+ options.onTextDelta?.(chunks[0]);
392
+ await Bun.sleep(50);
393
+ options.signal.throwIfAborted();
394
+ options.onTextDelta?.(chunks[1]);
395
+ await Bun.sleep(700);
396
+ options.signal.throwIfAborted();
397
+ options.onTextDelta?.(chunks[2]);
398
+ await Bun.sleep(100);
399
+ options.signal.throwIfAborted();
400
+ options.onTextDelta?.(chunks[3]);
401
+ await Bun.sleep(100);
402
+ options.signal.throwIfAborted();
403
+ return textOutput(prepared, chunks.join(""));
404
+ }
405
+
364
406
  private ptyResumeLayout(
365
407
  input: ModelRequestInput,
366
408
  prepared: PreparedModelRequest,
@@ -504,6 +546,60 @@ export class FakeModelClient implements ModelClient {
504
546
  return textOutput(prepared, "PTY_TOOL_CHAIN_DONE");
505
547
  }
506
548
 
549
+ private ptyTurnUndo(
550
+ input: ModelRequestInput,
551
+ prepared: PreparedModelRequest,
552
+ options: ModelRequestOptions,
553
+ ): ModelRequestOutput {
554
+ requireTools(input, ["Read", "Write", "Delete"]);
555
+ const prompt = lastUserMessage(input.messages);
556
+ if (prompt !== "PTY_UNDO_MUTATE") {
557
+ throw new Error(`Unexpected pty-turn-undo prompt: ${JSON.stringify(prompt)}.`);
558
+ }
559
+
560
+ const tools = toolMessagesAfterLastUser(input.messages);
561
+ const read = tools.find((message) => message.name === "Read");
562
+ if (read === undefined) {
563
+ return toolCallOutput(prepared, options, "Read", {
564
+ file_path: "pty-undo-modified.txt",
565
+ });
566
+ }
567
+ if (!read.content.includes("Read succeeded")) {
568
+ throw new Error("PTY undo Read tool did not succeed.");
569
+ }
570
+
571
+ const writes = tools.filter((message) => message.name === "Write");
572
+ if (writes.length === 0) {
573
+ return toolCallOutput(prepared, options, "Write", {
574
+ file_path: "pty-undo-modified.txt",
575
+ content: "after undo turn\n",
576
+ });
577
+ }
578
+ if (!writes[0]?.content.includes("Write succeeded")) {
579
+ throw new Error("PTY undo modifying Write did not succeed.");
580
+ }
581
+ if (writes.length === 1) {
582
+ return toolCallOutput(prepared, options, "Write", {
583
+ file_path: "pty-undo-created/nested.txt",
584
+ content: "created by undo turn\n",
585
+ });
586
+ }
587
+ if (!writes[1]?.content.includes("Write succeeded")) {
588
+ throw new Error("PTY undo creating Write did not succeed.");
589
+ }
590
+
591
+ const deletion = tools.find((message) => message.name === "Delete");
592
+ if (deletion === undefined) {
593
+ return toolCallOutput(prepared, options, "Delete", {
594
+ file_path: "pty-undo-deleted.bin",
595
+ });
596
+ }
597
+ if (!deletion.content.includes("Delete succeeded")) {
598
+ throw new Error("PTY undo Delete tool did not succeed.");
599
+ }
600
+ return textOutput(prepared, "PTY_UNDO_MUTATIONS_DONE");
601
+ }
602
+
507
603
  private ptyBackgroundTask(
508
604
  input: ModelRequestInput,
509
605
  prepared: PreparedModelRequest,
@@ -565,6 +661,87 @@ export class FakeModelClient implements ModelClient {
565
661
  return textOutput(prepared, "PTY_BACKGROUND_STOPPED");
566
662
  }
567
663
 
664
+ private ptyInteractiveTerminal(
665
+ input: ModelRequestInput,
666
+ prepared: PreparedModelRequest,
667
+ options: ModelRequestOptions,
668
+ ): ModelRequestOutput {
669
+ requireTools(input, ["Bash", "TaskOutput", "TaskInput"]);
670
+ const prompt = lastUserMessage(input.messages);
671
+ if (prompt === "PTY_INTERACTIVE_FOLLOWUP") {
672
+ requireMessage(input.messages, "assistant", "PTY_INTERACTIVE_DONE");
673
+ return textOutput(prepared, "PTY_INTERACTIVE_FOLLOWUP_DONE");
674
+ }
675
+ if (prompt !== "PTY_INTERACTIVE_TERMINAL" && prompt !== "PTY_INTERACTIVE_QUIT") {
676
+ throw new Error(
677
+ `Unexpected pty-interactive-terminal prompt: ${JSON.stringify(prompt)}.`,
678
+ );
679
+ }
680
+
681
+ const tools = toolMessagesAfterLastUser(input.messages);
682
+ const bash = tools.find((message) => message.name === "Bash");
683
+ if (bash === undefined) {
684
+ return toolCallOutput(prepared, options, "Bash", {
685
+ command: "python3 -q",
686
+ description: "Start interactive Python fixture",
687
+ tty: true,
688
+ timeout: 25,
689
+ });
690
+ }
691
+ if (!bash.content.includes("taskId=") || !bash.content.includes("tty=true")) {
692
+ throw new Error("PTY Bash task did not return an interactive task ID.");
693
+ }
694
+ const taskId = requireObservationValue(bash.content, "taskId");
695
+
696
+ const outputs = tools.filter((message) => message.name === "TaskOutput");
697
+ const output = outputs.at(-1);
698
+ if (output === undefined || !output.content.includes(">>>")) {
699
+ if (outputs.length >= 20) {
700
+ throw new Error("Interactive Python fixture did not show its prompt.");
701
+ }
702
+ return toolCallOutput(prepared, options, "TaskOutput", {
703
+ task_id: taskId,
704
+ });
705
+ }
706
+
707
+ const inputs = tools.filter((message) => message.name === "TaskInput");
708
+ if (inputs.length === 0) {
709
+ return toolCallOutput(prepared, options, "TaskInput", {
710
+ task_id: taskId,
711
+ chars:
712
+ prompt === "PTY_INTERACTIVE_QUIT"
713
+ ? "import os; print('PTY_INTERACTIVE_PID=' + str(os.getpid()))\n"
714
+ : "print(6 * 7)\n",
715
+ wait_ms: 250,
716
+ });
717
+ }
718
+
719
+ const latestInput = inputs.at(-1);
720
+ const expected = prompt === "PTY_INTERACTIVE_QUIT" ? "PTY_INTERACTIVE_PID=" : "42";
721
+ if (!latestInput?.content.includes(expected)) {
722
+ if (inputs.length >= 20) {
723
+ throw new Error(`Interactive Python fixture did not show ${expected}.`);
724
+ }
725
+ return toolCallOutput(prepared, options, "TaskInput", {
726
+ task_id: taskId,
727
+ chars: "",
728
+ wait_ms: 250,
729
+ });
730
+ }
731
+
732
+ if (prompt === "PTY_INTERACTIVE_QUIT") {
733
+ return textOutput(prepared, "PTY_INTERACTIVE_RUNNING");
734
+ }
735
+ if (!inputs.some((message) => message.content.includes("status=completed"))) {
736
+ return toolCallOutput(prepared, options, "TaskInput", {
737
+ task_id: taskId,
738
+ chars: "exit()\n",
739
+ wait_ms: 500,
740
+ });
741
+ }
742
+ return textOutput(prepared, "PTY_INTERACTIVE_DONE");
743
+ }
744
+
568
745
  private ptyResume(
569
746
  input: ModelRequestInput,
570
747
  prepared: PreparedModelRequest,
@@ -36,6 +36,7 @@ export type ModelMessageProtocol = {
36
36
 
37
37
  export type ModelRequestOptions = {
38
38
  signal: AbortSignal;
39
+ onTextDelta?: (content: string) => void;
39
40
  identity?: {
40
41
  iteration: IterationIdentity;
41
42
  runtimeSession: RuntimeSessionContext;
@@ -127,6 +128,8 @@ export type ProviderResponseErrorCode =
127
128
  | "reasoning_only_assistant"
128
129
  | "invalid_provider_response"
129
130
  | "invalid_provider_stream"
131
+ | "provider_rate_limited"
132
+ | "provider_unavailable"
130
133
  | "provider_request_error";
131
134
 
132
135
  export type ProviderResponseDiagnostics = {
@@ -11,7 +11,11 @@ import {
11
11
  import type { ImageAssetId, ImageAssetRef } from "../image/image-types";
12
12
  import type { ModelContextBudget } from "./model-context-profile";
13
13
  import type { InputTokenEstimator } from "./input-token-estimator";
14
- import { ModelRequestMediaAggregateError, ProviderResponseError } from "./model-client";
14
+ import {
15
+ ModelRequestMediaAggregateError,
16
+ ProviderResponseError,
17
+ type ProviderResponseErrorCode,
18
+ } from "./model-client";
15
19
  import type {
16
20
  MaterializedModelRequest,
17
21
  ModelClient,
@@ -30,7 +34,7 @@ import {
30
34
  toOpenAIChatMessages,
31
35
  toOpenAIChatTools,
32
36
  } from "./openai-chat-mapping";
33
- import { accumulateOpenAIChatCompletionChunks } from "./openai-chat-stream";
37
+ import { OpenAIChatCompletionStreamAccumulator } from "./openai-chat-stream";
34
38
  import { MoonshotInputTokenEstimator } from "./moonshot-input-token-estimator";
35
39
  import { sha256, stableJsonStringify } from "./model-request-preflight";
36
40
 
@@ -86,7 +90,9 @@ export class OpenAIChatModelClient implements ModelClient {
86
90
  apiKey: options.apiKey,
87
91
  baseURL: options.baseURL,
88
92
  timeout: options.timeoutMs ?? OPENAI_CHAT_TIMEOUT_MS,
89
- ...(supportsImages ? { maxRetries: 0 } : {}),
93
+ // Retries are orchestrated by the agent loop so they surface as
94
+ // cancellable, observable runtime events instead of hidden SDK waits.
95
+ maxRetries: 0,
90
96
  fetch: options.fetch,
91
97
  });
92
98
  if (options.tokenEstimator !== undefined) {
@@ -235,10 +241,7 @@ export class OpenAIChatModelClient implements ModelClient {
235
241
  }
236
242
 
237
243
  const response = this.stream
238
- ? accumulateOpenAIChatCompletionChunks(
239
- await this.collectStreamingChunks(prepared, options.signal),
240
- { provider: this.provider, model: this.options.model },
241
- )
244
+ ? await this.requestStreaming(prepared, options)
242
245
  : await this.requestNonStreaming(prepared, options.signal);
243
246
 
244
247
  return fromOpenAIChatCompletion(response, {
@@ -248,21 +251,30 @@ export class OpenAIChatModelClient implements ModelClient {
248
251
  });
249
252
  }
250
253
 
251
- private async collectStreamingChunks(
254
+ private async requestStreaming(
252
255
  prepared: PreparedModelRequest,
253
- signal: AbortSignal,
254
- ): Promise<unknown[]> {
256
+ options: ModelRequestOptions,
257
+ ): Promise<Record<string, unknown>> {
258
+ const accumulator = new OpenAIChatCompletionStreamAccumulator({
259
+ provider: this.provider,
260
+ model: this.options.model,
261
+ });
255
262
  try {
256
263
  const stream = await this.client.chat.completions.create(
257
264
  prepared.payload as ChatCompletionCreateParamsStreaming,
258
- { signal },
265
+ { signal: options.signal },
259
266
  );
260
- const chunks: unknown[] = [];
261
267
  for await (const chunk of stream) {
262
- chunks.push(chunk);
268
+ const content = accumulator.push(chunk);
269
+ if (content !== undefined && content !== "") {
270
+ options.onTextDelta?.(content);
271
+ }
263
272
  }
264
- return chunks;
273
+ return accumulator.finish();
265
274
  } catch (error) {
275
+ if (error instanceof ProviderResponseError) {
276
+ throw error;
277
+ }
266
278
  throw sanitizedProviderError(error, this.provider, this.options.model);
267
279
  }
268
280
  }
@@ -531,9 +543,36 @@ function sanitizedProviderError(
531
543
  )
532
544
  .replace(/Bearer\s+[A-Za-z0-9._~+/-]+/giu, "Bearer [redacted]");
533
545
  return new ProviderResponseError(
534
- "provider_request_error",
546
+ providerErrorCode(error),
535
547
  sanitized,
536
548
  { provider, model },
537
549
  { cause: error },
538
550
  );
539
551
  }
552
+
553
+ function providerErrorCode(error: unknown): ProviderResponseErrorCode {
554
+ const status = providerErrorStatus(error);
555
+ if (status === 429) {
556
+ return "provider_rate_limited";
557
+ }
558
+ if (status === 500 || status === 502 || status === 503 || status === 504) {
559
+ return "provider_unavailable";
560
+ }
561
+ if (status === undefined && isProviderConnectionError(error)) {
562
+ return "provider_unavailable";
563
+ }
564
+ return "provider_request_error";
565
+ }
566
+
567
+ function providerErrorStatus(error: unknown): number | undefined {
568
+ if (typeof error !== "object" || error === null || !("status" in error)) {
569
+ return undefined;
570
+ }
571
+ const status = (error as { status?: unknown }).status;
572
+ return typeof status === "number" ? status : undefined;
573
+ }
574
+
575
+ function isProviderConnectionError(error: unknown): boolean {
576
+ // APIConnectionTimeoutError extends APIConnectionError in the SDK.
577
+ return error instanceof OpenAI.APIConnectionError;
578
+ }