tinker-agent 1.11.0 → 2.1.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 +44 -1
- package/README.md +33 -24
- package/package.json +2 -1
- package/src/agent/context-meter.ts +12 -85
- package/src/agent/loop.ts +1 -14
- package/src/agent/runtime-session.ts +9 -25
- package/src/agent/session-ledger.ts +12 -5
- package/src/agent/tool-result-content.ts +76 -0
- package/src/agent/types.ts +14 -2
- package/src/cli/config.ts +4 -5
- package/src/cli/model-profiles.ts +38 -97
- package/src/cli/public-config-contract.ts +24 -88
- package/src/cli/runner-dependencies.ts +5 -7
- package/src/cli/tui-memory.ts +1 -3
- package/src/cli/tui-runner.tsx +4 -0
- package/src/context/compiled-context-hash.ts +2 -1
- package/src/context/compiled-context-validator.ts +13 -4
- package/src/context/context-protocol-validator.ts +33 -2
- package/src/context/context-revision-compiler.ts +2 -1
- package/src/context/context-revision.ts +8 -2
- package/src/context/context-swap-renderer.ts +46 -12
- package/src/context/prefix-retirement-planner.ts +13 -9
- package/src/context/protocol-frame.ts +74 -7
- package/src/context/swap-planner.ts +19 -14
- package/src/events/observation-text-log.ts +1 -1
- package/src/events/stdout-event-printer.ts +6 -0
- package/src/image/image-asset-store.ts +32 -3
- package/src/image/image-input-policy.ts +53 -2
- package/src/image/image-probe.ts +8 -2
- package/src/image/provider-image.ts +99 -0
- package/src/memory/contracts.ts +61 -3
- package/src/memory/memory-coordinator.ts +313 -49
- package/src/memory/memory-extractor.ts +48 -48
- package/src/memory/memory-get-tool.ts +86 -0
- package/src/memory/memory-search-tool.ts +122 -33
- package/src/memory/memory-store.ts +227 -20
- package/src/model/fake-model-client.ts +177 -124
- package/src/model/model-client.ts +62 -11
- package/src/model/model-request-preflight.ts +0 -1
- package/src/model/openai-chat-mapping.ts +2 -1
- package/src/model/openai-chat-model-client.ts +26 -38
- package/src/model/openai-model-utils.ts +109 -40
- package/src/model/openai-responses-mapping.ts +25 -1
- package/src/model/openai-responses-model-client.ts +27 -40
- package/src/model/token-estimator.ts +26 -3
- package/src/observation/observation-builder.ts +100 -25
- package/src/session/session-history-reader.ts +128 -5
- package/src/session/session-schema.ts +59 -9
- package/src/session/session-store.ts +342 -205
- package/src/tools/registry.ts +18 -0
- package/src/tools/types.ts +46 -0
- package/src/tools/view-image.ts +89 -0
- package/src/tools/wait.ts +85 -0
- package/src/tui/components/memory-browser.tsx +3 -0
- package/src/tui/event-store.ts +61 -2
- package/src/model/input-token-estimator.ts +0 -25
- package/src/model/moonshot-input-token-estimator.ts +0 -111
- package/src/model/openai-responses-token-estimator.ts +0 -155
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add cross-session derived memory. Completed turns are distilled into memory
|
|
13
|
+
records that the agent can recall in later sessions and workspaces through
|
|
14
|
+
`MemorySearch`, which fuses vector similarity with FTS5 keyword matching via
|
|
15
|
+
RRF hybrid ranking, and `MemoryGet`, which reads a full record by its id.
|
|
16
|
+
- Add a `toolResultModalities` model-profile setting. Image-capable profiles can
|
|
17
|
+
now receive `ViewImage` results as real image content in the model request
|
|
18
|
+
instead of text-only descriptions.
|
|
19
|
+
- Add a `Wait` tool that pauses the agent loop for a cancellable whole number of
|
|
20
|
+
seconds (1 to 3600), useful for spacing polling attempts.
|
|
21
|
+
- Expand a leading `~` to the user's home directory in `TINKER_MODELS` and
|
|
22
|
+
`TINKER_WORKSPACE` paths.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Show `MemorySearch` keywords alongside the query in TUI tool summaries, so
|
|
27
|
+
hybrid recall behavior is visible while observing the agent.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Accept `memory_get` raw results in stored tool history, so sessions that
|
|
32
|
+
contain MemoryGet calls remain resumable.
|
|
33
|
+
|
|
34
|
+
## [2.0.0] - 2026-08-20
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Estimate image input locally with deterministic size buckets instead of a
|
|
39
|
+
provider-side token-estimation request. Images are normalized for orientation
|
|
40
|
+
and provider limits before estimation and upload, keeping preflight accounting
|
|
41
|
+
aligned with the payload sent to the model.
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- Remove the `tokenEstimator` model-profile setting. Existing image profiles must
|
|
46
|
+
delete that field; sessions created under the previous image policy remain
|
|
47
|
+
inspectable but cannot be resumed for execution.
|
|
48
|
+
|
|
8
49
|
## [1.11.0] - 2026-08-15
|
|
9
50
|
|
|
10
51
|
### Added
|
|
@@ -208,7 +249,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
208
249
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
209
250
|
executable.
|
|
210
251
|
|
|
211
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/
|
|
252
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.1.0...HEAD
|
|
253
|
+
[2.1.0]: https://github.com/ishowshao/tinker/releases/tag/v2.1.0
|
|
254
|
+
[2.0.0]: https://github.com/ishowshao/tinker/releases/tag/v2.0.0
|
|
212
255
|
[1.11.0]: https://github.com/ishowshao/tinker/releases/tag/v1.11.0
|
|
213
256
|
[1.10.1]: https://github.com/ishowshao/tinker/releases/tag/v1.10.1
|
|
214
257
|
[1.9.0]: https://github.com/ishowshao/tinker/releases/tag/v1.9.0
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ are required. Boolean environment values accept case-insensitive `true/false`,
|
|
|
138
138
|
<!-- BEGIN GENERATED: PUBLIC ENVIRONMENT VARIABLES -->
|
|
139
139
|
| Variable | Area | Applies | Required | Type | Default | Secret | Description |
|
|
140
140
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
141
|
-
| `TINKER_MODELS` | Model | All modes | No | Non-empty string | — | No | Optional model profiles JSON path.
|
|
141
|
+
| `TINKER_MODELS` | Model | All modes | No | Non-empty string | — | No | Optional model profiles JSON path. A leading ~ expands to the home directory; other relative paths resolve from the process cwd. |
|
|
142
142
|
| `TINKER_MODEL` | Model | Env mode | Env mode | Non-empty string | — | No | Model name used when model profiles are not configured. |
|
|
143
143
|
| `TINKER_API` | Model | Env mode | No | Non-empty string | `"chat-completions"` | No | Model API adapter: "chat-completions" or "responses". |
|
|
144
144
|
| `TINKER_BASE_URL` | Model | Env mode | Env mode | Non-empty string | — | No | OpenAI-compatible API root URL; do not append /chat/completions or /responses. |
|
|
@@ -148,7 +148,7 @@ are required. Boolean environment values accept case-insensitive `true/false`,
|
|
|
148
148
|
| `TINKER_INCLUDE_REASONING_CONTENT` | Model | Env mode | No | Boolean | `false` | No | Replay provider reasoning_content in Chat Completions history; ignored by Responses. |
|
|
149
149
|
| `TINKER_STREAM` | Model | Env mode | No | Boolean | `true` | No | Use streaming transport for the selected model API. |
|
|
150
150
|
| `TINKER_WEBFETCH_REFINE_MODEL` | Model | Env mode | No | Non-empty string | — | No | Optional WebFetch refiner model; currently must match TINKER_MODEL. |
|
|
151
|
-
| `TINKER_WORKSPACE` | Workspace | All modes | No | Non-empty string | Process cwd | No | Workspace path.
|
|
151
|
+
| `TINKER_WORKSPACE` | Workspace | All modes | No | Non-empty string | Process cwd | No | Workspace path. A leading ~ expands to the home directory; other relative paths resolve from the process cwd. |
|
|
152
152
|
| `TINKER_MAX_ITERATIONS` | Workspace | All modes | No | Positive integer | `512` | No | Maximum agent-loop iterations per turn. |
|
|
153
153
|
| `EXA_API_KEY` | Tooling | All modes | No | Non-empty string | — | Yes | Enables WebSearch and the Exa WebFetch backend when set. |
|
|
154
154
|
| `TINKER_MCP_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `60000` | No | MCP tool-call timeout in milliseconds. |
|
|
@@ -194,7 +194,7 @@ Profile fields:
|
|
|
194
194
|
| `includeReasoningContent` | No | JSON boolean | `false` | No | Replay provider reasoning_content in Chat Completions history; ignored by Responses. |
|
|
195
195
|
| `stream` | No | JSON boolean | `true` | No | Use streaming transport for the selected model API. |
|
|
196
196
|
| `inputModalities` | No | Normalized modality array | `["text"]` | No | Accepted model input modalities; normalizes to ["text"] or ["text", "image"]. |
|
|
197
|
-
| `
|
|
197
|
+
| `toolResultModalities` | No | Normalized modality array | `["text"]` | No | Accepted tool-result modalities; normalizes to ["text"] or ["text", "image"]. |
|
|
198
198
|
|
|
199
199
|
`reasoning` fields:
|
|
200
200
|
|
|
@@ -205,17 +205,6 @@ Profile fields:
|
|
|
205
205
|
|
|
206
206
|
The optional `reasoning` object declares provider-specific effort values. Efforts must be unique non-whitespace strings, `reset` is reserved by the TUI command, and `defaultEffort` must appear in `supportedEfforts`. Omitting `reasoning` sends no effort parameter and disables `/reasoning` for that profile.
|
|
207
207
|
|
|
208
|
-
`tokenEstimator` fields:
|
|
209
|
-
|
|
210
|
-
| Field | Type / constraint | Secret | Description |
|
|
211
|
-
| --- | --- | --- | --- |
|
|
212
|
-
| `kind` | Literal `"moonshot-estimate-token-count-v1"` | No | Estimator protocol discriminator. |
|
|
213
|
-
| `model` | Non-empty string | No | Estimator model name. |
|
|
214
|
-
| `apiBase` | Non-empty string | No | Estimator API base URL. |
|
|
215
|
-
| `apiKey` | Non-empty string | Yes | Estimator API credential. |
|
|
216
|
-
| `timeoutMs` | Integer 1000–60000 | No | Estimator request timeout in milliseconds. |
|
|
217
|
-
| `maxRetries` | Literal `0` | No | Estimator retry count; retries are disabled. |
|
|
218
|
-
|
|
219
208
|
Text-only profile example:
|
|
220
209
|
|
|
221
210
|
```json
|
|
@@ -224,6 +213,7 @@ Text-only profile example:
|
|
|
224
213
|
"profiles": {
|
|
225
214
|
"text": {
|
|
226
215
|
"model": "example-text-model",
|
|
216
|
+
"api": "chat-completions",
|
|
227
217
|
"apiBase": "https://api.example.com/v1",
|
|
228
218
|
"apiKey": "your-model-api-key",
|
|
229
219
|
"contextWindowTokens": 128000,
|
|
@@ -240,6 +230,9 @@ Text-only profile example:
|
|
|
240
230
|
"stream": true,
|
|
241
231
|
"inputModalities": [
|
|
242
232
|
"text"
|
|
233
|
+
],
|
|
234
|
+
"toolResultModalities": [
|
|
235
|
+
"text"
|
|
243
236
|
]
|
|
244
237
|
}
|
|
245
238
|
}
|
|
@@ -254,6 +247,7 @@ Image-capable profile example:
|
|
|
254
247
|
"profiles": {
|
|
255
248
|
"image": {
|
|
256
249
|
"model": "example-vision-model",
|
|
250
|
+
"api": "responses",
|
|
257
251
|
"apiBase": "https://api.example.com/v1",
|
|
258
252
|
"apiKey": "your-model-api-key",
|
|
259
253
|
"contextWindowTokens": 128000,
|
|
@@ -272,14 +266,10 @@ Image-capable profile example:
|
|
|
272
266
|
"text",
|
|
273
267
|
"image"
|
|
274
268
|
],
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
"apiKey": "your-estimator-api-key",
|
|
280
|
-
"timeoutMs": 30000,
|
|
281
|
-
"maxRetries": 0
|
|
282
|
-
}
|
|
269
|
+
"toolResultModalities": [
|
|
270
|
+
"text",
|
|
271
|
+
"image"
|
|
272
|
+
]
|
|
283
273
|
}
|
|
284
274
|
}
|
|
285
275
|
}
|
|
@@ -313,6 +303,7 @@ Atomic-memory profile example:
|
|
|
313
303
|
"profiles": {
|
|
314
304
|
"text": {
|
|
315
305
|
"model": "example-text-model",
|
|
306
|
+
"api": "chat-completions",
|
|
316
307
|
"apiBase": "https://api.example.com/v1",
|
|
317
308
|
"apiKey": "your-model-api-key",
|
|
318
309
|
"contextWindowTokens": 128000,
|
|
@@ -329,6 +320,9 @@ Atomic-memory profile example:
|
|
|
329
320
|
"stream": true,
|
|
330
321
|
"inputModalities": [
|
|
331
322
|
"text"
|
|
323
|
+
],
|
|
324
|
+
"toolResultModalities": [
|
|
325
|
+
"text"
|
|
332
326
|
]
|
|
333
327
|
}
|
|
334
328
|
},
|
|
@@ -370,7 +364,7 @@ configured profile.
|
|
|
370
364
|
### Image Input
|
|
371
365
|
|
|
372
366
|
Image attachment is enabled only for a profile whose `inputModalities` explicitly
|
|
373
|
-
includes `image
|
|
367
|
+
includes `image`. In the interactive
|
|
374
368
|
TUI, type `@` and select a file that is inside the workspace and visible to the
|
|
375
369
|
workspace search rules. One-shot commands, clipboard image bytes, remote URLs, and
|
|
376
370
|
files outside or ignored by the workspace search are not supported.
|
|
@@ -378,10 +372,25 @@ files outside or ignored by the workspace search are not supported.
|
|
|
378
372
|
Tinker accepts PNG (not APNG), JPEG, and static WebP. It rejects GIF, animated
|
|
379
373
|
WebP, and other formats. A message and provider request may contain at most eight
|
|
380
374
|
images; each image may be at most 20 MiB, 4096 pixels on either edge, and 8,847,360
|
|
381
|
-
pixels in total.
|
|
375
|
+
pixels in total. Provider requests preserve smaller images and proportionally
|
|
376
|
+
downscale larger images to a maximum 2048-pixel long edge. Context planning uses
|
|
377
|
+
fixed local token buckets derived from the materialized dimensions and performs no
|
|
378
|
+
independent token-estimator request. See the
|
|
379
|
+
[`image token bucket design`](docs/image-token-bucket-estimation-design.md) and
|
|
382
380
|
[`multimodal image input design`](docs/multimodal-image-input-design.md) for the
|
|
383
381
|
complete fixed policy and persistence contract.
|
|
384
382
|
|
|
383
|
+
`ViewImage(file_path)` is registered only when the selected profile declares both
|
|
384
|
+
`inputModalities: ["text", "image"]` and
|
|
385
|
+
`toolResultModalities: ["text", "image"]`. The first implementation supports
|
|
386
|
+
image tool results through the Responses adapter; Chat Completions remains
|
|
387
|
+
text-only for tool results. Relative paths stay inside the workspace, absolute
|
|
388
|
+
paths may explicitly select an external local file, and symbolic links are
|
|
389
|
+
rejected. Canonical history stores content-addressed image references rather than
|
|
390
|
+
Base64, while stdout, TUI, Recall, and logs show deterministic text summaries.
|
|
391
|
+
See the [`ViewImage tool design`](docs/view-image-tool-design.md) for the complete
|
|
392
|
+
capability, persistence, provider, and compaction contract.
|
|
393
|
+
|
|
385
394
|
### Built-in Slash Commands
|
|
386
395
|
|
|
387
396
|
<!-- BEGIN GENERATED: BUILT-IN SLASH COMMANDS -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinker-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.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",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"bench:long-session": "bun scripts/bench-long-session-memory.ts",
|
|
51
51
|
"bench:i3-provider-smoke": "bun scripts/bench-i3-provider-smoke.ts",
|
|
52
52
|
"bench:k3-image-live": "bun scripts/k3-image-live-smoke.ts",
|
|
53
|
+
"bench:view-image-live": "bun scripts/view-image-live-smoke.ts",
|
|
53
54
|
"bench:i4-active-recall": "bun scripts/bench-i4-active-recall.ts",
|
|
54
55
|
"qualify:i4-active-recall": "bun scripts/qualify-i4-active-recall.ts",
|
|
55
56
|
"release:verify": "bun scripts/verify-release-package.ts",
|
|
@@ -2,13 +2,8 @@ import type { ModelContextBudget } from "../model/model-context-profile";
|
|
|
2
2
|
import type {
|
|
3
3
|
ModelRequestOutput,
|
|
4
4
|
ModelUsage,
|
|
5
|
-
MaterializedModelRequest,
|
|
6
5
|
PreparedModelRequest,
|
|
7
6
|
} from "../model/model-client";
|
|
8
|
-
import type {
|
|
9
|
-
InputTokenEstimate,
|
|
10
|
-
InputTokenEstimator,
|
|
11
|
-
} from "../model/input-token-estimator";
|
|
12
7
|
import {
|
|
13
8
|
assertContextBudget,
|
|
14
9
|
contextPressure,
|
|
@@ -18,6 +13,7 @@ import {
|
|
|
18
13
|
import { lastPromptPrefixHash, promptPrefixHashes } from "../model/prompt-prefix-hash";
|
|
19
14
|
import {
|
|
20
15
|
estimatePromptSegments,
|
|
16
|
+
guardedContextTokens,
|
|
21
17
|
RollingTokenCalibration,
|
|
22
18
|
type RawContextBreakdown,
|
|
23
19
|
} from "../model/token-estimator";
|
|
@@ -68,7 +64,6 @@ export class ContextMeter {
|
|
|
68
64
|
private anchor?: MeasuredContextAnchor;
|
|
69
65
|
private lastProviderUsage?: ModelUsage;
|
|
70
66
|
private calibrationIdentity?: string;
|
|
71
|
-
private readonly providerEstimateCache = new Map<string, InputTokenEstimate>();
|
|
72
67
|
|
|
73
68
|
constructor(
|
|
74
69
|
private readonly budget: ModelContextBudget,
|
|
@@ -129,13 +124,14 @@ export class ContextMeter {
|
|
|
129
124
|
let guardedDeltaTokens: number | undefined;
|
|
130
125
|
if (anchor === undefined) {
|
|
131
126
|
source = "estimated_full";
|
|
132
|
-
usedInputTokens =
|
|
127
|
+
usedInputTokens = guardedEstimate(rawFullEstimate, correctionFactor);
|
|
133
128
|
} else {
|
|
134
129
|
source = "measured_plus_estimated_delta";
|
|
135
|
-
|
|
130
|
+
const rawDelta = estimatePromptSegments(
|
|
136
131
|
prepared.promptSegments.slice(anchor.segmentCount),
|
|
137
|
-
)
|
|
138
|
-
|
|
132
|
+
);
|
|
133
|
+
rawDeltaTokens = rawDelta.totalTokens;
|
|
134
|
+
guardedDeltaTokens = guardedEstimate(rawDelta, correctionFactor);
|
|
139
135
|
usedInputTokens = anchor.totalTokens + guardedDeltaTokens;
|
|
140
136
|
}
|
|
141
137
|
|
|
@@ -217,62 +213,6 @@ export class ContextMeter {
|
|
|
217
213
|
};
|
|
218
214
|
}
|
|
219
215
|
|
|
220
|
-
applyProviderEstimate(
|
|
221
|
-
prepared: PreparedModelRequest,
|
|
222
|
-
estimate: {
|
|
223
|
-
inputTokens: number;
|
|
224
|
-
coverage: "messages" | "full_request";
|
|
225
|
-
},
|
|
226
|
-
): ContextUsageSnapshot {
|
|
227
|
-
if (!Number.isSafeInteger(estimate.inputTokens) || estimate.inputTokens < 0) {
|
|
228
|
-
throw new Error("Provider input estimate must be a non-negative safe integer.");
|
|
229
|
-
}
|
|
230
|
-
const local = this.measure(prepared);
|
|
231
|
-
const measurement = this.measurements.get(prepared)!;
|
|
232
|
-
const guardedTools =
|
|
233
|
-
estimate.coverage === "messages"
|
|
234
|
-
? Math.ceil(
|
|
235
|
-
measurement.rawFullEstimate.toolSchemaTokens * local.correctionFactor,
|
|
236
|
-
)
|
|
237
|
-
: 0;
|
|
238
|
-
const providerGuarded = estimate.inputTokens + guardedTools;
|
|
239
|
-
if (providerGuarded <= local.usedInputTokens) {
|
|
240
|
-
return local;
|
|
241
|
-
}
|
|
242
|
-
const snapshot: ContextUsageSnapshot = {
|
|
243
|
-
...local,
|
|
244
|
-
usedInputTokens: providerGuarded,
|
|
245
|
-
source: "provider_estimated",
|
|
246
|
-
pressure: contextPressure(providerGuarded, this.budget),
|
|
247
|
-
};
|
|
248
|
-
this.measurements.set(prepared, {
|
|
249
|
-
rawFullEstimate: measurement.rawFullEstimate,
|
|
250
|
-
snapshot,
|
|
251
|
-
});
|
|
252
|
-
return snapshot;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async estimateProviderInput(
|
|
256
|
-
prepared: MaterializedModelRequest,
|
|
257
|
-
estimator: InputTokenEstimator,
|
|
258
|
-
options: { signal: AbortSignal },
|
|
259
|
-
): Promise<InputTokenEstimate> {
|
|
260
|
-
options.signal.throwIfAborted();
|
|
261
|
-
const key = providerEstimateCacheKey(
|
|
262
|
-
prepared,
|
|
263
|
-
estimator.compatibility.coverageVersion,
|
|
264
|
-
);
|
|
265
|
-
const cached = this.providerEstimateCache.get(key);
|
|
266
|
-
if (cached !== undefined) {
|
|
267
|
-
return cached;
|
|
268
|
-
}
|
|
269
|
-
const estimate = await estimator.estimate(prepared, options);
|
|
270
|
-
options.signal.throwIfAborted();
|
|
271
|
-
const frozen = Object.freeze({ ...estimate });
|
|
272
|
-
this.providerEstimateCache.set(key, frozen);
|
|
273
|
-
return frozen;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
216
|
assertWithinBudget(snapshot: ContextUsageSnapshot): void {
|
|
277
217
|
assertContextBudget({
|
|
278
218
|
usedInputTokens: snapshot.usedInputTokens,
|
|
@@ -295,7 +235,6 @@ export class ContextMeter {
|
|
|
295
235
|
this.lastProviderUsage = undefined;
|
|
296
236
|
this.measurements = new WeakMap();
|
|
297
237
|
this.calibration.clear();
|
|
298
|
-
this.providerEstimateCache.clear();
|
|
299
238
|
this.calibrationIdentity = nextIdentity;
|
|
300
239
|
throw new Error(
|
|
301
240
|
"Context revision changed the request configuration or tool schema.",
|
|
@@ -304,7 +243,6 @@ export class ContextMeter {
|
|
|
304
243
|
this.anchor = undefined;
|
|
305
244
|
this.lastProviderUsage = undefined;
|
|
306
245
|
this.measurements = new WeakMap();
|
|
307
|
-
this.providerEstimateCache.clear();
|
|
308
246
|
this.calibrationIdentity = nextIdentity;
|
|
309
247
|
}
|
|
310
248
|
|
|
@@ -315,7 +253,6 @@ export class ContextMeter {
|
|
|
315
253
|
this.measurements = new WeakMap();
|
|
316
254
|
this.calibration.clear();
|
|
317
255
|
this.calibrationIdentity = undefined;
|
|
318
|
-
this.providerEstimateCache.clear();
|
|
319
256
|
}
|
|
320
257
|
|
|
321
258
|
private usableAnchor(
|
|
@@ -330,10 +267,7 @@ export class ContextMeter {
|
|
|
330
267
|
anchor.requestConfigHash !== prepared.requestConfigHash ||
|
|
331
268
|
anchor.toolSchemaHash !== prepared.toolSchemaHash ||
|
|
332
269
|
anchor.segmentCount > prepared.promptSegments.length ||
|
|
333
|
-
prefixHashes[anchor.segmentCount] !== anchor.prefixHash
|
|
334
|
-
prepared.promptSegments
|
|
335
|
-
.slice(anchor.segmentCount)
|
|
336
|
-
.some((segment) => (segment.media?.length ?? 0) > 0)
|
|
270
|
+
prefixHashes[anchor.segmentCount] !== anchor.prefixHash
|
|
337
271
|
) {
|
|
338
272
|
this.anchor = undefined;
|
|
339
273
|
return undefined;
|
|
@@ -359,18 +293,11 @@ export class ContextMeter {
|
|
|
359
293
|
}
|
|
360
294
|
}
|
|
361
295
|
|
|
362
|
-
function
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
):
|
|
366
|
-
return
|
|
367
|
-
prepared.requestConfigHash,
|
|
368
|
-
prepared.toolSchemaHash,
|
|
369
|
-
lastPromptPrefixHash(
|
|
370
|
-
promptPrefixHashes(prepared.requestConfigHash, prepared.promptSegments),
|
|
371
|
-
),
|
|
372
|
-
coverageVersion,
|
|
373
|
-
].join(":");
|
|
296
|
+
function guardedEstimate(
|
|
297
|
+
breakdown: RawContextBreakdown,
|
|
298
|
+
correctionFactor: number,
|
|
299
|
+
): number {
|
|
300
|
+
return guardedContextTokens(breakdown, correctionFactor);
|
|
374
301
|
}
|
|
375
302
|
|
|
376
303
|
function assertMeasuredContextAnchor(anchor: MeasuredContextAnchor): void {
|
package/src/agent/loop.ts
CHANGED
|
@@ -175,20 +175,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
|
|
|
175
175
|
assetStore: input.assetStore,
|
|
176
176
|
signal: input.signal,
|
|
177
177
|
});
|
|
178
|
-
|
|
179
|
-
preflight = input.contextMeter.measure(request);
|
|
180
|
-
} else {
|
|
181
|
-
const estimator = input.model.inputTokenEstimator;
|
|
182
|
-
if (estimator === undefined) {
|
|
183
|
-
throw new Error("Image model request has no input token estimator.");
|
|
184
|
-
}
|
|
185
|
-
const estimate = await input.contextMeter.estimateProviderInput(
|
|
186
|
-
request as MaterializedModelRequest,
|
|
187
|
-
estimator,
|
|
188
|
-
{ signal: input.signal },
|
|
189
|
-
);
|
|
190
|
-
preflight = input.contextMeter.applyProviderEstimate(request, estimate);
|
|
191
|
-
}
|
|
178
|
+
preflight = input.contextMeter.measure(request);
|
|
192
179
|
input.contextMeter.assertWithinBudget(preflight);
|
|
193
180
|
await input.runtimeSession.append({
|
|
194
181
|
type: "context.usage.updated",
|
|
@@ -308,6 +308,7 @@ type CommonRuntimeSessionInput = {
|
|
|
308
308
|
webFetchRefiner?: Refiner;
|
|
309
309
|
toolingConfig?: PublicToolingConfig;
|
|
310
310
|
memorySearch?: ToolExecutor;
|
|
311
|
+
memoryGet?: ToolExecutor;
|
|
311
312
|
completedTurnHook?: CompletedTurnHook;
|
|
312
313
|
enableTurnUndo?: boolean;
|
|
313
314
|
bashGuard?: {
|
|
@@ -580,12 +581,8 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
580
581
|
includeReasoningContent: input.includeReasoningContent,
|
|
581
582
|
contextProfile: input.contextProfile,
|
|
582
583
|
messageProtocol: input.modelClient.messageProtocol,
|
|
583
|
-
inputModalities: input.modelClient.inputModalities
|
|
584
|
-
|
|
585
|
-
? {}
|
|
586
|
-
: {
|
|
587
|
-
tokenEstimator: input.modelClient.inputTokenEstimator.compatibility,
|
|
588
|
-
}),
|
|
584
|
+
inputModalities: input.modelClient.inputModalities,
|
|
585
|
+
toolResultModalities: input.modelClient.toolResultModalities,
|
|
589
586
|
});
|
|
590
587
|
if (input.selection.mode === "resume") {
|
|
591
588
|
store.assertSessionCompatibility(compatibility);
|
|
@@ -659,6 +656,10 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
659
656
|
workspaceRoot: input.workspaceRoot,
|
|
660
657
|
runtimeSession: session.context,
|
|
661
658
|
historyReader: store.historyReader(),
|
|
659
|
+
imageAssetStore: assetStore,
|
|
660
|
+
supportsViewImage:
|
|
661
|
+
input.modelClient.inputModalities.includes("image") &&
|
|
662
|
+
input.modelClient.toolResultModalities.includes("image"),
|
|
662
663
|
...(input.enableTurnUndo === true ? { enableTurnUndo: true } : {}),
|
|
663
664
|
webFetchRefiner: input.webFetchRefiner,
|
|
664
665
|
toolingConfig: input.toolingConfig,
|
|
@@ -670,6 +671,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
670
671
|
...(input.memorySearch === undefined
|
|
671
672
|
? {}
|
|
672
673
|
: { memorySearch: input.memorySearch }),
|
|
674
|
+
...(input.memoryGet === undefined ? {} : { memoryGet: input.memoryGet }),
|
|
673
675
|
...(session.skillCatalog.skills.size === 0
|
|
674
676
|
? {}
|
|
675
677
|
: {
|
|
@@ -1402,7 +1404,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1402
1404
|
messageId: message.messageId,
|
|
1403
1405
|
frameId: message.frameId,
|
|
1404
1406
|
ordinal: message.ordinal,
|
|
1405
|
-
content: message.
|
|
1407
|
+
content: message.displayText,
|
|
1406
1408
|
contentSha256: message.contentSha256,
|
|
1407
1409
|
},
|
|
1408
1410
|
name: activation.name,
|
|
@@ -1641,24 +1643,6 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1641
1643
|
{ assetStore: this.assetStore, signal: controller.signal },
|
|
1642
1644
|
);
|
|
1643
1645
|
admissionSnapshot = this.contextMeter.measure(admissionPrepared);
|
|
1644
|
-
if (
|
|
1645
|
-
prepared.mediaOccurrenceCount > 0 &&
|
|
1646
|
-
admissionSnapshot.source !== "measured_plus_estimated_delta"
|
|
1647
|
-
) {
|
|
1648
|
-
const estimator = this.input.modelClient.inputTokenEstimator;
|
|
1649
|
-
if (estimator === undefined) {
|
|
1650
|
-
throw new Error("Image model request has no input token estimator.");
|
|
1651
|
-
}
|
|
1652
|
-
const estimate = await this.contextMeter.estimateProviderInput(
|
|
1653
|
-
admissionPrepared,
|
|
1654
|
-
estimator,
|
|
1655
|
-
{ signal: controller.signal },
|
|
1656
|
-
);
|
|
1657
|
-
admissionSnapshot = this.contextMeter.applyProviderEstimate(
|
|
1658
|
-
admissionPrepared,
|
|
1659
|
-
estimate,
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
1646
|
this.contextMeter.assertWithinBudget(admissionSnapshot);
|
|
1663
1647
|
controller.signal.throwIfAborted();
|
|
1664
1648
|
const turn = this.stageTurn(input.userMessage);
|
|
@@ -26,12 +26,15 @@ import {
|
|
|
26
26
|
} from "../context/context-protocol-validator";
|
|
27
27
|
import {
|
|
28
28
|
CURRENT_TOOL_OBSERVATION_FORMAT,
|
|
29
|
+
canonicalToolResultContentHash,
|
|
29
30
|
contentHash,
|
|
31
|
+
displayTextForCompletion,
|
|
30
32
|
immutableCanonicalClone,
|
|
31
33
|
immutableRecord,
|
|
32
34
|
observationForCompletion,
|
|
33
35
|
rawResultHash,
|
|
34
36
|
userMessageHash,
|
|
37
|
+
validateReturnedToolObservation,
|
|
35
38
|
type CanonicalMessageRecord,
|
|
36
39
|
type ProtocolContextView,
|
|
37
40
|
type ProtocolFrame,
|
|
@@ -583,6 +586,7 @@ export class InMemorySessionLedger implements SessionLedger {
|
|
|
583
586
|
assertSameToolCall(expectedCall, completionInput.call);
|
|
584
587
|
validateCompletionInput(completionInput);
|
|
585
588
|
const content = observationForCompletion(completionInput);
|
|
589
|
+
const displayText = displayTextForCompletion(completionInput);
|
|
586
590
|
const createdAt = this.clock();
|
|
587
591
|
const messageId = this.input.idFactory.createMessageId();
|
|
588
592
|
const message = immutableRecord<CanonicalMessageRecord>({
|
|
@@ -590,7 +594,7 @@ export class InMemorySessionLedger implements SessionLedger {
|
|
|
590
594
|
sessionId: this.input.sessionId,
|
|
591
595
|
frameId: frameBefore.frameId,
|
|
592
596
|
ordinal: this.view.messages.length + messages.length + 1,
|
|
593
|
-
contentSha256:
|
|
597
|
+
contentSha256: canonicalToolResultContentHash(content),
|
|
594
598
|
createdAt,
|
|
595
599
|
role: "tool",
|
|
596
600
|
turnId: pending.turn.turnId,
|
|
@@ -599,6 +603,7 @@ export class InMemorySessionLedger implements SessionLedger {
|
|
|
599
603
|
providerToolCallId: expectedCall.providerToolCallId,
|
|
600
604
|
name: expectedCall.name,
|
|
601
605
|
content,
|
|
606
|
+
displayText,
|
|
602
607
|
origin: completionInput.kind === "returned" ? "tool" : "runtime",
|
|
603
608
|
});
|
|
604
609
|
const completion = canonicalCompletion(completionInput);
|
|
@@ -608,7 +613,7 @@ export class InMemorySessionLedger implements SessionLedger {
|
|
|
608
613
|
toolCallId: expectedCall.toolCallId,
|
|
609
614
|
toolMessageId: messageId,
|
|
610
615
|
completion,
|
|
611
|
-
observationSha256:
|
|
616
|
+
observationSha256: canonicalToolResultContentHash(content),
|
|
612
617
|
createdAt,
|
|
613
618
|
});
|
|
614
619
|
messages.push(message);
|
|
@@ -1006,9 +1011,11 @@ function canonicalCompletion(input: ToolCompletionInput): ToolCompletion {
|
|
|
1006
1011
|
|
|
1007
1012
|
function validateCompletionInput(input: ToolCompletionInput): void {
|
|
1008
1013
|
if (input.kind === "returned") {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1014
|
+
validateReturnedToolObservation({
|
|
1015
|
+
toolName: input.call.name,
|
|
1016
|
+
raw: input.raw,
|
|
1017
|
+
content: input.observation,
|
|
1018
|
+
});
|
|
1012
1019
|
immutableCanonicalClone(input.raw);
|
|
1013
1020
|
return;
|
|
1014
1021
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
2
|
+
import { validateImageAssetRef } from "../image/image-types";
|
|
3
|
+
import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
|
|
4
|
+
import type { ToolResultContent } from "./types";
|
|
5
|
+
|
|
6
|
+
export function canonicalToolResultContentHash(
|
|
7
|
+
content: readonly ToolResultContent[],
|
|
8
|
+
): string {
|
|
9
|
+
validateToolResultContent(content);
|
|
10
|
+
return sha256(stableJsonStringify(content));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateToolResultContent(content: readonly ToolResultContent[]): void {
|
|
14
|
+
if (content.length === 0) {
|
|
15
|
+
throw new Error("Tool result content must be a non-empty array.");
|
|
16
|
+
}
|
|
17
|
+
let previousWasText = false;
|
|
18
|
+
let imageCount = 0;
|
|
19
|
+
for (const block of content) {
|
|
20
|
+
switch (block.type) {
|
|
21
|
+
case "text":
|
|
22
|
+
if (block.text.trim() === "") {
|
|
23
|
+
throw new Error("Tool result text blocks must not be empty.");
|
|
24
|
+
}
|
|
25
|
+
if (previousWasText) {
|
|
26
|
+
throw new Error("Consecutive tool result text blocks must be merged.");
|
|
27
|
+
}
|
|
28
|
+
previousWasText = true;
|
|
29
|
+
break;
|
|
30
|
+
case "image":
|
|
31
|
+
validateImageAssetRef(block.asset);
|
|
32
|
+
imageCount += 1;
|
|
33
|
+
previousWasText = false;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (imageCount > IMAGE_INPUT_POLICY.maxImagesPerMessage) {
|
|
38
|
+
throw new Error("Tool result content exceeds the per-message image limit.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function textToolResultContent(text: string): readonly ToolResultContent[] {
|
|
43
|
+
const content = Object.freeze([Object.freeze({ type: "text" as const, text })]);
|
|
44
|
+
validateToolResultContent(content);
|
|
45
|
+
return content;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function toolResultDisplayText(content: readonly ToolResultContent[]): string {
|
|
49
|
+
validateToolResultContent(content);
|
|
50
|
+
return content
|
|
51
|
+
.map((block) =>
|
|
52
|
+
block.type === "text"
|
|
53
|
+
? block.text
|
|
54
|
+
: `[Image: ${block.asset.mimeType}, ${block.asset.width}x${block.asset.height}, ${block.asset.byteLength} bytes, asset=${shortAssetId(block.asset.assetId)}]`,
|
|
55
|
+
)
|
|
56
|
+
.join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function toolResultText(content: readonly ToolResultContent[]): string {
|
|
60
|
+
validateToolResultContent(content);
|
|
61
|
+
if (content.some((block) => block.type === "image")) {
|
|
62
|
+
throw new Error("Text-only tool result mapping received an image block.");
|
|
63
|
+
}
|
|
64
|
+
return content
|
|
65
|
+
.map((block) => {
|
|
66
|
+
if (block.type !== "text") {
|
|
67
|
+
throw new Error("Text-only tool result mapping received an image block.");
|
|
68
|
+
}
|
|
69
|
+
return block.text;
|
|
70
|
+
})
|
|
71
|
+
.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shortAssetId(assetId: string): string {
|
|
75
|
+
return `${assetId.slice(0, 12)}…`;
|
|
76
|
+
}
|
package/src/agent/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IterationId, SessionId, ToolCallId, TurnId } from "../ids/runtime-id";
|
|
2
|
-
import type { UserMessage } from "../image/image-types";
|
|
2
|
+
import type { ImageAssetRef, UserMessage } from "../image/image-types";
|
|
3
3
|
|
|
4
4
|
export type {
|
|
5
5
|
CodePointRange,
|
|
@@ -45,12 +45,24 @@ export type AssistantMessage = {
|
|
|
45
45
|
toolCalls?: readonly ToolCall[];
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export type ToolResultTextContent = {
|
|
49
|
+
readonly type: "text";
|
|
50
|
+
readonly text: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ToolResultImageContent = {
|
|
54
|
+
readonly type: "image";
|
|
55
|
+
readonly asset: ImageAssetRef;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ToolResultContent = ToolResultTextContent | ToolResultImageContent;
|
|
59
|
+
|
|
48
60
|
export type ToolMessage = {
|
|
49
61
|
role: "tool";
|
|
50
62
|
toolCallId: ToolCallId;
|
|
51
63
|
providerToolCallId: string;
|
|
52
64
|
name: string;
|
|
53
|
-
content:
|
|
65
|
+
content: readonly ToolResultContent[];
|
|
54
66
|
};
|
|
55
67
|
|
|
56
68
|
export type AgentMessage =
|