tinker-agent 2.0.0 → 2.2.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 +27 -2
- package/package.json +2 -1
- package/src/agent/context-meter.ts +2 -4
- package/src/agent/runtime-session.ts +9 -2
- 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 -0
- package/src/cli/model-profiles.ts +41 -2
- package/src/cli/public-config-contract.ts +30 -7
- package/src/cli/runner-dependencies.ts +5 -0
- package/src/cli/tui-memory.ts +1 -0
- 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/memory/contracts.ts +63 -3
- package/src/memory/memory-coordinator.ts +319 -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 +129 -76
- package/src/model/model-client.ts +62 -11
- package/src/model/openai-chat-mapping.ts +2 -1
- package/src/model/openai-chat-model-client.ts +22 -10
- package/src/model/openai-model-utils.ts +61 -30
- package/src/model/openai-responses-mapping.ts +25 -1
- package/src/model/openai-responses-model-client.ts +27 -11
- package/src/model/token-estimator.ts +10 -0
- 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 +343 -196
- 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/components/prompt-input.tsx +48 -25
- package/src/tui/event-store.ts +61 -2
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.2.0] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Log raw vector cosine scores in `MemorySearch` diagnostics, making hybrid
|
|
13
|
+
recall ranking easier to inspect and tune.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Restyle the prompt-input footer status line with a muted color palette:
|
|
18
|
+
model name, workspace path, Git branch, and cache hit rate each get a soft
|
|
19
|
+
accent color, and normal-pressure context usage now renders in a cool gray
|
|
20
|
+
instead of plain dim. Red and yellow remain reserved for context-pressure
|
|
21
|
+
warnings.
|
|
22
|
+
|
|
23
|
+
## [2.1.0] - 2026-08-29
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Add cross-session derived memory. Completed turns are distilled into memory
|
|
28
|
+
records that the agent can recall in later sessions and workspaces through
|
|
29
|
+
`MemorySearch`, which fuses vector similarity with FTS5 keyword matching via
|
|
30
|
+
RRF hybrid ranking, and `MemoryGet`, which reads a full record by its id.
|
|
31
|
+
- Add a `toolResultModalities` model-profile setting. Image-capable profiles can
|
|
32
|
+
now receive `ViewImage` results as real image content in the model request
|
|
33
|
+
instead of text-only descriptions.
|
|
34
|
+
- Add a `Wait` tool that pauses the agent loop for a cancellable whole number of
|
|
35
|
+
seconds (1 to 3600), useful for spacing polling attempts.
|
|
36
|
+
- Expand a leading `~` to the user's home directory in `TINKER_MODELS` and
|
|
37
|
+
`TINKER_WORKSPACE` paths.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Show `MemorySearch` keywords alongside the query in TUI tool summaries, so
|
|
42
|
+
hybrid recall behavior is visible while observing the agent.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Accept `memory_get` raw results in stored tool history, so sessions that
|
|
47
|
+
contain MemoryGet calls remain resumable.
|
|
48
|
+
|
|
8
49
|
## [2.0.0] - 2026-08-20
|
|
9
50
|
|
|
10
51
|
### Changed
|
|
@@ -223,7 +264,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
223
264
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
224
265
|
executable.
|
|
225
266
|
|
|
226
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.
|
|
267
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v2.2.0...HEAD
|
|
268
|
+
[2.2.0]: https://github.com/ishowshao/tinker/releases/tag/v2.2.0
|
|
269
|
+
[2.1.0]: https://github.com/ishowshao/tinker/releases/tag/v2.1.0
|
|
227
270
|
[2.0.0]: https://github.com/ishowshao/tinker/releases/tag/v2.0.0
|
|
228
271
|
[1.11.0]: https://github.com/ishowshao/tinker/releases/tag/v1.11.0
|
|
229
272
|
[1.10.1]: https://github.com/ishowshao/tinker/releases/tag/v1.10.1
|
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,6 +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
|
+
| `toolResultModalities` | No | Normalized modality array | `["text"]` | No | Accepted tool-result modalities; normalizes to ["text"] or ["text", "image"]. |
|
|
197
198
|
|
|
198
199
|
`reasoning` fields:
|
|
199
200
|
|
|
@@ -212,6 +213,7 @@ Text-only profile example:
|
|
|
212
213
|
"profiles": {
|
|
213
214
|
"text": {
|
|
214
215
|
"model": "example-text-model",
|
|
216
|
+
"api": "chat-completions",
|
|
215
217
|
"apiBase": "https://api.example.com/v1",
|
|
216
218
|
"apiKey": "your-model-api-key",
|
|
217
219
|
"contextWindowTokens": 128000,
|
|
@@ -228,6 +230,9 @@ Text-only profile example:
|
|
|
228
230
|
"stream": true,
|
|
229
231
|
"inputModalities": [
|
|
230
232
|
"text"
|
|
233
|
+
],
|
|
234
|
+
"toolResultModalities": [
|
|
235
|
+
"text"
|
|
231
236
|
]
|
|
232
237
|
}
|
|
233
238
|
}
|
|
@@ -242,6 +247,7 @@ Image-capable profile example:
|
|
|
242
247
|
"profiles": {
|
|
243
248
|
"image": {
|
|
244
249
|
"model": "example-vision-model",
|
|
250
|
+
"api": "responses",
|
|
245
251
|
"apiBase": "https://api.example.com/v1",
|
|
246
252
|
"apiKey": "your-model-api-key",
|
|
247
253
|
"contextWindowTokens": 128000,
|
|
@@ -259,6 +265,10 @@ Image-capable profile example:
|
|
|
259
265
|
"inputModalities": [
|
|
260
266
|
"text",
|
|
261
267
|
"image"
|
|
268
|
+
],
|
|
269
|
+
"toolResultModalities": [
|
|
270
|
+
"text",
|
|
271
|
+
"image"
|
|
262
272
|
]
|
|
263
273
|
}
|
|
264
274
|
}
|
|
@@ -293,6 +303,7 @@ Atomic-memory profile example:
|
|
|
293
303
|
"profiles": {
|
|
294
304
|
"text": {
|
|
295
305
|
"model": "example-text-model",
|
|
306
|
+
"api": "chat-completions",
|
|
296
307
|
"apiBase": "https://api.example.com/v1",
|
|
297
308
|
"apiKey": "your-model-api-key",
|
|
298
309
|
"contextWindowTokens": 128000,
|
|
@@ -309,6 +320,9 @@ Atomic-memory profile example:
|
|
|
309
320
|
"stream": true,
|
|
310
321
|
"inputModalities": [
|
|
311
322
|
"text"
|
|
323
|
+
],
|
|
324
|
+
"toolResultModalities": [
|
|
325
|
+
"text"
|
|
312
326
|
]
|
|
313
327
|
}
|
|
314
328
|
},
|
|
@@ -366,6 +380,17 @@ independent token-estimator request. See the
|
|
|
366
380
|
[`multimodal image input design`](docs/multimodal-image-input-design.md) for the
|
|
367
381
|
complete fixed policy and persistence contract.
|
|
368
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
|
+
|
|
369
394
|
### Built-in Slash Commands
|
|
370
395
|
|
|
371
396
|
<!-- BEGIN GENERATED: BUILT-IN SLASH COMMANDS -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinker-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { lastPromptPrefixHash, promptPrefixHashes } from "../model/prompt-prefix-hash";
|
|
14
14
|
import {
|
|
15
15
|
estimatePromptSegments,
|
|
16
|
+
guardedContextTokens,
|
|
16
17
|
RollingTokenCalibration,
|
|
17
18
|
type RawContextBreakdown,
|
|
18
19
|
} from "../model/token-estimator";
|
|
@@ -296,10 +297,7 @@ function guardedEstimate(
|
|
|
296
297
|
breakdown: RawContextBreakdown,
|
|
297
298
|
correctionFactor: number,
|
|
298
299
|
): number {
|
|
299
|
-
return (
|
|
300
|
-
Math.ceil(breakdown.textAndProtocolTokens * correctionFactor) +
|
|
301
|
-
breakdown.imageTokens
|
|
302
|
-
);
|
|
300
|
+
return guardedContextTokens(breakdown, correctionFactor);
|
|
303
301
|
}
|
|
304
302
|
|
|
305
303
|
function assertMeasuredContextAnchor(anchor: MeasuredContextAnchor): void {
|
|
@@ -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,7 +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
|
+
inputModalities: input.modelClient.inputModalities,
|
|
585
|
+
toolResultModalities: input.modelClient.toolResultModalities,
|
|
584
586
|
});
|
|
585
587
|
if (input.selection.mode === "resume") {
|
|
586
588
|
store.assertSessionCompatibility(compatibility);
|
|
@@ -654,6 +656,10 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
654
656
|
workspaceRoot: input.workspaceRoot,
|
|
655
657
|
runtimeSession: session.context,
|
|
656
658
|
historyReader: store.historyReader(),
|
|
659
|
+
imageAssetStore: assetStore,
|
|
660
|
+
supportsViewImage:
|
|
661
|
+
input.modelClient.inputModalities.includes("image") &&
|
|
662
|
+
input.modelClient.toolResultModalities.includes("image"),
|
|
657
663
|
...(input.enableTurnUndo === true ? { enableTurnUndo: true } : {}),
|
|
658
664
|
webFetchRefiner: input.webFetchRefiner,
|
|
659
665
|
toolingConfig: input.toolingConfig,
|
|
@@ -665,6 +671,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
665
671
|
...(input.memorySearch === undefined
|
|
666
672
|
? {}
|
|
667
673
|
: { memorySearch: input.memorySearch }),
|
|
674
|
+
...(input.memoryGet === undefined ? {} : { memoryGet: input.memoryGet }),
|
|
668
675
|
...(session.skillCatalog.skills.size === 0
|
|
669
676
|
? {}
|
|
670
677
|
: {
|
|
@@ -1397,7 +1404,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1397
1404
|
messageId: message.messageId,
|
|
1398
1405
|
frameId: message.frameId,
|
|
1399
1406
|
ordinal: message.ordinal,
|
|
1400
|
-
content: message.
|
|
1407
|
+
content: message.displayText,
|
|
1401
1408
|
contentSha256: message.contentSha256,
|
|
1402
1409
|
},
|
|
1403
1410
|
name: activation.name,
|
|
@@ -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 =
|
package/src/cli/config.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
persistDefaultProfile,
|
|
13
13
|
profileToContextProfile,
|
|
14
14
|
type ModelInputModality,
|
|
15
|
+
type ToolResultModality,
|
|
15
16
|
type ModelProfile,
|
|
16
17
|
type ModelProfiles,
|
|
17
18
|
unknownProfileError,
|
|
@@ -39,6 +40,7 @@ export type RunnerConfig = {
|
|
|
39
40
|
readonly contextBudget: ModelContextBudget;
|
|
40
41
|
readonly profileName?: string;
|
|
41
42
|
readonly inputModalities: readonly ModelInputModality[];
|
|
43
|
+
readonly toolResultModalities: readonly ToolResultModality[];
|
|
42
44
|
readonly bashGuardMode: "guard" | "yolo";
|
|
43
45
|
readonly bashGuardSource: "default" | "environment" | "cli";
|
|
44
46
|
};
|
|
@@ -180,6 +182,7 @@ function runnerConfigTemplateFromProfile(
|
|
|
180
182
|
contextBudget: deriveModelContextBudget(contextProfile),
|
|
181
183
|
profileName: profile.name,
|
|
182
184
|
inputModalities: profile.inputModalities,
|
|
185
|
+
toolResultModalities: profile.toolResultModalities,
|
|
183
186
|
bashGuardMode: environment.bashGuardMode,
|
|
184
187
|
bashGuardSource: environment.bashGuardSource,
|
|
185
188
|
});
|
|
@@ -205,6 +208,7 @@ function runnerConfigTemplateFromEnvironment(
|
|
|
205
208
|
contextProfile,
|
|
206
209
|
contextBudget: deriveModelContextBudget(contextProfile),
|
|
207
210
|
inputModalities: Object.freeze(["text"] as const),
|
|
211
|
+
toolResultModalities: Object.freeze(["text"] as const),
|
|
208
212
|
bashGuardMode: environment.bashGuardMode,
|
|
209
213
|
bashGuardSource: environment.bashGuardSource,
|
|
210
214
|
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./public-config-contract";
|
|
14
14
|
import type { MemoryEmbeddingConfig } from "../memory/contracts";
|
|
15
15
|
import type { ReasoningEffortConfig } from "../model/reasoning-effort";
|
|
16
|
+
import type { ModelInputModality, ToolResultModality } from "../model/model-client";
|
|
16
17
|
|
|
17
18
|
export type ModelProfile = {
|
|
18
19
|
readonly name: string;
|
|
@@ -26,9 +27,9 @@ export type ModelProfile = {
|
|
|
26
27
|
readonly includeReasoningContent: boolean;
|
|
27
28
|
readonly stream: boolean;
|
|
28
29
|
readonly inputModalities: readonly ModelInputModality[];
|
|
30
|
+
readonly toolResultModalities: readonly ToolResultModality[];
|
|
29
31
|
};
|
|
30
|
-
|
|
31
|
-
export type ModelInputModality = "text" | "image";
|
|
32
|
+
export type { ModelInputModality, ToolResultModality } from "../model/model-client";
|
|
32
33
|
|
|
33
34
|
export type ModelProfiles = {
|
|
34
35
|
readonly defaultProfile: string;
|
|
@@ -272,6 +273,15 @@ function parseProfile(
|
|
|
272
273
|
value.inputModalities,
|
|
273
274
|
`${where}: "inputModalities"`,
|
|
274
275
|
);
|
|
276
|
+
const toolResultModalities = parseToolResultModalities(
|
|
277
|
+
value.toolResultModalities,
|
|
278
|
+
`${where}: "toolResultModalities"`,
|
|
279
|
+
);
|
|
280
|
+
if (toolResultModalities.includes("image") && !inputModalities.includes("image")) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`${where}: "toolResultModalities" cannot include "image" unless "inputModalities" also includes "image".`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
275
285
|
createModelContextProfile({
|
|
276
286
|
contextWindowTokens,
|
|
277
287
|
maxSupportedOutputTokens,
|
|
@@ -289,9 +299,38 @@ function parseProfile(
|
|
|
289
299
|
includeReasoningContent,
|
|
290
300
|
stream,
|
|
291
301
|
inputModalities,
|
|
302
|
+
toolResultModalities,
|
|
292
303
|
});
|
|
293
304
|
}
|
|
294
305
|
|
|
306
|
+
function parseToolResultModalities(
|
|
307
|
+
value: unknown,
|
|
308
|
+
name: string,
|
|
309
|
+
): readonly ToolResultModality[] {
|
|
310
|
+
if (value === undefined) {
|
|
311
|
+
return modelProfileField("toolResultModalities").defaultValue;
|
|
312
|
+
}
|
|
313
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
314
|
+
throw new Error(`${name} must be a non-empty array.`);
|
|
315
|
+
}
|
|
316
|
+
const modalities = value.map((rawEntry): ToolResultModality => {
|
|
317
|
+
const entry: unknown = rawEntry;
|
|
318
|
+
if (entry !== "text" && entry !== "image") {
|
|
319
|
+
throw new Error(`${name} contains an unsupported modality.`);
|
|
320
|
+
}
|
|
321
|
+
return entry;
|
|
322
|
+
});
|
|
323
|
+
if (new Set(modalities).size !== modalities.length) {
|
|
324
|
+
throw new Error(`${name} must not contain duplicates.`);
|
|
325
|
+
}
|
|
326
|
+
if (!modalities.includes("text")) {
|
|
327
|
+
throw new Error(`${name} must include "text".`);
|
|
328
|
+
}
|
|
329
|
+
return Object.freeze(
|
|
330
|
+
modalities.includes("image") ? (["text", "image"] as const) : (["text"] as const),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
295
334
|
function parseReasoning(value: unknown, where: string): ReasoningEffortConfig {
|
|
296
335
|
if (!isRecord(value)) {
|
|
297
336
|
throw new Error(`${where} must be an object.`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "node:os";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { rgPath } from "@vscode/ripgrep";
|
|
3
4
|
import { parseModelApi, type ModelApi } from "../model/model-api";
|
|
@@ -29,7 +30,7 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
29
30
|
secret: false,
|
|
30
31
|
section: "model",
|
|
31
32
|
description:
|
|
32
|
-
"Optional model profiles JSON path.
|
|
33
|
+
"Optional model profiles JSON path. A leading ~ expands to the home directory; other relative paths resolve from the process cwd.",
|
|
33
34
|
}),
|
|
34
35
|
publicField({
|
|
35
36
|
name: "TINKER_MODEL",
|
|
@@ -126,7 +127,8 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
126
127
|
defaultSource: "process-cwd",
|
|
127
128
|
secret: false,
|
|
128
129
|
section: "workspace",
|
|
129
|
-
description:
|
|
130
|
+
description:
|
|
131
|
+
"Workspace path. A leading ~ expands to the home directory; other relative paths resolve from the process cwd.",
|
|
130
132
|
}),
|
|
131
133
|
publicField({
|
|
132
134
|
name: "TINKER_MAX_ITERATIONS",
|
|
@@ -242,7 +244,11 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
242
244
|
|
|
243
245
|
export type ModelProfileField = {
|
|
244
246
|
readonly name: string;
|
|
245
|
-
readonly valueKind:
|
|
247
|
+
readonly valueKind:
|
|
248
|
+
| PublicConfigValueKind
|
|
249
|
+
| "input-modalities"
|
|
250
|
+
| "tool-result-modalities"
|
|
251
|
+
| "reasoning";
|
|
246
252
|
readonly required: boolean;
|
|
247
253
|
readonly defaultValue?: string | number | boolean | readonly string[];
|
|
248
254
|
readonly secret: boolean;
|
|
@@ -333,6 +339,15 @@ export const MODEL_PROFILE_FIELDS = Object.freeze([
|
|
|
333
339
|
description:
|
|
334
340
|
'Accepted model input modalities; normalizes to ["text"] or ["text", "image"].',
|
|
335
341
|
}),
|
|
342
|
+
profileField({
|
|
343
|
+
name: "toolResultModalities",
|
|
344
|
+
valueKind: "tool-result-modalities",
|
|
345
|
+
required: false,
|
|
346
|
+
defaultValue: Object.freeze(["text"] as const),
|
|
347
|
+
secret: false,
|
|
348
|
+
description:
|
|
349
|
+
'Accepted tool-result modalities; normalizes to ["text"] or ["text", "image"].',
|
|
350
|
+
}),
|
|
336
351
|
]);
|
|
337
352
|
|
|
338
353
|
export type ModelReasoningField = {
|
|
@@ -523,7 +538,7 @@ export function parsePublicEnvironment(
|
|
|
523
538
|
values[field.name] = parseEnvironmentField(field, env[field.name], mode, cwd);
|
|
524
539
|
}
|
|
525
540
|
|
|
526
|
-
const workspaceRoot =
|
|
541
|
+
const workspaceRoot = resolveUserPath(
|
|
527
542
|
cwd,
|
|
528
543
|
requiredStringValue(values, "TINKER_WORKSPACE"),
|
|
529
544
|
);
|
|
@@ -572,9 +587,7 @@ export function parsePublicEnvironment(
|
|
|
572
587
|
return Object.freeze({
|
|
573
588
|
...common,
|
|
574
589
|
mode,
|
|
575
|
-
modelsPath:
|
|
576
|
-
? configuredModelsPath
|
|
577
|
-
: path.resolve(cwd, configuredModelsPath),
|
|
590
|
+
modelsPath: resolveUserPath(cwd, configuredModelsPath),
|
|
578
591
|
});
|
|
579
592
|
}
|
|
580
593
|
|
|
@@ -673,6 +686,16 @@ function optionalRawString(value: string | undefined): string | undefined {
|
|
|
673
686
|
return normalized === undefined || normalized === "" ? undefined : normalized;
|
|
674
687
|
}
|
|
675
688
|
|
|
689
|
+
function resolveUserPath(cwd: string, value: string): string {
|
|
690
|
+
let expanded = value;
|
|
691
|
+
if (value === "~") {
|
|
692
|
+
expanded = os.homedir();
|
|
693
|
+
} else if (value.startsWith("~/") || value.startsWith(`~${path.sep}`)) {
|
|
694
|
+
expanded = path.join(os.homedir(), value.slice(2));
|
|
695
|
+
}
|
|
696
|
+
return path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
|
|
697
|
+
}
|
|
698
|
+
|
|
676
699
|
function optionalStringValue(
|
|
677
700
|
values: Record<PublicConfigFieldName, ParsedPrimitive>,
|
|
678
701
|
name: PublicConfigFieldName,
|
|
@@ -66,6 +66,8 @@ export function createModelClient(
|
|
|
66
66
|
| "apiKey"
|
|
67
67
|
| "apiBase"
|
|
68
68
|
| "inputModalities"
|
|
69
|
+
| "toolResultModalities"
|
|
70
|
+
| "profileName"
|
|
69
71
|
>,
|
|
70
72
|
env: NodeJS.ProcessEnv = process.env,
|
|
71
73
|
reasoningEffort?: ReasoningEffortController,
|
|
@@ -78,6 +80,7 @@ export function createModelClient(
|
|
|
78
80
|
model: config.modelName,
|
|
79
81
|
contextBudget: config.contextBudget,
|
|
80
82
|
inputModalities: config.inputModalities,
|
|
83
|
+
toolResultModalities: config.toolResultModalities,
|
|
81
84
|
...(activeReasoningEffort === undefined
|
|
82
85
|
? {}
|
|
83
86
|
: { reasoningEffort: activeReasoningEffort }),
|
|
@@ -95,6 +98,8 @@ export function createModelClient(
|
|
|
95
98
|
stream: config.stream,
|
|
96
99
|
contextBudget: config.contextBudget,
|
|
97
100
|
inputModalities: config.inputModalities,
|
|
101
|
+
toolResultModalities: config.toolResultModalities,
|
|
102
|
+
profileName: config.profileName,
|
|
98
103
|
...(activeReasoningEffort === undefined
|
|
99
104
|
? {}
|
|
100
105
|
: { reasoningEffort: activeReasoningEffort }),
|
package/src/cli/tui-memory.ts
CHANGED
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -129,6 +129,10 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
129
129
|
workspaceRoot,
|
|
130
130
|
sessionId,
|
|
131
131
|
}),
|
|
132
|
+
memoryGet: memoryCoordinator.createGetToolExecutor({
|
|
133
|
+
workspaceRoot,
|
|
134
|
+
sessionId,
|
|
135
|
+
}),
|
|
132
136
|
completedTurnHook: memoryCoordinator,
|
|
133
137
|
}),
|
|
134
138
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
2
|
+
import { toolResultText } from "../agent/tool-result-content";
|
|
2
3
|
import type { CompiledContextEntry, SwapOverride } from "./context-revision";
|
|
3
4
|
import {
|
|
4
5
|
contentHash,
|
|
@@ -132,7 +133,7 @@ function renderedMessageDescriptor(entry: CompiledContextEntry): unknown {
|
|
|
132
133
|
contentSha256:
|
|
133
134
|
entry.representation === "canonical"
|
|
134
135
|
? entry.sourceContentSha256
|
|
135
|
-
: contentHash(message.content),
|
|
136
|
+
: contentHash(toolResultText(message.content)),
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
139
|
}
|