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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,55 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.7.0] - 2026-08-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Run interactive terminal programs through PTY-backed Bash tasks, send exact
|
|
13
|
+
keystrokes with `TaskInput`, and inspect their current terminal screen with
|
|
14
|
+
`TaskOutput`.
|
|
15
|
+
- Search all resumable sessions from the `/resume` picker by text from their
|
|
16
|
+
first user prompt, including older sessions beyond the 20 most recent.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Keep the live background-task panel compact by showing at most two tasks and
|
|
21
|
+
label PTY-backed tasks explicitly.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Complete PTY tasks reliably after subprocess exit on Linux, including the EIO
|
|
26
|
+
signal reported when the terminal closes.
|
|
27
|
+
|
|
28
|
+
## [1.6.0] - 2026-08-01
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Add an `/undo` slash command that restores the files changed by the most recent
|
|
33
|
+
file-mutation turn from in-memory snapshots, with restore notices shown in the
|
|
34
|
+
TUI timeline.
|
|
35
|
+
- Give the agent a Delete tool for removing files, guarded by the same permission
|
|
36
|
+
flow as the other file-mutation tools.
|
|
37
|
+
- Guard destructive Bash commands behind an interactive confirmation prompt, with
|
|
38
|
+
a `/yolo` opt-out for sessions where unattended execution is intended.
|
|
39
|
+
- Show the latest provider cache hit rate in the TUI prompt status line.
|
|
40
|
+
- Stream assistant output in the TUI as sealed Markdown sections, so settled
|
|
41
|
+
content no longer repaints while the turn is still running.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Retry transient provider failures automatically with backoff instead of
|
|
46
|
+
surfacing them as immediate turn errors.
|
|
47
|
+
- Update runtime dependencies, including `commander` 15, `openai` 7, and the
|
|
48
|
+
latest `@assistant-ui/react-ink` packages.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Floor the displayed cache hit rate so append-heavy turns never show a false
|
|
53
|
+
100%.
|
|
54
|
+
- Bound the TUI live region below the viewport height to protect the prompt
|
|
55
|
+
frame from being pushed out of view.
|
|
56
|
+
|
|
8
57
|
## [1.5.1] - 2026-07-29
|
|
9
58
|
|
|
10
59
|
### Changed
|
|
@@ -100,7 +149,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
100
149
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
101
150
|
executable.
|
|
102
151
|
|
|
103
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/v1.
|
|
152
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v1.7.0...HEAD
|
|
153
|
+
[1.7.0]: https://github.com/ishowshao/tinker/releases/tag/v1.7.0
|
|
154
|
+
[1.6.0]: https://github.com/ishowshao/tinker/releases/tag/v1.6.0
|
|
104
155
|
[1.5.1]: https://github.com/ishowshao/tinker/releases/tag/v1.5.1
|
|
105
156
|
[1.5.0]: https://github.com/ishowshao/tinker/releases/tag/v1.5.0
|
|
106
157
|
[1.4.0]: https://github.com/ishowshao/tinker/releases/tag/v1.4.0
|
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ Built with [Bun](https://bun.sh) + TypeScript ESM, powered by [Ink](https://gith
|
|
|
11
11
|
- **Built-in tools**:
|
|
12
12
|
- `Glob` / `Grep` — Find and search files by pattern or content
|
|
13
13
|
- `Read` / `Write` / `Edit` — File I/O with content hashing and concurrent-modification protection
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
14
|
+
- `Delete` — Delete one existing regular file without directory or symlink support
|
|
15
|
+
- `Bash` — Run foreground, background, and PTY shell commands with per-task working directories
|
|
16
|
+
- `TaskList` / `TaskOutput` / `TaskInput` / `TaskStop` — Inspect, interact with, and stop long-running shell tasks
|
|
16
17
|
- `WebSearch` — Search the web via Exa API
|
|
17
18
|
- `WebFetch` — Fetch and refine web page content (local, browser, or Exa backend)
|
|
18
19
|
- `Recall` — Search or retrieve model-visible history from the current session
|
|
@@ -28,7 +29,7 @@ Built with [Bun](https://bun.sh) + TypeScript ESM, powered by [Ink](https://gith
|
|
|
28
29
|
- **Infinite Context architecture**: Immutable canonical history, deterministic
|
|
29
30
|
context revisions, Recall-addressable cold state, and qualified prefix retirement
|
|
30
31
|
keep long-running sessions recoverable without pretending the model has infinite
|
|
31
|
-
tokens. See the [technical design](docs/infinite-context-technical-design
|
|
32
|
+
tokens. See the [technical design](docs/infinite-context-technical-design.md).
|
|
32
33
|
- **Choice of models**: Uses an OpenAI-compatible Chat Completions transport with
|
|
33
34
|
explicit model and context limits. Actual provider support must be established
|
|
34
35
|
by a qualification matrix; transport compatibility alone is not a guarantee.
|
|
@@ -76,9 +77,9 @@ The installed package exposes this public CLI:
|
|
|
76
77
|
| --- | --- |
|
|
77
78
|
| `tinker` | Start the interactive terminal interface. |
|
|
78
79
|
| `tinker --profile <profile-name>` | Start the TUI with a selected model profile. |
|
|
79
|
-
| `tinker run [--profile <profile-name>] <prompt>` | Submit one shell-quoted prompt argument. |
|
|
80
|
-
| `tinker run [--profile <profile-name>] --stdin` | Read the prompt from standard input until EOF. |
|
|
81
|
-
| `tinker run [--profile <profile-name>] --file <path>` | Read the prompt from a UTF-8 text file. |
|
|
80
|
+
| `tinker run [--profile <profile-name>] [--yolo] <prompt>` | Submit one shell-quoted prompt argument. |
|
|
81
|
+
| `tinker run [--profile <profile-name>] [--yolo] --stdin` | Read the prompt from standard input until EOF. |
|
|
82
|
+
| `tinker run [--profile <profile-name>] [--yolo] --file <path>` | Read the prompt from a UTF-8 text file. |
|
|
82
83
|
| `tinker --help` | Show top-level CLI help. |
|
|
83
84
|
| `tinker help run` | Show one-shot command help. |
|
|
84
85
|
| `tinker --version` | Print the installed package version. |
|
|
@@ -146,6 +147,7 @@ are required. Boolean environment values accept case-insensitive `true/false`,
|
|
|
146
147
|
| `TINKER_MCP_MAX_OBSERVATION_CHARS` | Tooling | All modes | No | Positive integer | `40000` | No | Maximum model-visible characters in one MCP result. |
|
|
147
148
|
| `TINKER_BASH_DEFAULT_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `5000` | No | Default Bash foreground timeout in milliseconds. |
|
|
148
149
|
| `TINKER_BASH_MAX_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `600000` | No | Maximum Bash foreground timeout in milliseconds. |
|
|
150
|
+
| `TINKER_YOLO` | Tooling | All modes | No | Boolean | `false` | No | Allow high-confidence destructive Bash commands without confirmation. |
|
|
149
151
|
| `TINKER_GREP_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `20000` | No | Bundled ripgrep invocation timeout in milliseconds. |
|
|
150
152
|
| `TINKER_GREP_MAX_BUFFER_BYTES` | Tooling | All modes | No | Positive integer | `20000000` | No | Maximum buffered output from one ripgrep invocation. |
|
|
151
153
|
| `TINKER_WEBFETCH_REFINE_THRESHOLD` | Tooling | All modes | No | Positive integer | `2000` | No | Content-length threshold that enables WebFetch refinement. |
|
|
@@ -335,8 +337,10 @@ complete fixed policy and persistence contract.
|
|
|
335
337
|
| `/status` | Show session and context details |
|
|
336
338
|
| `/skills` | Show available and active Agent Skills |
|
|
337
339
|
| `/mcp` | Show MCP servers and runtime tools |
|
|
340
|
+
| `/yolo [on\|off]` | Show or change destructive Bash confirmation |
|
|
338
341
|
| `/memory` | Browse stored global memories |
|
|
339
342
|
| `/compact [retire]` | Swap tool output or retire a cold history prefix |
|
|
343
|
+
| `/undo` | Undo the latest Write/Edit/Delete turn |
|
|
340
344
|
| `/clear` | Start a new session and clear conversation |
|
|
341
345
|
| `/fork` | Clone the current session |
|
|
342
346
|
| `/view <path>` | View a local UTF-8 text file |
|
|
@@ -394,6 +398,10 @@ call always returns the complete requested line range. Use `offset` and `limit`
|
|
|
394
398
|
to page through larger files; oversized requests fail instead of returning
|
|
395
399
|
truncated content.
|
|
396
400
|
|
|
401
|
+
`Delete` removes one existing regular file by workspace-relative or absolute path.
|
|
402
|
+
It rejects directories and symbolic links, and clears any in-memory file snapshot
|
|
403
|
+
only after the removal succeeds.
|
|
404
|
+
|
|
397
405
|
## Project Structure
|
|
398
406
|
|
|
399
407
|
```
|
|
@@ -401,7 +409,7 @@ tinker/
|
|
|
401
409
|
├── src/
|
|
402
410
|
│ ├── cli/ # Entry points (tui, run), config
|
|
403
411
|
│ ├── agent/ # Agent loop, session ledger, turn cancellation, context metering
|
|
404
|
-
│ ├── tools/ # Tool executors (bash, glob, grep, read, write, edit, recall, etc.)
|
|
412
|
+
│ ├── tools/ # Tool executors (bash, glob, grep, read, write, edit, delete, recall, etc.)
|
|
405
413
|
│ ├── model/ # Model clients (OpenAI-compatible, fake), chat mapping, preflight
|
|
406
414
|
│ ├── mcp/ # MCP server management, tool executor adapter
|
|
407
415
|
│ ├── observation/ # Tool result → model-visible text
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinker-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A personal coding agent with an interactive TUI and one-shot CLI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -74,22 +74,25 @@
|
|
|
74
74
|
"typecheck": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@assistant-ui/react-ink": "^0.0.
|
|
78
|
-
"@assistant-ui/react-ink-markdown": "^0.0.
|
|
77
|
+
"@assistant-ui/react-ink": "^0.0.35",
|
|
78
|
+
"@assistant-ui/react-ink-markdown": "^0.0.34",
|
|
79
79
|
"@inkjs/ui": "^2.0.0",
|
|
80
80
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
81
81
|
"@mozilla/readability": "^0.6.0",
|
|
82
82
|
"@vscode/ripgrep": "1.18.0",
|
|
83
|
+
"@xterm/addon-unicode11": "0.9.0",
|
|
84
|
+
"@xterm/headless": "6.0.0",
|
|
83
85
|
"ansi-escapes": "^7.3.0",
|
|
84
86
|
"bun": "1.3.14",
|
|
85
87
|
"clipboardy": "^5.3.1",
|
|
86
|
-
"commander": "^
|
|
88
|
+
"commander": "^15.0.0",
|
|
87
89
|
"diff": "^9.0.0",
|
|
88
90
|
"glob": "^13.0.6",
|
|
89
91
|
"ink": "^7.1.0",
|
|
90
92
|
"linkedom": "^0.18.13",
|
|
91
93
|
"markdansi": "0.3.2",
|
|
92
|
-
"
|
|
94
|
+
"marked": "^18.0.5",
|
|
95
|
+
"openai": "^7.1.0",
|
|
93
96
|
"react": "^19.2.7",
|
|
94
97
|
"sharp": "^0.35.3",
|
|
95
98
|
"shiki": "^4.3.1",
|
|
@@ -104,8 +107,6 @@
|
|
|
104
107
|
"@types/chrome": "^0.2.2",
|
|
105
108
|
"@types/react": "^19.2.17",
|
|
106
109
|
"@types/turndown": "^5.0.6",
|
|
107
|
-
"@xterm/addon-unicode11": "0.9.0",
|
|
108
|
-
"@xterm/headless": "6.0.0",
|
|
109
110
|
"eslint": "^10.6.0",
|
|
110
111
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
111
112
|
"globals": "^17.7.0",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IterationIdentity } from "./types";
|
|
2
|
+
|
|
3
|
+
export type AssistantTextDeltaUpdate = IterationIdentity & {
|
|
4
|
+
attemptNumber: number;
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface AssistantTextDeltaSink {
|
|
9
|
+
updateAssistantTextDelta(update: AssistantTextDeltaUpdate): void;
|
|
10
|
+
}
|
package/src/agent/loop.ts
CHANGED
|
@@ -73,6 +73,7 @@ export type RunAgentInput = {
|
|
|
73
73
|
prepared: MaterializedModelRequest;
|
|
74
74
|
usage: ContextUsageSnapshot;
|
|
75
75
|
};
|
|
76
|
+
transientRetryDelaysMs?: readonly number[];
|
|
76
77
|
};
|
|
77
78
|
|
|
78
79
|
export class FatalAgentTurnError extends Error {
|
|
@@ -85,7 +86,10 @@ export class FatalAgentTurnError extends Error {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
const
|
|
89
|
+
const REASONING_ONLY_RETRY_LIMIT = 1 as const;
|
|
90
|
+
const TRANSIENT_RETRY_DELAYS_MS = [2_000, 4_000, 8_000, 16_000] as const;
|
|
91
|
+
const MODEL_REQUEST_MAX_ATTEMPTS =
|
|
92
|
+
1 + REASONING_ONLY_RETRY_LIMIT + TRANSIENT_RETRY_DELAYS_MS.length;
|
|
89
93
|
|
|
90
94
|
export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
91
95
|
let lastIteration: IterationIdentity | undefined;
|
|
@@ -226,23 +230,22 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
226
230
|
return failedResult(error, iteration);
|
|
227
231
|
}
|
|
228
232
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
identity: {
|
|
232
|
-
iteration,
|
|
233
|
-
runtimeSession: input.runtimeSession,
|
|
234
|
-
},
|
|
235
|
-
};
|
|
233
|
+
const transientRetryDelaysMs =
|
|
234
|
+
input.transientRetryDelaysMs ?? TRANSIENT_RETRY_DELAYS_MS;
|
|
236
235
|
let modelOutput: ModelRequestOutput | undefined;
|
|
237
|
-
let successfulAttempt:
|
|
238
|
-
|
|
236
|
+
let successfulAttempt: number | undefined;
|
|
237
|
+
let attemptNumber = 0;
|
|
238
|
+
let reasoningOnlyRetries = 0;
|
|
239
|
+
let transientRetries = 0;
|
|
240
|
+
while (true) {
|
|
239
241
|
if (input.signal.aborted) {
|
|
240
242
|
return cancelledResult(
|
|
241
243
|
cancellation(input.signal, iteration, "model_request"),
|
|
242
244
|
iteration,
|
|
243
245
|
);
|
|
244
246
|
}
|
|
245
|
-
|
|
247
|
+
attemptNumber += 1;
|
|
248
|
+
if (attemptNumber > 1) {
|
|
246
249
|
await input.runtimeSession.append({
|
|
247
250
|
type: "model.request.started",
|
|
248
251
|
...iteration,
|
|
@@ -255,7 +258,32 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
255
258
|
|
|
256
259
|
try {
|
|
257
260
|
throwIfTurnCancelled(input.signal);
|
|
258
|
-
|
|
261
|
+
let acceptingTextDeltas = true;
|
|
262
|
+
const onTextDelta =
|
|
263
|
+
input.runtimeSession.updateAssistantTextDelta === undefined
|
|
264
|
+
? undefined
|
|
265
|
+
: (content: string) => {
|
|
266
|
+
if (!acceptingTextDeltas || input.signal.aborted) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
input.runtimeSession.updateAssistantTextDelta?.({
|
|
270
|
+
...iteration,
|
|
271
|
+
attemptNumber,
|
|
272
|
+
content,
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
try {
|
|
276
|
+
modelOutput = await input.model.request(request, {
|
|
277
|
+
signal: input.signal,
|
|
278
|
+
identity: {
|
|
279
|
+
iteration,
|
|
280
|
+
runtimeSession: input.runtimeSession,
|
|
281
|
+
},
|
|
282
|
+
...(onTextDelta === undefined ? {} : { onTextDelta }),
|
|
283
|
+
});
|
|
284
|
+
} finally {
|
|
285
|
+
acceptingTextDeltas = false;
|
|
286
|
+
}
|
|
259
287
|
throwIfTurnCancelled(input.signal);
|
|
260
288
|
successfulAttempt = attemptNumber;
|
|
261
289
|
break;
|
|
@@ -267,12 +295,11 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
267
295
|
);
|
|
268
296
|
}
|
|
269
297
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
: "not_retryable";
|
|
298
|
+
const decision = modelRequestRetryDecision(error, {
|
|
299
|
+
reasoningOnlyRetries,
|
|
300
|
+
transientRetries,
|
|
301
|
+
transientRetryDelaysMs,
|
|
302
|
+
});
|
|
276
303
|
await input.runtimeSession.append({
|
|
277
304
|
type: "model.request.failed",
|
|
278
305
|
...iteration,
|
|
@@ -280,14 +307,26 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
280
307
|
error,
|
|
281
308
|
request,
|
|
282
309
|
attemptNumber,
|
|
283
|
-
retryDisposition,
|
|
310
|
+
retryDisposition: decision.disposition,
|
|
311
|
+
...(decision.disposition === "scheduled" && decision.delayMs > 0
|
|
312
|
+
? { retryDelayMs: decision.delayMs }
|
|
313
|
+
: {}),
|
|
284
314
|
}),
|
|
285
315
|
});
|
|
286
316
|
|
|
287
|
-
if (
|
|
317
|
+
if (decision.disposition === "scheduled") {
|
|
318
|
+
if (decision.kind === "reasoning_only") {
|
|
319
|
+
reasoningOnlyRetries += 1;
|
|
320
|
+
} else {
|
|
321
|
+
transientRetries += 1;
|
|
322
|
+
await waitForRetryDelay(decision.delayMs, input.signal);
|
|
323
|
+
}
|
|
288
324
|
continue;
|
|
289
325
|
}
|
|
290
|
-
if (
|
|
326
|
+
if (
|
|
327
|
+
decision.disposition === "exhausted" &&
|
|
328
|
+
decision.kind === "reasoning_only"
|
|
329
|
+
) {
|
|
291
330
|
return failedResult(
|
|
292
331
|
new Error(
|
|
293
332
|
`Provider returned reasoning without final text or tool calls in both attempts (provider=${request.provider}, model=${request.model}).`,
|
|
@@ -701,11 +740,65 @@ function isReasoningOnlyProviderError(error: unknown): error is ProviderResponse
|
|
|
701
740
|
);
|
|
702
741
|
}
|
|
703
742
|
|
|
743
|
+
function isTransientProviderError(error: unknown): error is ProviderResponseError {
|
|
744
|
+
return (
|
|
745
|
+
error instanceof ProviderResponseError &&
|
|
746
|
+
(error.code === "provider_rate_limited" || error.code === "provider_unavailable")
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
type ModelRequestRetryDecision =
|
|
751
|
+
| {
|
|
752
|
+
disposition: "scheduled";
|
|
753
|
+
kind: "reasoning_only" | "transient";
|
|
754
|
+
delayMs: number;
|
|
755
|
+
}
|
|
756
|
+
| { disposition: "exhausted"; kind: "reasoning_only" | "transient" }
|
|
757
|
+
| { disposition: "not_retryable" };
|
|
758
|
+
|
|
759
|
+
function modelRequestRetryDecision(
|
|
760
|
+
error: unknown,
|
|
761
|
+
state: {
|
|
762
|
+
reasoningOnlyRetries: number;
|
|
763
|
+
transientRetries: number;
|
|
764
|
+
transientRetryDelaysMs: readonly number[];
|
|
765
|
+
},
|
|
766
|
+
): ModelRequestRetryDecision {
|
|
767
|
+
if (isReasoningOnlyProviderError(error)) {
|
|
768
|
+
return state.reasoningOnlyRetries < REASONING_ONLY_RETRY_LIMIT
|
|
769
|
+
? { disposition: "scheduled", kind: "reasoning_only", delayMs: 0 }
|
|
770
|
+
: { disposition: "exhausted", kind: "reasoning_only" };
|
|
771
|
+
}
|
|
772
|
+
if (isTransientProviderError(error)) {
|
|
773
|
+
const delayMs = state.transientRetryDelaysMs[state.transientRetries];
|
|
774
|
+
return delayMs === undefined
|
|
775
|
+
? { disposition: "exhausted", kind: "transient" }
|
|
776
|
+
: { disposition: "scheduled", kind: "transient", delayMs };
|
|
777
|
+
}
|
|
778
|
+
return { disposition: "not_retryable" };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function waitForRetryDelay(delayMs: number, signal: AbortSignal): Promise<void> {
|
|
782
|
+
if (delayMs <= 0 || signal.aborted) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
await new Promise<void>((resolve) => {
|
|
786
|
+
const timer = setTimeout(cleanup, delayMs);
|
|
787
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
788
|
+
function cleanup() {
|
|
789
|
+
clearTimeout(timer);
|
|
790
|
+
signal.removeEventListener("abort", cleanup);
|
|
791
|
+
resolve();
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
704
796
|
function modelRequestFailureData(input: {
|
|
705
797
|
error: unknown;
|
|
706
798
|
request: PreparedModelRequest;
|
|
707
|
-
attemptNumber:
|
|
799
|
+
attemptNumber: number;
|
|
708
800
|
retryDisposition: "scheduled" | "not_retryable" | "exhausted";
|
|
801
|
+
retryDelayMs?: number;
|
|
709
802
|
}) {
|
|
710
803
|
const providerError =
|
|
711
804
|
input.error instanceof ProviderResponseError ? input.error : undefined;
|
|
@@ -714,6 +807,7 @@ function modelRequestFailureData(input: {
|
|
|
714
807
|
maxAttempts: MODEL_REQUEST_MAX_ATTEMPTS,
|
|
715
808
|
code: providerError?.code ?? ("provider_request_error" as const),
|
|
716
809
|
retryDisposition: input.retryDisposition,
|
|
810
|
+
...(input.retryDelayMs === undefined ? {} : { retryDelayMs: input.retryDelayMs }),
|
|
717
811
|
provider: input.request.provider,
|
|
718
812
|
model: input.request.model,
|
|
719
813
|
error: errorMessage(input.error),
|