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.
- package/CHANGELOG.md +52 -1
- package/README.md +15 -7
- package/package.json +8 -7
- package/src/agent/assistant-text-delta.ts +10 -0
- package/src/agent/loop.ts +116 -22
- package/src/agent/runtime-session.ts +248 -1
- package/src/cli/command-line.ts +9 -1
- package/src/cli/config.ts +17 -4
- package/src/cli/main.ts +1 -0
- package/src/cli/public-cli-contract.ts +4 -0
- package/src/cli/public-config-contract.ts +25 -1
- package/src/cli/run-runner.ts +5 -0
- package/src/cli/runner-dependencies.ts +4 -1
- package/src/cli/tui-runner.tsx +17 -2
- package/src/events/bash-result-detail.ts +13 -6
- package/src/events/observation-text-log.ts +26 -1
- package/src/events/stdout-event-printer.ts +18 -2
- package/src/events/types.ts +14 -2
- package/src/model/fake-model-client.ts +177 -0
- package/src/model/model-client.ts +3 -0
- package/src/model/openai-chat-model-client.ts +54 -15
- package/src/model/openai-chat-stream.ts +95 -72
- package/src/observation/observation-builder.ts +54 -6
- package/src/session/session-catalog.ts +17 -11
- package/src/session/session-store.ts +2 -0
- package/src/tools/bash-guard.ts +131 -0
- package/src/tools/bash-task.ts +129 -90
- package/src/tools/bash.ts +75 -13
- package/src/tools/delete.ts +182 -0
- package/src/tools/edit.ts +68 -9
- package/src/tools/registry.ts +49 -3
- package/src/tools/shell-process.ts +296 -0
- package/src/tools/task-input.ts +229 -0
- package/src/tools/task-output-tool.ts +4 -1
- package/src/tools/terminal-screen.ts +105 -0
- package/src/tools/turn-undo-manager.ts +794 -0
- package/src/tools/types.ts +45 -0
- package/src/tools/write.ts +65 -14
- package/src/tui/app.tsx +161 -45
- package/src/tui/assistant-markdown-section-framer.ts +135 -0
- package/src/tui/components/background-tasks.tsx +3 -2
- package/src/tui/components/bash-confirmation.tsx +27 -0
- package/src/tui/components/context-status.tsx +11 -1
- package/src/tui/components/footer.tsx +8 -5
- package/src/tui/components/prompt-input.tsx +13 -1
- package/src/tui/components/resume-session-picker.tsx +292 -46
- package/src/tui/components/timeline.tsx +10 -0
- package/src/tui/context-format.ts +17 -0
- package/src/tui/event-store.ts +76 -4
- package/src/tui/slash-commands.ts +28 -0
- package/src/tui/tui-projection-store.ts +246 -7
- 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 (
|
|
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 (
|
|
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
|
}
|
package/src/events/types.ts
CHANGED
|
@@ -285,8 +285,8 @@ export type TurnFinishedData = {
|
|
|
285
285
|
};
|
|
286
286
|
|
|
287
287
|
export type ModelRequestAttemptData = {
|
|
288
|
-
attemptNumber:
|
|
289
|
-
maxAttempts:
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
|
254
|
+
private async requestStreaming(
|
|
252
255
|
prepared: PreparedModelRequest,
|
|
253
|
-
|
|
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
|
-
|
|
268
|
+
const content = accumulator.push(chunk);
|
|
269
|
+
if (content !== undefined && content !== "") {
|
|
270
|
+
options.onTextDelta?.(content);
|
|
271
|
+
}
|
|
263
272
|
}
|
|
264
|
-
return
|
|
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
|
-
|
|
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
|
+
}
|