tinker-agent 2.10.0 → 2.12.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 +47 -1
- package/README.md +44 -67
- package/package.json +4 -3
- package/src/agent/loop.ts +50 -13
- package/src/agent/runtime-provider-retry.ts +115 -0
- package/src/agent/runtime-session-contracts.ts +13 -29
- package/src/agent/runtime-session.ts +42 -67
- package/src/cli/config.ts +0 -29
- package/src/cli/model-profiles.ts +0 -80
- package/src/cli/public-config-contract.ts +1 -82
- package/src/cli/tui-runner.tsx +3 -41
- package/src/events/observation-text-log.ts +7 -0
- package/src/events/types.ts +8 -0
- package/src/image/abortable-file-open.ts +54 -0
- package/src/image/image-asset-store.ts +7 -1
- package/src/memory/contracts.ts +0 -256
- package/src/memory/memory-create-tool.ts +3 -5
- package/src/memory/memory-files.ts +145 -0
- package/src/memory/memory-search-output.ts +23 -0
- package/src/memory/memory-search-tool.ts +79 -175
- package/src/memory/memory-search.ts +115 -0
- package/src/model/fake-model-client.ts +20 -1
- package/src/model/openai-model-utils.ts +45 -1
- package/src/model/openai-responses-mapping.ts +11 -0
- package/src/model/openai-responses-stream.ts +14 -0
- package/src/observation/observation-builder.ts +5 -1
- package/src/session/scoped-query-database.ts +27 -0
- package/src/session/session-history-access.ts +4 -3
- package/src/session/session-store-contracts.ts +0 -23
- package/src/session/session-store.ts +9 -106
- package/src/tools/grep.ts +12 -4
- package/src/tools/registry.ts +15 -18
- package/src/tools/types.ts +12 -0
- package/src/tui/app.tsx +51 -7
- package/src/tui/components/ask-user.tsx +15 -8
- package/src/tui/components/prompt-input.tsx +14 -6
- package/src/tui/components/timeline.tsx +17 -9
- package/src/tui/event-store.ts +13 -1
- package/src/tui/file-mention.ts +29 -5
- package/src/tui/tui-projection-store.ts +5 -2
- package/src/tui/tui-session-controller.ts +8 -0
- package/src/tui/workspace-file-search.ts +21 -0
- package/src/cli/tui-memory.ts +0 -69
- package/src/memory/embedding-client.ts +0 -105
- package/src/memory/memory-coordinator.ts +0 -1274
- package/src/memory/memory-delete-tool.ts +0 -88
- package/src/memory/memory-extractor.ts +0 -252
- package/src/memory/memory-get-tool.ts +0 -86
- package/src/memory/memory-log.ts +0 -88
- package/src/memory/memory-store.ts +0 -1133
- package/src/memory/memory-update-tool.ts +0 -142
- package/src/memory/vector.ts +0 -153
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.12.0] - 2026-09-11
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Replace the vector memory system with plain-text Markdown records. Session
|
|
13
|
+
transcripts are appended to `memory/records/<sessionId>.md` in the global
|
|
14
|
+
Tinker home, and `MemoryCreate` saves explicit Markdown notes. `MemorySearch`
|
|
15
|
+
searches all memory files with literal, case-insensitive keywords and
|
|
16
|
+
paginated, grouped passages; the `MemoryGet`, `MemoryUpdate`, and
|
|
17
|
+
`MemoryDelete` tools and the `memory` model-profile configuration are
|
|
18
|
+
removed. Old memory configuration is ignored, and the existing memory
|
|
19
|
+
database and diagnostic files are moved to `memory-legacy/` on first use,
|
|
20
|
+
preserved but no longer searched. Memory now works in both the TUI and the
|
|
21
|
+
one-shot CLI without any model profile or embedding service.
|
|
22
|
+
- Simplify `MemoryCreate` to a Markdown note with the `text` heading and an
|
|
23
|
+
optional `summary` body, recording creation time and source workspace.
|
|
24
|
+
|
|
25
|
+
## [2.11.0] - 2026-09-08
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Offer directories in the TUI `@` file-mention popup. Ancestor directories of
|
|
30
|
+
workspace files now appear alongside files, ranked after matching files, and
|
|
31
|
+
selecting a directory inserts its path directly instead of attempting an
|
|
32
|
+
image import.
|
|
33
|
+
- Offer a retry selection in the TUI after provider retries are exhausted, so a
|
|
34
|
+
failed model request can be retried without re-entering the prompt.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Cache ESLint results for `check:fast` and split the fast test tier into
|
|
39
|
+
twelve independent shards, shortening local iteration while keeping the
|
|
40
|
+
serial full test run as the final gate.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Align `Grep` context-line precedence and remove an unsupported regex hint
|
|
45
|
+
from its documentation.
|
|
46
|
+
- Warm up the fake ripgrep binary in tests to absorb the macOS first-execution
|
|
47
|
+
validation delay.
|
|
48
|
+
- Cancel pending image file opens without leaking file handles.
|
|
49
|
+
- Emphasize TUI timeline labels and add spacing between messages.
|
|
50
|
+
- Finalize scoped SQLite queries before closing the session database.
|
|
51
|
+
|
|
8
52
|
## [2.10.0] - 2026-09-06
|
|
9
53
|
|
|
10
54
|
### Added
|
|
@@ -437,7 +481,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
437
481
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
438
482
|
executable.
|
|
439
483
|
|
|
440
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.
|
|
484
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.12.0...HEAD
|
|
485
|
+
[2.12.0]: https://github.com/ishowshao/tinker/releases/tag/v2.12.0
|
|
486
|
+
[2.11.0]: https://github.com/ishowshao/tinker/releases/tag/v2.11.0
|
|
441
487
|
[2.10.0]: https://github.com/ishowshao/tinker/releases/tag/v2.10.0
|
|
442
488
|
[2.9.0]: https://github.com/ishowshao/tinker/releases/tag/v2.9.0
|
|
443
489
|
[2.8.0]: https://github.com/ishowshao/tinker/releases/tag/v2.8.0
|
package/README.md
CHANGED
|
@@ -34,6 +34,19 @@ This does not pretend that any model has infinite tokens or guarantee that it wi
|
|
|
34
34
|
explicit model and context limits. Actual provider support must be established
|
|
35
35
|
by a qualification matrix; transport compatibility alone is not a guarantee.
|
|
36
36
|
|
|
37
|
+
### Provider retry selection
|
|
38
|
+
|
|
39
|
+
When automatic retries for a provider request are exhausted, the TUI offers
|
|
40
|
+
**Retry again** or **End this turn**. Retrying reuses the same request within the
|
|
41
|
+
same turn and iteration, with a fresh automatic retry allowance. Ending the turn
|
|
42
|
+
(or pressing Esc in the selection) records the original failure. The selection
|
|
43
|
+
adds no conversation message and applies only to the current running process.
|
|
44
|
+
Retryable failures include HTTP 429/500/502/503/504, structured `server_error`
|
|
45
|
+
and `rate_limit_exceeded` errors inside provider streams or failed Responses,
|
|
46
|
+
and transient connection failures, including socket resets during streaming.
|
|
47
|
+
Tool failures and non-retryable errors keep their existing behavior. One-shot
|
|
48
|
+
and remote service runs do not wait for this TUI selection.
|
|
49
|
+
|
|
37
50
|
### PTY screen size
|
|
38
51
|
|
|
39
52
|
`Bash` accepts optional `cols` and `rows` for the initial PTY screen size:
|
|
@@ -52,7 +65,7 @@ creation; running tasks cannot be resized through these tools.
|
|
|
52
65
|
|
|
53
66
|
Both Recall tools accept an optional `sessionId` (a canonical session UUID).
|
|
54
67
|
Omitting it keeps the current-session behavior. In the TUI or one-shot CLI, the
|
|
55
|
-
agent can use a session ID you supply or a
|
|
68
|
+
agent can use a session ID you supply or find in a memory record's filename/header:
|
|
56
69
|
|
|
57
70
|
```ts
|
|
58
71
|
RecallSearch({ sessionId: sourceSessionId, query: "distinctive-anchor" })
|
|
@@ -367,70 +380,6 @@ Image-capable profile example:
|
|
|
367
380
|
}
|
|
368
381
|
```
|
|
369
382
|
|
|
370
|
-
The top-level `memory` object is optional. When present, every field below is required. It enables completed-turn extraction and `MemorySearch` only in the TUI; one-shot runs do not load memory.
|
|
371
|
-
|
|
372
|
-
| Field | Required | Type / constraint | Secret | Description |
|
|
373
|
-
| --- | --- | --- | --- | --- |
|
|
374
|
-
| `profile` | Yes | Non-empty string | No | Existing model profile used for completed-turn atomic-memory extraction. |
|
|
375
|
-
| `embedding` | Yes | Object | Yes | Single embedding profile for the global memory database. |
|
|
376
|
-
|
|
377
|
-
`memory.embedding` fields:
|
|
378
|
-
|
|
379
|
-
| Field | Required | Type / constraint | Secret | Description |
|
|
380
|
-
| --- | --- | --- | --- | --- |
|
|
381
|
-
| `name` | Yes | Non-empty string | No | Stable identity for the embedding space. |
|
|
382
|
-
| `kind` | Yes | Literal `"openai-compatible"` | No | Embedding transport kind. |
|
|
383
|
-
| `model` | Yes | Non-empty string | No | Embedding provider model name. |
|
|
384
|
-
| `apiBase` | Yes | Non-empty string | No | OpenAI-compatible API base URL. |
|
|
385
|
-
| `apiKey` | Yes | Non-empty string | Yes | Embedding provider credential. |
|
|
386
|
-
| `dimensions` | Yes | Positive integer | No | Fixed vector dimensions for the global memory database. |
|
|
387
|
-
|
|
388
|
-
Enabling memory sends completed-turn text (not image bytes) to `memory.profile`, and sends extracted candidates plus search queries to the embedding endpoint. Derived memories are stored in `~/.tinker/memory/memory.sqlite`; newly inserted memory text is appended to the private development log `~/.tinker/memory/extracted-memories.log`.
|
|
389
|
-
|
|
390
|
-
Atomic-memory profile example:
|
|
391
|
-
|
|
392
|
-
```json
|
|
393
|
-
{
|
|
394
|
-
"default": "text",
|
|
395
|
-
"profiles": {
|
|
396
|
-
"text": {
|
|
397
|
-
"model": "example-text-model",
|
|
398
|
-
"api": "chat-completions",
|
|
399
|
-
"apiBase": "https://api.example.com/v1",
|
|
400
|
-
"apiKey": "your-model-api-key",
|
|
401
|
-
"contextWindowTokens": 128000,
|
|
402
|
-
"maxSupportedOutputTokens": 8192,
|
|
403
|
-
"reasoning": {
|
|
404
|
-
"supportedEfforts": [
|
|
405
|
-
"low",
|
|
406
|
-
"medium",
|
|
407
|
-
"high"
|
|
408
|
-
],
|
|
409
|
-
"defaultEffort": "medium"
|
|
410
|
-
},
|
|
411
|
-
"includeReasoningContent": false,
|
|
412
|
-
"stream": true,
|
|
413
|
-
"inputModalities": [
|
|
414
|
-
"text"
|
|
415
|
-
],
|
|
416
|
-
"toolResultModalities": [
|
|
417
|
-
"text"
|
|
418
|
-
]
|
|
419
|
-
}
|
|
420
|
-
},
|
|
421
|
-
"memory": {
|
|
422
|
-
"profile": "text",
|
|
423
|
-
"embedding": {
|
|
424
|
-
"name": "example-embedding-space",
|
|
425
|
-
"kind": "openai-compatible",
|
|
426
|
-
"model": "example-embedding-model",
|
|
427
|
-
"apiBase": "https://embeddings.example.com/v1",
|
|
428
|
-
"apiKey": "your-embedding-api-key",
|
|
429
|
-
"dimensions": 1024
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
383
|
<!-- END GENERATED: MODEL PROFILE FIELDS -->
|
|
435
384
|
|
|
436
385
|
Set `api` to `"responses"` to use the standard Responses API. Keep `apiBase`
|
|
@@ -550,6 +499,34 @@ one-shot `tinker run` command. See the
|
|
|
550
499
|
[project command loader](src/tui/project-slash-commands.ts) for configuration
|
|
551
500
|
validation and loading behavior.
|
|
552
501
|
|
|
502
|
+
### Text memory
|
|
503
|
+
|
|
504
|
+
Memory works without a model profile or embedding service in both the TUI and
|
|
505
|
+
one-shot CLI. Session transcripts are appended to
|
|
506
|
+
`<home>/.tinker/memory/records/<sessionId>.md`. `MemoryCreate` saves explicit notes
|
|
507
|
+
to `memory/notes/<id>.md`, using `text` as the heading and optional `summary` as
|
|
508
|
+
the body, with creation time and source workspace.
|
|
509
|
+
|
|
510
|
+
`MemorySearch` searches Markdown under `<home>/.tinker/memory/` using literal,
|
|
511
|
+
case-insensitive keywords. Supply `keywords` (any keyword may match), optionally
|
|
512
|
+
`context` (default 3), `limit` (default 20 matching lines), and `offset` (default 0).
|
|
513
|
+
Each line counts once, even if several keywords match. Keywords cannot span lines.
|
|
514
|
+
Passages are grouped by file with line numbers; `>` marks matching lines and `…`
|
|
515
|
+
separates passages. Long lines retain up to 500 code points around the earliest
|
|
516
|
+
match. Use `Read` with `offset` and `limit` to expand a passage, or `nextOffset` to
|
|
517
|
+
continue searching. There are no regex, glob, output-mode, or multiline parameters.
|
|
518
|
+
Results follow file-path order; changes between calls can affect pagination.
|
|
519
|
+
There are no MemoryGet, MemoryUpdate, or
|
|
520
|
+
MemoryDelete tools. Resume appends to the same record; cloning copies historical
|
|
521
|
+
content; deleting a session leaves its memory record in place. `/memory` browses
|
|
522
|
+
previews of records and notes.
|
|
523
|
+
|
|
524
|
+
Old `models.json` memory configuration is ignored. Existing database and memory
|
|
525
|
+
diagnostic files are moved to `<home>/.tinker/memory-legacy/<id>/` on first use;
|
|
526
|
+
they are preserved but not searched or converted. Older session-local
|
|
527
|
+
`observations.md` files move to records when those sessions are resumed.
|
|
528
|
+
See [the memory design](docs/global-memory/global-memory-design.md).
|
|
529
|
+
|
|
553
530
|
### Global Runtime Data
|
|
554
531
|
|
|
555
532
|
Tinker keeps private runtime state in a global home directory instead of inside
|
|
@@ -557,8 +534,8 @@ your projects. Each workspace maps to
|
|
|
557
534
|
`~/.tinker/projects/<workspace-slug-hash>/` — the slug comes from the workspace
|
|
558
535
|
directory name and the hash from its canonical absolute path, so one project
|
|
559
536
|
always resolves to one storage directory even when reached through symlinks.
|
|
560
|
-
That directory holds per-session SQLite databases
|
|
561
|
-
|
|
537
|
+
That directory holds per-session SQLite databases and event logs
|
|
538
|
+
(`sessions/<id>/`), background Bash task logs (`bash/`), imported image
|
|
562
539
|
assets (`assets/images/`), and prompt history (`prompt-history.jsonl`).
|
|
563
540
|
Workspaces stay clean: no `.tinker/` directory is created inside them.
|
|
564
541
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinker-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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",
|
|
@@ -66,14 +66,15 @@
|
|
|
66
66
|
"docs:check": "bun scripts/render-public-contract-docs.ts --check",
|
|
67
67
|
"tinker": "bun src/cli/index.ts",
|
|
68
68
|
"check": "bun run check:source-lines && bun run typecheck && bun run format:check && bun run lint && bun run docs:check && bun run test && bun run bench:smoke",
|
|
69
|
-
"check:fast": "bun run check:source-lines && bun run typecheck && bun run format:check && bun run lint && bun run test:fast",
|
|
69
|
+
"check:fast": "bun run check:source-lines && bun run typecheck && bun run format:check && bun run lint:fast && bun run test:fast",
|
|
70
70
|
"check:source-lines": "bun scripts/check-source-lines.ts",
|
|
71
71
|
"format": "biome format --write .",
|
|
72
72
|
"format:check": "biome format .",
|
|
73
73
|
"lint": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --max-warnings=0",
|
|
74
|
+
"lint:fast": "bun run lint --cache --cache-strategy content --cache-location .cache/eslint-fast/",
|
|
74
75
|
"lint:fix": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --fix",
|
|
75
76
|
"test": "FORCE_COLOR=0 bun test",
|
|
76
|
-
"test:fast": "
|
|
77
|
+
"test:fast": "bun scripts/run-test-shards.ts --path-ignore-patterns='**/*pty*.test.ts' --path-ignore-patterns='**/native-host-integration.test.ts'",
|
|
77
78
|
"test:e2e": "FORCE_COLOR=0 bun test src/__tests__/cli-pty src/__tests__/pty-tui-harness packages/tinker-chrome/__tests__/native-host-integration",
|
|
78
79
|
"typecheck": "tsc --noEmit"
|
|
79
80
|
},
|
package/src/agent/loop.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ModelRequestFailedData } from "../events/types";
|
|
2
|
+
import type { ProviderRetryDecision } from "./runtime-provider-retry";
|
|
1
3
|
import {
|
|
2
4
|
materializeModelRequest,
|
|
3
5
|
type MaterializedModelRequest,
|
|
@@ -74,6 +76,11 @@ export type RunAgentInput = {
|
|
|
74
76
|
usage: ContextUsageSnapshot;
|
|
75
77
|
};
|
|
76
78
|
transientRetryDelaysMs?: readonly number[];
|
|
79
|
+
requestProviderRetry?: (
|
|
80
|
+
iteration: IterationIdentity,
|
|
81
|
+
failure: ModelRequestFailedData,
|
|
82
|
+
signal: AbortSignal,
|
|
83
|
+
) => Promise<ProviderRetryDecision>;
|
|
77
84
|
};
|
|
78
85
|
|
|
79
86
|
export class FatalAgentTurnError extends Error {
|
|
@@ -196,12 +203,13 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
196
203
|
return failedResult(error, iteration);
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
let maxAttempts = MODEL_REQUEST_MAX_ATTEMPTS;
|
|
199
207
|
await input.runtimeSession.append({
|
|
200
208
|
type: "model.request.started",
|
|
201
209
|
...iteration,
|
|
202
210
|
data: {
|
|
203
211
|
attemptNumber: 1,
|
|
204
|
-
maxAttempts
|
|
212
|
+
maxAttempts,
|
|
205
213
|
},
|
|
206
214
|
});
|
|
207
215
|
|
|
@@ -240,7 +248,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
240
248
|
...iteration,
|
|
241
249
|
data: {
|
|
242
250
|
attemptNumber,
|
|
243
|
-
maxAttempts
|
|
251
|
+
maxAttempts,
|
|
244
252
|
},
|
|
245
253
|
});
|
|
246
254
|
}
|
|
@@ -289,19 +297,47 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
289
297
|
transientRetries,
|
|
290
298
|
transientRetryDelaysMs,
|
|
291
299
|
});
|
|
300
|
+
const failure = modelRequestFailureData({
|
|
301
|
+
error,
|
|
302
|
+
request,
|
|
303
|
+
attemptNumber,
|
|
304
|
+
maxAttempts,
|
|
305
|
+
retryDisposition: decision.disposition,
|
|
306
|
+
...(decision.disposition === "scheduled" && decision.delayMs > 0
|
|
307
|
+
? { retryDelayMs: decision.delayMs }
|
|
308
|
+
: {}),
|
|
309
|
+
});
|
|
292
310
|
await input.runtimeSession.append({
|
|
293
311
|
type: "model.request.failed",
|
|
294
312
|
...iteration,
|
|
295
|
-
data:
|
|
296
|
-
error,
|
|
297
|
-
request,
|
|
298
|
-
attemptNumber,
|
|
299
|
-
retryDisposition: decision.disposition,
|
|
300
|
-
...(decision.disposition === "scheduled" && decision.delayMs > 0
|
|
301
|
-
? { retryDelayMs: decision.delayMs }
|
|
302
|
-
: {}),
|
|
303
|
-
}),
|
|
313
|
+
data: failure,
|
|
304
314
|
});
|
|
315
|
+
if (decision.disposition === "exhausted" && input.requestProviderRetry) {
|
|
316
|
+
let retry: ProviderRetryDecision;
|
|
317
|
+
try {
|
|
318
|
+
retry = await input.requestProviderRetry(iteration, failure, input.signal);
|
|
319
|
+
} catch (retryError) {
|
|
320
|
+
if (input.signal.aborted) {
|
|
321
|
+
return cancelledResult(
|
|
322
|
+
cancellation(input.signal, iteration, "model_request"),
|
|
323
|
+
iteration,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
throw retryError;
|
|
327
|
+
}
|
|
328
|
+
if (input.signal.aborted) {
|
|
329
|
+
return cancelledResult(
|
|
330
|
+
cancellation(input.signal, iteration, "model_request"),
|
|
331
|
+
iteration,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (retry === "retry") {
|
|
335
|
+
reasoningOnlyRetries = 0;
|
|
336
|
+
transientRetries = 0;
|
|
337
|
+
maxAttempts = attemptNumber + MODEL_REQUEST_MAX_ATTEMPTS;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
305
341
|
|
|
306
342
|
if (decision.disposition === "scheduled") {
|
|
307
343
|
if (decision.kind === "reasoning_only") {
|
|
@@ -349,7 +385,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
349
385
|
...iteration,
|
|
350
386
|
data: {
|
|
351
387
|
attemptNumber: successfulAttempt,
|
|
352
|
-
maxAttempts
|
|
388
|
+
maxAttempts,
|
|
353
389
|
output: modelOutput,
|
|
354
390
|
},
|
|
355
391
|
});
|
|
@@ -823,6 +859,7 @@ function modelRequestFailureData(input: {
|
|
|
823
859
|
error: unknown;
|
|
824
860
|
request: PreparedModelRequest;
|
|
825
861
|
attemptNumber: number;
|
|
862
|
+
maxAttempts: number;
|
|
826
863
|
retryDisposition: "scheduled" | "not_retryable" | "exhausted";
|
|
827
864
|
retryDelayMs?: number;
|
|
828
865
|
}) {
|
|
@@ -830,7 +867,7 @@ function modelRequestFailureData(input: {
|
|
|
830
867
|
input.error instanceof ProviderResponseError ? input.error : undefined;
|
|
831
868
|
return {
|
|
832
869
|
attemptNumber: input.attemptNumber,
|
|
833
|
-
maxAttempts:
|
|
870
|
+
maxAttempts: input.maxAttempts,
|
|
834
871
|
code: providerError?.code ?? ("provider_request_error" as const),
|
|
835
872
|
retryDisposition: input.retryDisposition,
|
|
836
873
|
...(input.retryDelayMs === undefined ? {} : { retryDelayMs: input.retryDelayMs }),
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AgentEventInput, ModelRequestFailedData } from "../events/types";
|
|
2
|
+
import type { IterationIdentity } from "./types";
|
|
3
|
+
import { cancellationError } from "./turn-cancellation";
|
|
4
|
+
|
|
5
|
+
export type ProviderRetryDecision = "retry" | "stop";
|
|
6
|
+
export type ProviderRetryRequest = {
|
|
7
|
+
readonly requestId: string;
|
|
8
|
+
readonly failure: ModelRequestFailedData;
|
|
9
|
+
};
|
|
10
|
+
export type ProviderRetrySnapshot = { readonly pending?: ProviderRetryRequest };
|
|
11
|
+
export const EMPTY_PROVIDER_RETRY: ProviderRetrySnapshot = Object.freeze({});
|
|
12
|
+
|
|
13
|
+
type PendingRetry = {
|
|
14
|
+
request: ProviderRetryRequest;
|
|
15
|
+
iteration: IterationIdentity;
|
|
16
|
+
startedAt: number;
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
resolve: (decision: ProviderRetryDecision) => void;
|
|
19
|
+
reject: (error: unknown) => void;
|
|
20
|
+
removeAbortListener: () => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Process-local interaction; the original turn and model request remain open. */
|
|
24
|
+
export class RuntimeProviderRetry {
|
|
25
|
+
private snapshot: ProviderRetrySnapshot = EMPTY_PROVIDER_RETRY;
|
|
26
|
+
private pending?: PendingRetry;
|
|
27
|
+
private readonly listeners = new Set<() => void>();
|
|
28
|
+
|
|
29
|
+
constructor(private readonly append: (event: AgentEventInput) => Promise<void>) {}
|
|
30
|
+
|
|
31
|
+
read(): ProviderRetrySnapshot {
|
|
32
|
+
return this.snapshot;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
subscribe(listener: () => void): () => void {
|
|
36
|
+
this.listeners.add(listener);
|
|
37
|
+
return () => this.listeners.delete(listener);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async request(
|
|
41
|
+
iteration: IterationIdentity,
|
|
42
|
+
failure: ModelRequestFailedData,
|
|
43
|
+
signal: AbortSignal,
|
|
44
|
+
): Promise<ProviderRetryDecision> {
|
|
45
|
+
if (this.pending !== undefined) throw new Error("Provider retry already pending.");
|
|
46
|
+
if (signal.aborted) throw cancellationError(signal);
|
|
47
|
+
await this.append({ type: "model.retry.requested", ...iteration, data: failure });
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const request = Object.freeze({
|
|
50
|
+
requestId: `${iteration.iterationId}:${failure.attemptNumber}`,
|
|
51
|
+
failure: Object.freeze({ ...failure }),
|
|
52
|
+
});
|
|
53
|
+
const pending: PendingRetry = {
|
|
54
|
+
request,
|
|
55
|
+
iteration,
|
|
56
|
+
startedAt: Date.now(),
|
|
57
|
+
signal,
|
|
58
|
+
resolve,
|
|
59
|
+
reject,
|
|
60
|
+
removeAbortListener: () => signal.removeEventListener("abort", onAbort),
|
|
61
|
+
};
|
|
62
|
+
const onAbort = () => {
|
|
63
|
+
// settle rejects the waiting loop too; do not leave a detached rejection.
|
|
64
|
+
void this.settle(pending, "cancelled").catch(() => undefined);
|
|
65
|
+
};
|
|
66
|
+
this.pending = pending;
|
|
67
|
+
this.snapshot = Object.freeze({ pending: request });
|
|
68
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
+
if (signal.aborted) onAbort();
|
|
70
|
+
else this.notify();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async resolve(requestId: string, decision: ProviderRetryDecision): Promise<void> {
|
|
75
|
+
const pending = this.pending;
|
|
76
|
+
if (pending === undefined || pending.request.requestId !== requestId) {
|
|
77
|
+
throw new Error("Provider retry question is no longer pending.");
|
|
78
|
+
}
|
|
79
|
+
if (decision !== "retry" && decision !== "stop") {
|
|
80
|
+
throw new Error("Invalid provider retry decision.");
|
|
81
|
+
}
|
|
82
|
+
await this.settle(pending, decision);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async settle(
|
|
86
|
+
pending: PendingRetry,
|
|
87
|
+
decision: ProviderRetryDecision | "cancelled",
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
if (this.pending !== pending) return;
|
|
90
|
+
this.pending = undefined;
|
|
91
|
+
this.snapshot = EMPTY_PROVIDER_RETRY;
|
|
92
|
+
pending.removeAbortListener();
|
|
93
|
+
this.notify();
|
|
94
|
+
try {
|
|
95
|
+
await this.append({
|
|
96
|
+
type: "model.retry.resolved",
|
|
97
|
+
...pending.iteration,
|
|
98
|
+
data: {
|
|
99
|
+
attemptNumber: pending.request.failure.attemptNumber,
|
|
100
|
+
decision,
|
|
101
|
+
durationMs: Date.now() - pending.startedAt,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
if (decision === "cancelled") pending.reject(cancellationError(pending.signal));
|
|
105
|
+
else pending.resolve(decision);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
pending.reject(error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private notify(): void {
|
|
113
|
+
for (const listener of this.listeners) listener();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProviderRetryDecision,
|
|
3
|
+
ProviderRetrySnapshot,
|
|
4
|
+
} from "./runtime-provider-retry";
|
|
1
5
|
import type { PublicToolingConfig } from "../cli/public-config-contract";
|
|
2
6
|
import type { ContextAutomationPolicy } from "../context/context-automation-policy";
|
|
3
7
|
import type {
|
|
@@ -11,7 +15,7 @@ import type { ContextSurfaceComponent } from "../context/context-surface";
|
|
|
11
15
|
import type { ToolCompletionInput } from "../context/protocol-frame";
|
|
12
16
|
import type { EventSink } from "../events/event-sink";
|
|
13
17
|
import type { AgentEventInput, AgentEventType } from "../events/types";
|
|
14
|
-
import type { RuntimeIdFactory, SessionId
|
|
18
|
+
import type { RuntimeIdFactory, SessionId } from "../ids/runtime-id";
|
|
15
19
|
import type { ImportedImageAsset } from "../image/image-asset-store";
|
|
16
20
|
import type { ImageAssetRef, UserMessage } from "../image/image-types";
|
|
17
21
|
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
@@ -24,11 +28,7 @@ import type {
|
|
|
24
28
|
} from "../model/model-context-profile";
|
|
25
29
|
import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
|
|
26
30
|
import type { ObservationBuilder } from "../observation/observation-builder";
|
|
27
|
-
import type {
|
|
28
|
-
CompletedTurnSnapshot,
|
|
29
|
-
SessionRecoveryResult,
|
|
30
|
-
SessionStore,
|
|
31
|
-
} from "../session/session-store";
|
|
31
|
+
import type { SessionRecoveryResult, SessionStore } from "../session/session-store";
|
|
32
32
|
import type { SkillCatalogSnapshot } from "../skills/skill-loader";
|
|
33
33
|
import type { createDefaultTooling } from "../tools/registry";
|
|
34
34
|
import type { TurnUndoResult } from "../tools/turn-undo-manager";
|
|
@@ -118,6 +118,12 @@ export type RuntimeSession = {
|
|
|
118
118
|
subscribeBashGuard(listener: () => void): () => void;
|
|
119
119
|
setYoloMode(enabled: boolean): void;
|
|
120
120
|
resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
|
|
121
|
+
providerRetry(): ProviderRetrySnapshot;
|
|
122
|
+
subscribeProviderRetry(listener: () => void): () => void;
|
|
123
|
+
resolveProviderRetry(
|
|
124
|
+
requestId: string,
|
|
125
|
+
decision: ProviderRetryDecision,
|
|
126
|
+
): Promise<void>;
|
|
121
127
|
askUser(): AskUserSnapshot;
|
|
122
128
|
subscribeAskUser(listener: () => void): () => void;
|
|
123
129
|
resolveAskUser(response: AskUserResolution): Promise<void>;
|
|
@@ -201,25 +207,6 @@ export type SkillsUpdateSummary = {
|
|
|
201
207
|
readonly addedOverrideCount: number;
|
|
202
208
|
};
|
|
203
209
|
|
|
204
|
-
export type CompletedTurnHookInput = {
|
|
205
|
-
readonly workspaceRoot: string;
|
|
206
|
-
readonly sessionId: SessionId;
|
|
207
|
-
readonly turnId: TurnId;
|
|
208
|
-
readonly snapshot: CompletedTurnSnapshot;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
export type CompletedTurnHookFailure = {
|
|
212
|
-
readonly workspaceRoot: string;
|
|
213
|
-
readonly sessionId: SessionId;
|
|
214
|
-
readonly turnId: TurnId;
|
|
215
|
-
readonly reason: "completed_turn_snapshot_failed" | "completed_turn_enqueue_failed";
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
export type CompletedTurnHook = {
|
|
219
|
-
enqueue(input: CompletedTurnHookInput): void;
|
|
220
|
-
recordFailure(input: CompletedTurnHookFailure): void;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
210
|
export type CommonRuntimeSessionInput = {
|
|
224
211
|
workspaceRoot: string;
|
|
225
212
|
homeRoot?: string;
|
|
@@ -244,13 +231,10 @@ export type CommonRuntimeSessionInput = {
|
|
|
244
231
|
webFetchRefiner?: Refiner;
|
|
245
232
|
toolingConfig?: PublicToolingConfig;
|
|
246
233
|
memorySearch?: ToolExecutor;
|
|
247
|
-
memoryGet?: ToolExecutor;
|
|
248
234
|
memoryCreate?: ToolExecutor;
|
|
249
|
-
memoryUpdate?: ToolExecutor;
|
|
250
|
-
memoryDelete?: ToolExecutor;
|
|
251
|
-
completedTurnHook?: CompletedTurnHook;
|
|
252
235
|
enableTurnUndo?: boolean;
|
|
253
236
|
enableAskUser?: boolean;
|
|
237
|
+
enableProviderRetryPrompt?: boolean;
|
|
254
238
|
bashGuard?: {
|
|
255
239
|
readonly mode: "guard" | "yolo";
|
|
256
240
|
readonly source: Exclude<BashGuardSource, "session">;
|