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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import type { UserMessage } from "../agent/types";
|
|
2
|
+
import type { AgentMessage, UserMessage } from "../agent/types";
|
|
3
3
|
import {
|
|
4
4
|
IMAGE_INPUT_POLICY,
|
|
5
5
|
imagePlanningTokens,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type MaterializedModelRequest,
|
|
14
14
|
type ModelMaterializeOptions,
|
|
15
15
|
type ModelRequestInput,
|
|
16
|
-
type
|
|
16
|
+
type PreparedMediaOccurrence,
|
|
17
17
|
type PreparedModelRequest,
|
|
18
18
|
type PreparedPromptSegment,
|
|
19
19
|
type ProviderResponseErrorCode,
|
|
@@ -124,22 +124,30 @@ export function exactJsonBodyBytes(
|
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export function imageUserSegment(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
127
|
+
export function imageUserSegment(
|
|
128
|
+
message: UserMessage,
|
|
129
|
+
messageOrdinal: number,
|
|
130
|
+
): PreparedPromptSegment {
|
|
131
|
+
const media = message.attachments!.map(
|
|
132
|
+
(attachment, blockPosition): PreparedMediaOccurrence => {
|
|
133
|
+
const dimensions = providerImageDimensions(attachment.width, attachment.height);
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
asset: Object.freeze({
|
|
136
|
+
assetId: attachment.assetId,
|
|
137
|
+
mimeType: attachment.mimeType,
|
|
138
|
+
byteLength: attachment.byteLength,
|
|
139
|
+
width: attachment.width,
|
|
140
|
+
height: attachment.height,
|
|
141
|
+
}),
|
|
142
|
+
source: "user_attachment",
|
|
143
|
+
messageOrdinal,
|
|
144
|
+
blockPosition,
|
|
145
|
+
width: dimensions.width,
|
|
146
|
+
height: dimensions.height,
|
|
147
|
+
planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
);
|
|
143
151
|
return Object.freeze({
|
|
144
152
|
kind: "user",
|
|
145
153
|
normalizedText: message.content,
|
|
@@ -147,6 +155,35 @@ export function imageUserSegment(message: UserMessage): PreparedPromptSegment {
|
|
|
147
155
|
});
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
export function imageToolSegment(
|
|
159
|
+
message: Extract<AgentMessage, { role: "tool" }>,
|
|
160
|
+
messageOrdinal: number,
|
|
161
|
+
normalizedText: string,
|
|
162
|
+
): PreparedPromptSegment {
|
|
163
|
+
const media = message.content.flatMap((block, blockPosition) => {
|
|
164
|
+
if (block.type !== "image") {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const dimensions = providerImageDimensions(block.asset.width, block.asset.height);
|
|
168
|
+
return [
|
|
169
|
+
Object.freeze<PreparedMediaOccurrence>({
|
|
170
|
+
asset: Object.freeze({ ...block.asset }),
|
|
171
|
+
source: "tool_result",
|
|
172
|
+
messageOrdinal,
|
|
173
|
+
blockPosition,
|
|
174
|
+
width: dimensions.width,
|
|
175
|
+
height: dimensions.height,
|
|
176
|
+
planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
|
|
177
|
+
}),
|
|
178
|
+
];
|
|
179
|
+
});
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
kind: "tool",
|
|
182
|
+
normalizedText,
|
|
183
|
+
...(media.length === 0 ? {} : { media: Object.freeze(media) }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
150
187
|
export function normalizedEndpointPolicy(baseURL: string | undefined): string {
|
|
151
188
|
const url = new URL(baseURL ?? "https://api.openai.com/v1");
|
|
152
189
|
url.username = "";
|
|
@@ -208,21 +245,15 @@ function distinctPreparedAssets(
|
|
|
208
245
|
const assets = new Map<ImageAssetId, ImageAssetRef>();
|
|
209
246
|
for (const segment of segments) {
|
|
210
247
|
for (const media of segment.media ?? []) {
|
|
211
|
-
const asset =
|
|
212
|
-
|
|
213
|
-
mimeType: media.mimeType,
|
|
214
|
-
byteLength: media.byteLength,
|
|
215
|
-
width: media.sourceWidth,
|
|
216
|
-
height: media.sourceHeight,
|
|
217
|
-
});
|
|
218
|
-
const existing = assets.get(media.assetId);
|
|
248
|
+
const asset = media.asset;
|
|
249
|
+
const existing = assets.get(asset.assetId);
|
|
219
250
|
if (
|
|
220
251
|
existing !== undefined &&
|
|
221
252
|
stableJsonStringify(existing) !== stableJsonStringify(asset)
|
|
222
253
|
) {
|
|
223
|
-
throw new Error(`Conflicting descriptors for image ${
|
|
254
|
+
throw new Error(`Conflicting descriptors for image ${asset.assetId}.`);
|
|
224
255
|
}
|
|
225
|
-
assets.set(
|
|
256
|
+
assets.set(asset.assetId, asset);
|
|
226
257
|
}
|
|
227
258
|
}
|
|
228
259
|
return assets;
|
|
@@ -296,10 +327,10 @@ function materializedPromptSegments(
|
|
|
296
327
|
...segment,
|
|
297
328
|
media: Object.freeze(
|
|
298
329
|
segment.media.map((media) => {
|
|
299
|
-
const image = images.get(media.assetId);
|
|
330
|
+
const image = images.get(media.asset.assetId);
|
|
300
331
|
if (image === undefined) {
|
|
301
332
|
throw new Error(
|
|
302
|
-
`Image ${media.assetId.slice(0, 12)}… was not materialized.`,
|
|
333
|
+
`Image ${media.asset.assetId.slice(0, 12)}… was not materialized.`,
|
|
303
334
|
);
|
|
304
335
|
}
|
|
305
336
|
return Object.freeze({
|
|
@@ -10,6 +10,10 @@ import type {
|
|
|
10
10
|
ToolCall,
|
|
11
11
|
UserMessage,
|
|
12
12
|
} from "../agent/types";
|
|
13
|
+
import {
|
|
14
|
+
toolResultText,
|
|
15
|
+
validateToolResultContent,
|
|
16
|
+
} from "../agent/tool-result-content";
|
|
13
17
|
import type { RuntimeSessionContext } from "../agent/runtime-session";
|
|
14
18
|
import {
|
|
15
19
|
parseImageAssetId,
|
|
@@ -61,11 +65,31 @@ export function toOpenAIResponsesItems(
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
if (message.role === "tool") {
|
|
68
|
+
validateToolResultContent(message.content);
|
|
69
|
+
const hasImage = message.content.some((block) => block.type === "image");
|
|
64
70
|
return [
|
|
65
71
|
{
|
|
66
72
|
type: "function_call_output",
|
|
67
73
|
call_id: message.providerToolCallId,
|
|
68
|
-
output:
|
|
74
|
+
output: hasImage
|
|
75
|
+
? message.content.map((block) =>
|
|
76
|
+
block.type === "text"
|
|
77
|
+
? ({ type: "input_text", text: block.text } as const)
|
|
78
|
+
: ({
|
|
79
|
+
type: "input_image",
|
|
80
|
+
detail: "auto" as const,
|
|
81
|
+
image_url:
|
|
82
|
+
options.materializedImages === undefined
|
|
83
|
+
? (imageAssetUrlMarker(
|
|
84
|
+
block.asset.assetId,
|
|
85
|
+
) as unknown as string)
|
|
86
|
+
: requireMaterializedImage(
|
|
87
|
+
options.materializedImages,
|
|
88
|
+
block.asset.assetId,
|
|
89
|
+
),
|
|
90
|
+
} as const),
|
|
91
|
+
)
|
|
92
|
+
: toolResultText(message.content),
|
|
69
93
|
},
|
|
70
94
|
];
|
|
71
95
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
IMAGE_INPUT_POLICY_VERSION,
|
|
10
10
|
} from "../image/image-input-policy";
|
|
11
11
|
import type { ModelContextBudget } from "./model-context-profile";
|
|
12
|
-
import { ProviderResponseError } from "./model-client";
|
|
12
|
+
import { ProviderResponseError, validateModelModalities } from "./model-client";
|
|
13
13
|
import type {
|
|
14
14
|
MaterializedModelRequest,
|
|
15
15
|
ModelClient,
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
} from "./model-client";
|
|
24
24
|
import {
|
|
25
25
|
deepFreeze,
|
|
26
|
+
imageToolSegment,
|
|
26
27
|
imageUserSegment,
|
|
27
28
|
materializeOpenAIRequest,
|
|
28
29
|
normalizedEndpointPolicy,
|
|
@@ -39,7 +40,7 @@ import { OpenAIResponsesStreamAccumulator } from "./openai-responses-stream";
|
|
|
39
40
|
import type { ReasoningEffortController } from "./reasoning-effort";
|
|
40
41
|
import { sha256, stableJsonStringify } from "./model-request-preflight";
|
|
41
42
|
|
|
42
|
-
const OPENAI_RESPONSES_SERIALIZATION_VERSION = "openai-responses-
|
|
43
|
+
const OPENAI_RESPONSES_SERIALIZATION_VERSION = "openai-responses-v2";
|
|
43
44
|
const OPENAI_RESPONSES_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
44
45
|
|
|
45
46
|
export class OpenAIResponsesModelClient implements ModelClient {
|
|
@@ -48,6 +49,7 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
48
49
|
serializationVersion: OPENAI_RESPONSES_SERIALIZATION_VERSION,
|
|
49
50
|
});
|
|
50
51
|
readonly inputModalities: readonly ("text" | "image")[];
|
|
52
|
+
readonly toolResultModalities: readonly ("text" | "image")[];
|
|
51
53
|
readonly reasoningEffort?: ReasoningEffortController;
|
|
52
54
|
private readonly client: OpenAI;
|
|
53
55
|
private readonly preparedRequests = new WeakSet<object>();
|
|
@@ -61,6 +63,8 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
61
63
|
contextBudget: ModelContextBudget;
|
|
62
64
|
baseURL?: string;
|
|
63
65
|
inputModalities?: readonly ("text" | "image")[];
|
|
66
|
+
toolResultModalities?: readonly ("text" | "image")[];
|
|
67
|
+
profileName?: string;
|
|
64
68
|
model: string;
|
|
65
69
|
providerName?: string;
|
|
66
70
|
reasoningEffort?: ReasoningEffortController;
|
|
@@ -72,10 +76,15 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
72
76
|
this.provider = options.providerName ?? "responses-compatible";
|
|
73
77
|
this.stream = options.stream ?? true;
|
|
74
78
|
this.reasoningEffort = options.reasoningEffort;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const modalities = validateModelModalities({
|
|
80
|
+
profileName: options.profileName,
|
|
81
|
+
adapter: this.messageProtocol.adapter,
|
|
82
|
+
inputModalities: options.inputModalities ?? ["text"],
|
|
83
|
+
toolResultModalities: options.toolResultModalities ?? ["text"],
|
|
84
|
+
adapterToolResultModalities: ["text", "image"],
|
|
85
|
+
});
|
|
86
|
+
this.inputModalities = modalities.inputModalities;
|
|
87
|
+
this.toolResultModalities = modalities.toolResultModalities;
|
|
79
88
|
this.client = new OpenAI({
|
|
80
89
|
apiKey: options.apiKey,
|
|
81
90
|
baseURL: options.baseURL,
|
|
@@ -113,11 +122,17 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
113
122
|
const messageSegments = input.messages.map(
|
|
114
123
|
(message, index): PreparedPromptSegment =>
|
|
115
124
|
message.role === "user" && message.attachments !== undefined
|
|
116
|
-
? imageUserSegment(message)
|
|
117
|
-
:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
? imageUserSegment(message, index + 1)
|
|
126
|
+
: message.role === "tool"
|
|
127
|
+
? imageToolSegment(
|
|
128
|
+
message,
|
|
129
|
+
index + 1,
|
|
130
|
+
stableJsonStringify(itemsByMessage[index]),
|
|
131
|
+
)
|
|
132
|
+
: {
|
|
133
|
+
kind: segmentKind(message.role),
|
|
134
|
+
normalizedText: stableJsonStringify(itemsByMessage[index]),
|
|
135
|
+
},
|
|
121
136
|
);
|
|
122
137
|
const mediaOccurrenceCount = messageSegments.reduce(
|
|
123
138
|
(total, segment) => total + (segment.media?.length ?? 0),
|
|
@@ -132,6 +147,7 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
132
147
|
requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
133
148
|
stream: this.stream,
|
|
134
149
|
inputModalities: this.inputModalities,
|
|
150
|
+
toolResultModalities: this.toolResultModalities,
|
|
135
151
|
requestPolicy: { store: false, toolChoice: "auto" },
|
|
136
152
|
imagePolicy: {
|
|
137
153
|
version: IMAGE_INPUT_POLICY_VERSION,
|
|
@@ -85,6 +85,16 @@ export function estimatePromptSegments(
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export function guardedContextTokens(
|
|
89
|
+
breakdown: RawContextBreakdown,
|
|
90
|
+
correctionFactor: number,
|
|
91
|
+
): number {
|
|
92
|
+
return (
|
|
93
|
+
Math.ceil(breakdown.textAndProtocolTokens * correctionFactor) +
|
|
94
|
+
breakdown.imageTokens
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
export class RollingTokenCalibration {
|
|
89
99
|
private readonly samples: number[] = [];
|
|
90
100
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ToolCall } from "../agent/types";
|
|
2
|
+
import type { ToolResultContent } from "../agent/types";
|
|
3
|
+
import {
|
|
4
|
+
textToolResultContent,
|
|
5
|
+
toolResultDisplayText,
|
|
6
|
+
} from "../agent/tool-result-content";
|
|
2
7
|
import type {
|
|
3
8
|
BashRawResult,
|
|
4
9
|
DeleteFileRawResult,
|
|
@@ -6,6 +11,7 @@ import type {
|
|
|
6
11
|
GenericToolRawResult,
|
|
7
12
|
GlobRawResult,
|
|
8
13
|
GrepRawResult,
|
|
14
|
+
MemoryGetRawResult,
|
|
9
15
|
MemorySearchRawResult,
|
|
10
16
|
McpToolRawResult,
|
|
11
17
|
ReadFileRawResult,
|
|
@@ -17,66 +23,100 @@ import type {
|
|
|
17
23
|
TaskStopRawResult,
|
|
18
24
|
ToolRawResult,
|
|
19
25
|
UpdatePlanRawResult,
|
|
26
|
+
ViewImageRawResult,
|
|
27
|
+
WaitRawResult,
|
|
20
28
|
WebFetchRawResult,
|
|
21
29
|
WebSearchRawResult,
|
|
22
30
|
WriteFileRawResult,
|
|
23
31
|
} from "../tools/types";
|
|
24
32
|
|
|
25
33
|
export type ToolObservation = {
|
|
26
|
-
content:
|
|
34
|
+
readonly content: readonly ToolResultContent[];
|
|
35
|
+
readonly displayText: string;
|
|
27
36
|
};
|
|
28
37
|
|
|
29
38
|
export class ObservationBuilder {
|
|
30
39
|
build(input: { call: ToolCall; raw: ToolRawResult }): ToolObservation {
|
|
31
40
|
switch (input.raw.kind) {
|
|
32
41
|
case "glob":
|
|
33
|
-
return
|
|
42
|
+
return textObservation(renderGlobObservation(input.raw));
|
|
34
43
|
case "grep":
|
|
35
|
-
return
|
|
44
|
+
return textObservation(renderGrepObservation(input.raw));
|
|
36
45
|
case "read":
|
|
37
|
-
return
|
|
46
|
+
return textObservation(renderReadObservation(input.raw));
|
|
47
|
+
case "view_image":
|
|
48
|
+
return renderViewImageObservation(input.raw);
|
|
38
49
|
case "recall":
|
|
39
|
-
return
|
|
50
|
+
return textObservation(renderRecallObservation(input.raw));
|
|
40
51
|
case "memory_search":
|
|
41
|
-
return
|
|
52
|
+
return textObservation(renderMemorySearchObservation(input.raw));
|
|
53
|
+
case "memory_get":
|
|
54
|
+
return textObservation(renderMemoryGetObservation(input.raw));
|
|
42
55
|
case "skill":
|
|
43
|
-
return
|
|
56
|
+
return textObservation(renderSkillObservation(input.raw));
|
|
44
57
|
case "write":
|
|
45
|
-
return
|
|
58
|
+
return textObservation(renderWriteObservation(input.raw));
|
|
46
59
|
case "edit":
|
|
47
|
-
return
|
|
60
|
+
return textObservation(renderEditObservation(input.raw));
|
|
48
61
|
case "delete":
|
|
49
|
-
return
|
|
62
|
+
return textObservation(renderDeleteObservation(input.raw));
|
|
50
63
|
case "bash":
|
|
51
|
-
return
|
|
64
|
+
return textObservation(renderBashObservation(input.raw));
|
|
52
65
|
case "update_plan":
|
|
53
|
-
return
|
|
66
|
+
return textObservation(renderUpdatePlanObservation(input.raw));
|
|
67
|
+
case "wait":
|
|
68
|
+
return textObservation(renderWaitObservation(input.raw));
|
|
54
69
|
case "task_list":
|
|
55
|
-
return
|
|
70
|
+
return textObservation(renderTaskListObservation(input.raw));
|
|
56
71
|
case "task_output":
|
|
57
|
-
return
|
|
72
|
+
return textObservation(renderTaskOutputObservation(input.raw));
|
|
58
73
|
case "task_input":
|
|
59
|
-
return
|
|
74
|
+
return textObservation(renderTaskInputObservation(input.raw));
|
|
60
75
|
case "task_stop":
|
|
61
|
-
return
|
|
76
|
+
return textObservation(renderTaskStopObservation(input.raw));
|
|
62
77
|
case "web_search":
|
|
63
|
-
return
|
|
78
|
+
return textObservation(renderWebSearchObservation(input.raw));
|
|
64
79
|
case "web_fetch":
|
|
65
|
-
return
|
|
80
|
+
return textObservation(renderWebFetchObservation(input.raw));
|
|
66
81
|
case "mcp":
|
|
67
|
-
return
|
|
82
|
+
return textObservation(renderMcpObservation(input.raw));
|
|
68
83
|
case "generic":
|
|
69
|
-
return
|
|
84
|
+
return textObservation(renderGenericObservation(input.raw));
|
|
70
85
|
default:
|
|
71
86
|
return assertNever(input.raw);
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
function textObservation(text: string): ToolObservation {
|
|
92
|
+
return Object.freeze({ content: textToolResultContent(text), displayText: text });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderViewImageObservation(raw: ViewImageRawResult): ToolObservation {
|
|
96
|
+
if (!raw.ok || raw.asset === undefined) {
|
|
97
|
+
return textObservation(
|
|
98
|
+
`ViewImage failed for ${raw.filePath || "(unknown path)"}: ${raw.error ?? "Unknown error."}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const text = `Viewed image ${raw.filePath} (${raw.asset.mimeType}, ${raw.asset.width}x${raw.asset.height}, ${raw.asset.byteLength} bytes, asset=${raw.asset.assetId.slice(0, 12)}…).`;
|
|
102
|
+
const content = Object.freeze([
|
|
103
|
+
Object.freeze({ type: "text" as const, text }),
|
|
104
|
+
Object.freeze({ type: "image" as const, asset: raw.asset }),
|
|
105
|
+
]);
|
|
106
|
+
const displayText = toolResultDisplayText(content);
|
|
107
|
+
return Object.freeze({ content, displayText });
|
|
108
|
+
}
|
|
109
|
+
|
|
76
110
|
function renderUpdatePlanObservation(raw: UpdatePlanRawResult): string {
|
|
77
111
|
return raw.ok ? "Plan updated." : `UpdatePlan failed: ${raw.error}`;
|
|
78
112
|
}
|
|
79
113
|
|
|
114
|
+
function renderWaitObservation(raw: WaitRawResult): string {
|
|
115
|
+
return raw.ok
|
|
116
|
+
? `Waited ${raw.seconds} second${raw.seconds === 1 ? "" : "s"}.`
|
|
117
|
+
: `Wait failed: ${raw.error}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
80
120
|
function assertNever(value: never): never {
|
|
81
121
|
throw new Error(`Unhandled tool raw result: ${JSON.stringify(value)}`);
|
|
82
122
|
}
|
|
@@ -247,16 +287,51 @@ function renderMemorySearchObservation(raw: MemorySearchRawResult): string {
|
|
|
247
287
|
if (!raw.ok) {
|
|
248
288
|
return `MemorySearch unavailable: ${raw.error}`;
|
|
249
289
|
}
|
|
290
|
+
const degradedNote =
|
|
291
|
+
raw.degraded === "vector"
|
|
292
|
+
? " vector search unavailable; keyword results only."
|
|
293
|
+
: raw.degraded === "fts"
|
|
294
|
+
? " keyword search unavailable; vector results only."
|
|
295
|
+
: "";
|
|
250
296
|
if (raw.matches.length === 0) {
|
|
251
|
-
return
|
|
297
|
+
return `MemorySearch found no stored memories.${degradedNote}`;
|
|
252
298
|
}
|
|
253
|
-
const header = `MemorySearch returned ${raw.matches.length} derived
|
|
299
|
+
const header = `MemorySearch returned ${raw.matches.length} derived historical memory records.${degradedNote} They describe past turns and may be stale or wrong; verify current workspace facts with current tools before relying on them.`;
|
|
300
|
+
const footer =
|
|
301
|
+
"Use MemoryGet on a result's memory id when its summary is truncated or you need the exact stored record; use RecallSearch on its source session for the full original context.";
|
|
254
302
|
return [
|
|
255
303
|
header,
|
|
256
|
-
...raw.matches.map(
|
|
257
|
-
|
|
258
|
-
`${index + 1}. score=${match.score.toFixed(3)} created_at=${match.createdAt} workspace=${match.sourceWorkspace}
|
|
304
|
+
...raw.matches.map((match, index) =>
|
|
305
|
+
[
|
|
306
|
+
`${index + 1}. score=${match.score.toFixed(3)} via=${match.via.join(",")} created_at=${match.createdAt} workspace=${match.sourceWorkspace} session=${match.sourceSessionId} memory=${match.memoryId}`,
|
|
307
|
+
` ${match.text}`,
|
|
308
|
+
...(match.summary === "" ? [] : [` summary: ${match.summary}`]),
|
|
309
|
+
].join("\n"),
|
|
259
310
|
),
|
|
311
|
+
footer,
|
|
312
|
+
].join("\n\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function renderMemoryGetObservation(raw: MemoryGetRawResult): string {
|
|
316
|
+
if (!raw.ok) {
|
|
317
|
+
return `MemoryGet unavailable: ${raw.error}`;
|
|
318
|
+
}
|
|
319
|
+
if (raw.memory === null) {
|
|
320
|
+
return "MemoryGet found no stored memory with that id.";
|
|
321
|
+
}
|
|
322
|
+
const memory = raw.memory;
|
|
323
|
+
const header =
|
|
324
|
+
"MemoryGet returned one derived historical memory record. It describes a past turn and may be stale or wrong; verify current workspace facts with current tools before relying on it.";
|
|
325
|
+
const footer =
|
|
326
|
+
"Use RecallSearch on the source session when you need the full original context.";
|
|
327
|
+
return [
|
|
328
|
+
header,
|
|
329
|
+
[
|
|
330
|
+
`memory=${memory.memoryId} created_at=${memory.createdAt} workspace=${memory.sourceWorkspace} session=${memory.sourceSessionId} turn=${memory.sourceTurnId}`,
|
|
331
|
+
`text: ${memory.text}`,
|
|
332
|
+
...(memory.summary === "" ? [] : [`summary: ${memory.summary}`]),
|
|
333
|
+
].join("\n"),
|
|
334
|
+
footer,
|
|
260
335
|
].join("\n\n");
|
|
261
336
|
}
|
|
262
337
|
|