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
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { appendFile } from "node:fs/promises";
|
|
3
3
|
import type { AgentMessage, AssistantMessage } from "../agent/types";
|
|
4
|
+
import { toolResultDisplayText } from "../agent/tool-result-content";
|
|
4
5
|
import { cancellationError } from "../agent/turn-cancellation";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
IMAGE_INPUT_POLICY,
|
|
8
|
+
imagePlanningTokens,
|
|
9
|
+
providerImageDimensions,
|
|
10
|
+
} from "../image/image-input-policy";
|
|
6
11
|
import type { ImageAssetId, ImageAssetRef } from "../image/image-types";
|
|
7
|
-
import
|
|
12
|
+
import { materializeProviderImage } from "../image/provider-image";
|
|
8
13
|
import type { ModelContextBudget } from "./model-context-profile";
|
|
9
14
|
import type { ReasoningEffortController } from "./reasoning-effort";
|
|
10
15
|
import type {
|
|
@@ -15,16 +20,17 @@ import type {
|
|
|
15
20
|
ModelRequestInput,
|
|
16
21
|
ModelRequestOptions,
|
|
17
22
|
ModelRequestOutput,
|
|
18
|
-
|
|
23
|
+
PreparedMediaOccurrence,
|
|
19
24
|
PreparedModelRequest,
|
|
20
25
|
PreparedPromptSegment,
|
|
21
26
|
} from "./model-client";
|
|
27
|
+
import { validateModelModalities } from "./model-client";
|
|
22
28
|
import { sha256, stableJsonStringify } from "./model-request-preflight";
|
|
23
29
|
import { estimatePromptSegments } from "./token-estimator";
|
|
24
30
|
|
|
25
31
|
export class FakeModelClient implements ModelClient {
|
|
26
32
|
readonly inputModalities: readonly ("text" | "image")[];
|
|
27
|
-
readonly
|
|
33
|
+
readonly toolResultModalities: readonly ("text" | "image")[];
|
|
28
34
|
readonly reasoningEffort?: ReasoningEffortController;
|
|
29
35
|
readonly messageProtocol: ModelMessageProtocol = Object.freeze({
|
|
30
36
|
adapter: "fake",
|
|
@@ -40,56 +46,20 @@ export class FakeModelClient implements ModelClient {
|
|
|
40
46
|
model: string;
|
|
41
47
|
contextBudget: ModelContextBudget;
|
|
42
48
|
inputModalities?: readonly ("text" | "image")[];
|
|
49
|
+
toolResultModalities?: readonly ("text" | "image")[];
|
|
43
50
|
reasoningEffort?: ReasoningEffortController;
|
|
44
51
|
requestLogPath?: string;
|
|
45
|
-
tokenEstimator?: {
|
|
46
|
-
kind: "moonshot-estimate-token-count-v1";
|
|
47
|
-
model: string;
|
|
48
|
-
apiBase: string;
|
|
49
|
-
timeoutMs: number;
|
|
50
|
-
maxRetries: 0;
|
|
51
|
-
};
|
|
52
52
|
},
|
|
53
53
|
) {
|
|
54
54
|
this.reasoningEffort = options.reasoningEffort;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
options.tokenEstimator === undefined
|
|
64
|
-
) {
|
|
65
|
-
throw new Error("Image-capable fake model requires a token estimator.");
|
|
66
|
-
}
|
|
67
|
-
if (options.tokenEstimator !== undefined) {
|
|
68
|
-
const estimator = options.tokenEstimator;
|
|
69
|
-
const endpoint = tokenEstimatorEndpoint(estimator.apiBase);
|
|
70
|
-
this.inputTokenEstimator = Object.freeze({
|
|
71
|
-
kind: estimator.kind,
|
|
72
|
-
compatibility: Object.freeze({
|
|
73
|
-
kind: estimator.kind,
|
|
74
|
-
coverageVersion: "full-request-v1",
|
|
75
|
-
model: estimator.model,
|
|
76
|
-
endpoint,
|
|
77
|
-
timeoutMs: estimator.timeoutMs,
|
|
78
|
-
maxRetries: estimator.maxRetries,
|
|
79
|
-
}),
|
|
80
|
-
async estimate(
|
|
81
|
-
request: MaterializedModelRequest,
|
|
82
|
-
estimateOptions: { signal: AbortSignal },
|
|
83
|
-
) {
|
|
84
|
-
estimateOptions.signal.throwIfAborted();
|
|
85
|
-
return Object.freeze({
|
|
86
|
-
inputTokens: estimatePromptSegments(request.promptSegments).totalTokens,
|
|
87
|
-
source: "provider_estimated" as const,
|
|
88
|
-
coverage: "full_request" as const,
|
|
89
|
-
});
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
}
|
|
55
|
+
const modalities = validateModelModalities({
|
|
56
|
+
adapter: this.messageProtocol.adapter,
|
|
57
|
+
inputModalities: options.inputModalities ?? ["text"],
|
|
58
|
+
toolResultModalities: options.toolResultModalities ?? ["text"],
|
|
59
|
+
adapterToolResultModalities: ["text", "image"],
|
|
60
|
+
});
|
|
61
|
+
this.inputModalities = modalities.inputModalities;
|
|
62
|
+
this.toolResultModalities = modalities.toolResultModalities;
|
|
93
63
|
}
|
|
94
64
|
|
|
95
65
|
prepare(input: ModelRequestInput): PreparedModelRequest {
|
|
@@ -99,7 +69,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
99
69
|
normalizedText: stableJsonStringify(tool),
|
|
100
70
|
}),
|
|
101
71
|
);
|
|
102
|
-
const messageSegments = input.messages.map(
|
|
72
|
+
const messageSegments = input.messages.map((message, index) =>
|
|
73
|
+
toPromptSegment(message, index + 1),
|
|
74
|
+
);
|
|
103
75
|
const mediaOccurrenceCount = messageSegments.reduce(
|
|
104
76
|
(total, segment) => total + (segment.media?.length ?? 0),
|
|
105
77
|
0,
|
|
@@ -113,9 +85,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
113
85
|
model: this.options.model,
|
|
114
86
|
requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
115
87
|
inputModalities: this.inputModalities,
|
|
116
|
-
|
|
117
|
-
? {}
|
|
118
|
-
: { tokenEstimator: this.inputTokenEstimator.compatibility }),
|
|
88
|
+
toolResultModalities: this.toolResultModalities,
|
|
119
89
|
}),
|
|
120
90
|
);
|
|
121
91
|
const prepared: PreparedModelRequest = Object.freeze({
|
|
@@ -167,6 +137,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
167
137
|
const materializedAssets: Array<{
|
|
168
138
|
readonly assetId: ImageAssetId;
|
|
169
139
|
readonly byteLength: number;
|
|
140
|
+
readonly width: number;
|
|
141
|
+
readonly height: number;
|
|
142
|
+
readonly planningTokens: number;
|
|
170
143
|
readonly bytesSha256: string;
|
|
171
144
|
}> = [];
|
|
172
145
|
for (const asset of assets.values()) {
|
|
@@ -174,11 +147,15 @@ export class FakeModelClient implements ModelClient {
|
|
|
174
147
|
const bytes = await options.assetStore.readVerified(asset, {
|
|
175
148
|
signal: options.signal,
|
|
176
149
|
});
|
|
150
|
+
const image = await materializeProviderImage(bytes, asset.mimeType);
|
|
177
151
|
materializedAssets.push(
|
|
178
152
|
Object.freeze({
|
|
179
153
|
assetId: asset.assetId,
|
|
180
|
-
byteLength: bytes.byteLength,
|
|
181
|
-
|
|
154
|
+
byteLength: image.bytes.byteLength,
|
|
155
|
+
width: image.width,
|
|
156
|
+
height: image.height,
|
|
157
|
+
planningTokens: image.planningTokens,
|
|
158
|
+
bytesSha256: createHash("sha256").update(image.bytes).digest("hex"),
|
|
182
159
|
}),
|
|
183
160
|
);
|
|
184
161
|
}
|
|
@@ -191,6 +168,10 @@ export class FakeModelClient implements ModelClient {
|
|
|
191
168
|
const materialized = Object.freeze({
|
|
192
169
|
...prepared,
|
|
193
170
|
payload,
|
|
171
|
+
promptSegments: materializedFakePromptSegments(
|
|
172
|
+
prepared.promptSegments,
|
|
173
|
+
materializedAssets,
|
|
174
|
+
),
|
|
194
175
|
bodyBytes: Buffer.byteLength(stableJsonStringify(payload), "utf8"),
|
|
195
176
|
});
|
|
196
177
|
this.preparedInputs.set(materialized, input);
|
|
@@ -376,7 +357,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
376
357
|
description: "Exercise static history live tail",
|
|
377
358
|
});
|
|
378
359
|
}
|
|
379
|
-
if (!bash.
|
|
360
|
+
if (!toolMessageText(bash).includes("PTY_STATIC_LIVE_LINE_20")) {
|
|
380
361
|
throw new Error("PTY static-history Bash output was incomplete.");
|
|
381
362
|
}
|
|
382
363
|
return textOutput(prepared, "PTY_STATIC_LIVE_DONE");
|
|
@@ -520,9 +501,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
520
501
|
});
|
|
521
502
|
}
|
|
522
503
|
if (
|
|
523
|
-
!bash.
|
|
524
|
-
!bash.
|
|
525
|
-
!bash.
|
|
504
|
+
!toolMessageText(bash).includes("Bash failed") ||
|
|
505
|
+
!toolMessageText(bash).includes("exitCode=7") ||
|
|
506
|
+
!toolMessageText(bash).includes("PTY_TOOL_FAILURE_OUTPUT")
|
|
526
507
|
) {
|
|
527
508
|
throw new Error("PTY Bash failure branch returned an unexpected result.");
|
|
528
509
|
}
|
|
@@ -552,7 +533,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
552
533
|
content: "alpha\n",
|
|
553
534
|
});
|
|
554
535
|
}
|
|
555
|
-
if (!write.
|
|
536
|
+
if (!toolMessageText(write).includes("Write succeeded")) {
|
|
556
537
|
throw new Error("PTY Write tool did not succeed.");
|
|
557
538
|
}
|
|
558
539
|
|
|
@@ -564,7 +545,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
564
545
|
new_string: "beta",
|
|
565
546
|
});
|
|
566
547
|
}
|
|
567
|
-
if (!edit.
|
|
548
|
+
if (!toolMessageText(edit).includes("Edit succeeded")) {
|
|
568
549
|
throw new Error("PTY Edit tool did not succeed.");
|
|
569
550
|
}
|
|
570
551
|
|
|
@@ -576,8 +557,8 @@ export class FakeModelClient implements ModelClient {
|
|
|
576
557
|
});
|
|
577
558
|
}
|
|
578
559
|
if (
|
|
579
|
-
!bash.
|
|
580
|
-
!bash.
|
|
560
|
+
!toolMessageText(bash).includes("Bash completed") ||
|
|
561
|
+
!toolMessageText(bash).includes("PTY_BASH_OK:beta")
|
|
581
562
|
) {
|
|
582
563
|
throw new Error("PTY Bash tool did not verify the edited file.");
|
|
583
564
|
}
|
|
@@ -602,7 +583,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
602
583
|
file_path: "pty-undo-modified.txt",
|
|
603
584
|
});
|
|
604
585
|
}
|
|
605
|
-
if (!read.
|
|
586
|
+
if (!toolMessageText(read).includes("Read succeeded")) {
|
|
606
587
|
throw new Error("PTY undo Read tool did not succeed.");
|
|
607
588
|
}
|
|
608
589
|
|
|
@@ -613,7 +594,10 @@ export class FakeModelClient implements ModelClient {
|
|
|
613
594
|
content: "after undo turn\n",
|
|
614
595
|
});
|
|
615
596
|
}
|
|
616
|
-
if (
|
|
597
|
+
if (
|
|
598
|
+
writes[0] === undefined ||
|
|
599
|
+
!toolMessageText(writes[0]).includes("Write succeeded")
|
|
600
|
+
) {
|
|
617
601
|
throw new Error("PTY undo modifying Write did not succeed.");
|
|
618
602
|
}
|
|
619
603
|
if (writes.length === 1) {
|
|
@@ -622,7 +606,10 @@ export class FakeModelClient implements ModelClient {
|
|
|
622
606
|
content: "created by undo turn\n",
|
|
623
607
|
});
|
|
624
608
|
}
|
|
625
|
-
if (
|
|
609
|
+
if (
|
|
610
|
+
writes[1] === undefined ||
|
|
611
|
+
!toolMessageText(writes[1]).includes("Write succeeded")
|
|
612
|
+
) {
|
|
626
613
|
throw new Error("PTY undo creating Write did not succeed.");
|
|
627
614
|
}
|
|
628
615
|
|
|
@@ -632,7 +619,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
632
619
|
file_path: "pty-undo-deleted.bin",
|
|
633
620
|
});
|
|
634
621
|
}
|
|
635
|
-
if (!deletion.
|
|
622
|
+
if (!toolMessageText(deletion).includes("Delete succeeded")) {
|
|
636
623
|
throw new Error("PTY undo Delete tool did not succeed.");
|
|
637
624
|
}
|
|
638
625
|
return textOutput(prepared, "PTY_UNDO_MUTATIONS_DONE");
|
|
@@ -661,10 +648,10 @@ export class FakeModelClient implements ModelClient {
|
|
|
661
648
|
run_in_background: true,
|
|
662
649
|
});
|
|
663
650
|
}
|
|
664
|
-
if (!bash.
|
|
651
|
+
if (!toolMessageText(bash).includes("Bash command is running in background")) {
|
|
665
652
|
throw new Error("PTY Bash task did not enter the background.");
|
|
666
653
|
}
|
|
667
|
-
const taskId = requireObservationValue(bash
|
|
654
|
+
const taskId = requireObservationValue(toolMessageText(bash), "taskId");
|
|
668
655
|
|
|
669
656
|
if (prompt === "PTY_BACKGROUND_QUIT") {
|
|
670
657
|
return textOutput(prepared, "PTY_BACKGROUND_RUNNING");
|
|
@@ -672,7 +659,10 @@ export class FakeModelClient implements ModelClient {
|
|
|
672
659
|
|
|
673
660
|
const outputs = tools.filter((message) => message.name === "TaskOutput");
|
|
674
661
|
const output = outputs.at(-1);
|
|
675
|
-
if (
|
|
662
|
+
if (
|
|
663
|
+
output === undefined ||
|
|
664
|
+
!toolMessageText(output).includes("PTY_BACKGROUND_READY")
|
|
665
|
+
) {
|
|
676
666
|
if (outputs.length >= 20) {
|
|
677
667
|
throw new Error("PTY background task did not produce its ready marker.");
|
|
678
668
|
}
|
|
@@ -680,7 +670,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
680
670
|
task_id: taskId,
|
|
681
671
|
});
|
|
682
672
|
}
|
|
683
|
-
if (!output.
|
|
673
|
+
if (!toolMessageText(output).includes(`taskId=${taskId}`)) {
|
|
684
674
|
throw new Error("PTY TaskOutput returned the wrong task.");
|
|
685
675
|
}
|
|
686
676
|
|
|
@@ -691,8 +681,8 @@ export class FakeModelClient implements ModelClient {
|
|
|
691
681
|
});
|
|
692
682
|
}
|
|
693
683
|
if (
|
|
694
|
-
!stop.
|
|
695
|
-
!stop.
|
|
684
|
+
!toolMessageText(stop).includes(`taskId=${taskId}`) ||
|
|
685
|
+
!toolMessageText(stop).includes("status=killed")
|
|
696
686
|
) {
|
|
697
687
|
throw new Error("PTY TaskStop did not kill the background task.");
|
|
698
688
|
}
|
|
@@ -726,14 +716,17 @@ export class FakeModelClient implements ModelClient {
|
|
|
726
716
|
timeout: 25,
|
|
727
717
|
});
|
|
728
718
|
}
|
|
729
|
-
if (
|
|
719
|
+
if (
|
|
720
|
+
!toolMessageText(bash).includes("taskId=") ||
|
|
721
|
+
!toolMessageText(bash).includes("tty=true")
|
|
722
|
+
) {
|
|
730
723
|
throw new Error("PTY Bash task did not return an interactive task ID.");
|
|
731
724
|
}
|
|
732
|
-
const taskId = requireObservationValue(bash
|
|
725
|
+
const taskId = requireObservationValue(toolMessageText(bash), "taskId");
|
|
733
726
|
|
|
734
727
|
const outputs = tools.filter((message) => message.name === "TaskOutput");
|
|
735
728
|
const output = outputs.at(-1);
|
|
736
|
-
if (output === undefined || !output.
|
|
729
|
+
if (output === undefined || !toolMessageText(output).includes(">>>")) {
|
|
737
730
|
if (outputs.length >= 20) {
|
|
738
731
|
throw new Error("Interactive Python fixture did not show its prompt.");
|
|
739
732
|
}
|
|
@@ -756,7 +749,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
756
749
|
|
|
757
750
|
const latestInput = inputs.at(-1);
|
|
758
751
|
const expected = prompt === "PTY_INTERACTIVE_QUIT" ? "PTY_INTERACTIVE_PID=" : "42";
|
|
759
|
-
if (!latestInput
|
|
752
|
+
if (latestInput === undefined || !toolMessageText(latestInput).includes(expected)) {
|
|
760
753
|
if (inputs.length >= 20) {
|
|
761
754
|
throw new Error(`Interactive Python fixture did not show ${expected}.`);
|
|
762
755
|
}
|
|
@@ -770,7 +763,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
770
763
|
if (prompt === "PTY_INTERACTIVE_QUIT") {
|
|
771
764
|
return textOutput(prepared, "PTY_INTERACTIVE_RUNNING");
|
|
772
765
|
}
|
|
773
|
-
if (
|
|
766
|
+
if (
|
|
767
|
+
!inputs.some((message) => toolMessageText(message).includes("status=completed"))
|
|
768
|
+
) {
|
|
774
769
|
return toolCallOutput(prepared, options, "TaskInput", {
|
|
775
770
|
task_id: taskId,
|
|
776
771
|
chars: "exit()\n",
|
|
@@ -805,7 +800,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
805
800
|
content: "PTY_RESUME_SIDE_EFFECT\n",
|
|
806
801
|
});
|
|
807
802
|
}
|
|
808
|
-
if (!write.
|
|
803
|
+
if (!toolMessageText(write).includes("Write succeeded")) {
|
|
809
804
|
throw new Error("PTY resume seed Write did not succeed.");
|
|
810
805
|
}
|
|
811
806
|
return textOutput(prepared, "PTY_RESUME_SEED_DONE");
|
|
@@ -837,7 +832,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
837
832
|
content: "PTY_INTERRUPT_SIDE_EFFECT\n",
|
|
838
833
|
});
|
|
839
834
|
}
|
|
840
|
-
if (!write.
|
|
835
|
+
if (!toolMessageText(write).includes("Write succeeded")) {
|
|
841
836
|
throw new Error("PTY interrupted Write did not succeed.");
|
|
842
837
|
}
|
|
843
838
|
return waitForCancellation(options.signal);
|
|
@@ -928,7 +923,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
928
923
|
content: "PTY_FORK_SHARED_HISTORY\n",
|
|
929
924
|
});
|
|
930
925
|
}
|
|
931
|
-
if (!write.
|
|
926
|
+
if (!toolMessageText(write).includes("Write succeeded")) {
|
|
932
927
|
throw new Error("PTY fork seed Write did not succeed.");
|
|
933
928
|
}
|
|
934
929
|
return textOutput(prepared, "PTY_FORK_SEED_DONE");
|
|
@@ -1005,7 +1000,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
1005
1000
|
file_path: "context-heavy.txt",
|
|
1006
1001
|
});
|
|
1007
1002
|
}
|
|
1008
|
-
if (!read.
|
|
1003
|
+
if (!toolMessageText(read).includes("PTY_CONTEXT_ORIGINAL_MARKER")) {
|
|
1009
1004
|
throw new Error("PTY context Read did not return the original marker.");
|
|
1010
1005
|
}
|
|
1011
1006
|
return textOutput(prepared, "PTY_CONTEXT_HEAVY_DONE");
|
|
@@ -1097,7 +1092,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
1097
1092
|
name: "pty-review",
|
|
1098
1093
|
});
|
|
1099
1094
|
}
|
|
1100
|
-
if (!skill.
|
|
1095
|
+
if (!toolMessageText(skill).includes("PTY_SKILL_INSTRUCTIONS")) {
|
|
1101
1096
|
throw new Error("PTY Skill result did not contain the fixture instructions.");
|
|
1102
1097
|
}
|
|
1103
1098
|
return textOutput(prepared, "PTY_SKILL_DONE");
|
|
@@ -1129,7 +1124,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
1129
1124
|
message: "PTY_MCP_PAYLOAD",
|
|
1130
1125
|
});
|
|
1131
1126
|
}
|
|
1132
|
-
if (!echo.
|
|
1127
|
+
if (!toolMessageText(echo).includes("echo: PTY_MCP_PAYLOAD")) {
|
|
1133
1128
|
throw new Error("PTY MCP echo returned unexpected content.");
|
|
1134
1129
|
}
|
|
1135
1130
|
return textOutput(prepared, "PTY_MCP_DONE\n\necho: PTY_MCP_PAYLOAD");
|
|
@@ -1217,10 +1212,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
1217
1212
|
"tool_calls",
|
|
1218
1213
|
);
|
|
1219
1214
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
)?.[1];
|
|
1215
|
+
const recallText = toolMessageText(latestRecallResult);
|
|
1216
|
+
if (recallText.startsWith("Recall searched")) {
|
|
1217
|
+
const source = recallText.match(/^source=(ctx:\/\/message\/[0-9a-f-]+)$/m)?.[1];
|
|
1224
1218
|
if (source === undefined) {
|
|
1225
1219
|
throw new Error("Fake RecallSearch did not return a source.");
|
|
1226
1220
|
}
|
|
@@ -1243,7 +1237,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
1243
1237
|
"tool_calls",
|
|
1244
1238
|
);
|
|
1245
1239
|
}
|
|
1246
|
-
if (!
|
|
1240
|
+
if (!recallText.includes("recall-smoke-marker")) {
|
|
1247
1241
|
throw new Error("Fake RecallGet did not recover the expected marker.");
|
|
1248
1242
|
}
|
|
1249
1243
|
return outputWithUsage(
|
|
@@ -1348,6 +1342,10 @@ function toolMessagesAfterLastUser(
|
|
|
1348
1342
|
);
|
|
1349
1343
|
}
|
|
1350
1344
|
|
|
1345
|
+
function toolMessageText(message: Extract<AgentMessage, { role: "tool" }>): string {
|
|
1346
|
+
return toolResultDisplayText(message.content);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1351
1349
|
function requireMessage(
|
|
1352
1350
|
messages: AgentMessage[],
|
|
1353
1351
|
role: "user" | "assistant",
|
|
@@ -1418,7 +1416,7 @@ function requireToolMessage(
|
|
|
1418
1416
|
(message) =>
|
|
1419
1417
|
message.role === "tool" &&
|
|
1420
1418
|
message.name === name &&
|
|
1421
|
-
message.
|
|
1419
|
+
toolMessageText(message).includes(content),
|
|
1422
1420
|
);
|
|
1423
1421
|
if (!found) {
|
|
1424
1422
|
throw new Error(`Fake PTY context is missing ${name} tool content ${content}.`);
|
|
@@ -1453,20 +1451,30 @@ function lastMessageIndex(
|
|
|
1453
1451
|
return -1;
|
|
1454
1452
|
}
|
|
1455
1453
|
|
|
1456
|
-
function toPromptSegment(
|
|
1454
|
+
function toPromptSegment(
|
|
1455
|
+
message: AgentMessage,
|
|
1456
|
+
messageOrdinal = 0,
|
|
1457
|
+
): PreparedPromptSegment {
|
|
1457
1458
|
if (message.role === "user" && message.attachments !== undefined) {
|
|
1458
1459
|
const media = message.attachments.map(
|
|
1459
|
-
(attachment):
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1460
|
+
(attachment, blockPosition): PreparedMediaOccurrence => {
|
|
1461
|
+
const dimensions = providerImageDimensions(attachment.width, attachment.height);
|
|
1462
|
+
return Object.freeze({
|
|
1463
|
+
asset: Object.freeze({
|
|
1464
|
+
assetId: attachment.assetId,
|
|
1465
|
+
mimeType: attachment.mimeType,
|
|
1466
|
+
byteLength: attachment.byteLength,
|
|
1467
|
+
width: attachment.width,
|
|
1468
|
+
height: attachment.height,
|
|
1469
|
+
}),
|
|
1470
|
+
source: "user_attachment",
|
|
1471
|
+
messageOrdinal,
|
|
1472
|
+
blockPosition,
|
|
1473
|
+
width: dimensions.width,
|
|
1474
|
+
height: dimensions.height,
|
|
1475
|
+
planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
|
|
1476
|
+
});
|
|
1477
|
+
},
|
|
1470
1478
|
);
|
|
1471
1479
|
return Object.freeze({
|
|
1472
1480
|
kind: "user",
|
|
@@ -1474,6 +1482,30 @@ function toPromptSegment(message: AgentMessage): PreparedPromptSegment {
|
|
|
1474
1482
|
media: Object.freeze(media),
|
|
1475
1483
|
});
|
|
1476
1484
|
}
|
|
1485
|
+
if (message.role === "tool") {
|
|
1486
|
+
const media = message.content.flatMap((block, blockPosition) => {
|
|
1487
|
+
if (block.type !== "image") {
|
|
1488
|
+
return [];
|
|
1489
|
+
}
|
|
1490
|
+
const dimensions = providerImageDimensions(block.asset.width, block.asset.height);
|
|
1491
|
+
return [
|
|
1492
|
+
Object.freeze<PreparedMediaOccurrence>({
|
|
1493
|
+
asset: Object.freeze({ ...block.asset }),
|
|
1494
|
+
source: "tool_result",
|
|
1495
|
+
messageOrdinal,
|
|
1496
|
+
blockPosition,
|
|
1497
|
+
width: dimensions.width,
|
|
1498
|
+
height: dimensions.height,
|
|
1499
|
+
planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
|
|
1500
|
+
}),
|
|
1501
|
+
];
|
|
1502
|
+
});
|
|
1503
|
+
return Object.freeze({
|
|
1504
|
+
kind: "tool",
|
|
1505
|
+
normalizedText: stableJsonStringify(message),
|
|
1506
|
+
...(media.length === 0 ? {} : { media: Object.freeze(media) }),
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1477
1509
|
return {
|
|
1478
1510
|
kind:
|
|
1479
1511
|
message.role === "system"
|
|
@@ -1491,33 +1523,55 @@ function distinctPreparedAssets(
|
|
|
1491
1523
|
const assets = new Map<ImageAssetId, ImageAssetRef>();
|
|
1492
1524
|
for (const segment of segments) {
|
|
1493
1525
|
for (const media of segment.media ?? []) {
|
|
1494
|
-
const asset =
|
|
1495
|
-
|
|
1496
|
-
mimeType: media.mimeType,
|
|
1497
|
-
byteLength: media.byteLength,
|
|
1498
|
-
width: media.width,
|
|
1499
|
-
height: media.height,
|
|
1500
|
-
});
|
|
1501
|
-
const existing = assets.get(media.assetId);
|
|
1526
|
+
const asset = media.asset;
|
|
1527
|
+
const existing = assets.get(asset.assetId);
|
|
1502
1528
|
if (
|
|
1503
1529
|
existing !== undefined &&
|
|
1504
1530
|
stableJsonStringify(existing) !== stableJsonStringify(asset)
|
|
1505
1531
|
) {
|
|
1506
|
-
throw new Error(`Conflicting fake image descriptors for ${
|
|
1532
|
+
throw new Error(`Conflicting fake image descriptors for ${asset.assetId}.`);
|
|
1507
1533
|
}
|
|
1508
|
-
assets.set(
|
|
1534
|
+
assets.set(asset.assetId, asset);
|
|
1509
1535
|
}
|
|
1510
1536
|
}
|
|
1511
1537
|
return assets;
|
|
1512
1538
|
}
|
|
1513
1539
|
|
|
1514
|
-
function
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1540
|
+
function materializedFakePromptSegments(
|
|
1541
|
+
segments: readonly PreparedPromptSegment[],
|
|
1542
|
+
images: readonly {
|
|
1543
|
+
assetId: ImageAssetId;
|
|
1544
|
+
width: number;
|
|
1545
|
+
height: number;
|
|
1546
|
+
planningTokens: number;
|
|
1547
|
+
}[],
|
|
1548
|
+
): readonly PreparedPromptSegment[] {
|
|
1549
|
+
const byId = new Map(images.map((image) => [image.assetId, image] as const));
|
|
1550
|
+
return Object.freeze(
|
|
1551
|
+
segments.map((segment) =>
|
|
1552
|
+
segment.media === undefined
|
|
1553
|
+
? segment
|
|
1554
|
+
: Object.freeze({
|
|
1555
|
+
...segment,
|
|
1556
|
+
media: Object.freeze(
|
|
1557
|
+
segment.media.map((media) => {
|
|
1558
|
+
const image = byId.get(media.asset.assetId);
|
|
1559
|
+
if (image === undefined) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`Fake image ${media.asset.assetId} was not materialized.`,
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
return Object.freeze({
|
|
1565
|
+
...media,
|
|
1566
|
+
width: image.width,
|
|
1567
|
+
height: image.height,
|
|
1568
|
+
planningTokens: image.planningTokens,
|
|
1569
|
+
});
|
|
1570
|
+
}),
|
|
1571
|
+
),
|
|
1572
|
+
}),
|
|
1573
|
+
),
|
|
1574
|
+
);
|
|
1521
1575
|
}
|
|
1522
1576
|
|
|
1523
1577
|
function recallMarker(
|
|
@@ -1537,10 +1591,9 @@ function recallMarker(
|
|
|
1537
1591
|
query: marker,
|
|
1538
1592
|
});
|
|
1539
1593
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
)?.[1];
|
|
1594
|
+
const recallText = toolMessageText(latestRecallResult);
|
|
1595
|
+
if (recallText.startsWith("Recall searched")) {
|
|
1596
|
+
const source = recallText.match(/^source=(ctx:\/\/message\/[0-9a-f-]+)$/m)?.[1];
|
|
1544
1597
|
if (source === undefined) {
|
|
1545
1598
|
throw new Error("Fake PTY RecallSearch did not return a source.");
|
|
1546
1599
|
}
|
|
@@ -1548,7 +1601,7 @@ function recallMarker(
|
|
|
1548
1601
|
source,
|
|
1549
1602
|
});
|
|
1550
1603
|
}
|
|
1551
|
-
if (!
|
|
1604
|
+
if (!recallText.includes(marker)) {
|
|
1552
1605
|
throw new Error(`Fake PTY RecallGet did not recover ${marker}.`);
|
|
1553
1606
|
}
|
|
1554
1607
|
return textOutput(prepared, finalText);
|