@agent-native/core 0.175.3 → 0.175.4-nightly-20260826190205
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/corpus/README.md +1 -1
- package/corpus/templates/clips/server/plugins/agent-chat.ts +3 -0
- package/corpus/templates/content/actions/_linked-local-document-edit.ts +167 -0
- package/corpus/templates/content/actions/edit-document.ts +181 -61
- package/corpus/templates/content/actions/import-content-source.ts +33 -1
- package/corpus/templates/content/actions/update-document.ts +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +68 -0
- package/corpus/templates/content/app/components/editor/LinkedLocalDocumentAgentBridge.tsx +245 -0
- package/corpus/templates/content/app/lib/local-content-source-files.ts +96 -9
- package/corpus/templates/content/shared/api.ts +2 -0
- package/corpus/templates/content/shared/content-source.ts +14 -0
- package/corpus/templates/content/shared/document-text-edits.ts +39 -0
- package/corpus/templates/content/shared/nfm.ts +347 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
- package/dist/agent/engine/ai-sdk-engine.js +14 -4
- package/dist/agent/engine/anthropic-engine.js +6 -4
- package/dist/agent/engine/builder-engine.js +11 -4
- package/dist/agent/engine/tool-name.d.ts +9 -0
- package/dist/agent/engine/tool-name.js +56 -0
- package/dist/agent/engine/translate-ai-sdk.d.ts +5 -3
- package/dist/agent/engine/translate-ai-sdk.js +16 -13
- package/dist/agent/engine/translate-anthropic.d.ts +8 -6
- package/dist/agent/engine/translate-anthropic.js +21 -22
- package/dist/agent/production-agent.js +23 -5
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/fetch-tool.js +73 -5
- package/dist/extensions/proxy-security.d.ts +5 -0
- package/dist/extensions/proxy-security.js +21 -13
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/server/agent-chat/context-tools.js +5 -0
- package/dist/server/credential-provider.js +47 -10
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/webmcp.mdx +5 -5
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* `ModelMessage` shapes.
|
|
8
8
|
*/
|
|
9
9
|
import { classifyProviderError, describeErrorWithCauses, } from "./error-detail.js";
|
|
10
|
+
import { createProviderToolNameMap, toEngineToolName, toProviderToolName, } from "./tool-name.js";
|
|
10
11
|
import { backfillEngineMessagesToolResults } from "./translate-anthropic.js";
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// EngineTool → AI SDK tool definition
|
|
@@ -19,7 +20,7 @@ import { backfillEngineMessagesToolResults } from "./translate-anthropic.js";
|
|
|
19
20
|
* schema is wrapped in the SDK's runtime validator; fall back to the raw JSON
|
|
20
21
|
* Schema object otherwise (mostly for unit tests that don't import `ai`).
|
|
21
22
|
*/
|
|
22
|
-
export function engineToolsToAISDK(tools, jsonSchema) {
|
|
23
|
+
export function engineToolsToAISDK(tools, jsonSchema, toolNameMap = createProviderToolNameMap(tools)) {
|
|
23
24
|
const result = {};
|
|
24
25
|
for (const tool of tools) {
|
|
25
26
|
const rawSchema = {
|
|
@@ -28,7 +29,8 @@ export function engineToolsToAISDK(tools, jsonSchema) {
|
|
|
28
29
|
properties: tool.inputSchema.properties ?? {},
|
|
29
30
|
required: tool.inputSchema.required ?? [],
|
|
30
31
|
};
|
|
31
|
-
|
|
32
|
+
const providerName = toProviderToolName(tool.name, toolNameMap);
|
|
33
|
+
result[providerName] = {
|
|
32
34
|
description: tool.description,
|
|
33
35
|
inputSchema: jsonSchema ? jsonSchema(rawSchema) : rawSchema,
|
|
34
36
|
};
|
|
@@ -92,7 +94,7 @@ export function engineMessageToAISDK(msg, opts) {
|
|
|
92
94
|
toolResultParts.push({
|
|
93
95
|
type: "tool-result",
|
|
94
96
|
toolCallId: part.toolCallId,
|
|
95
|
-
toolName: part.toolName,
|
|
97
|
+
toolName: toProviderToolName(part.toolName, opts?.toolNameMap),
|
|
96
98
|
output: toolResultOutputToAISDK(part, opts),
|
|
97
99
|
});
|
|
98
100
|
}
|
|
@@ -121,7 +123,7 @@ export function engineMessageToAISDK(msg, opts) {
|
|
|
121
123
|
content.push({
|
|
122
124
|
type: "tool-call",
|
|
123
125
|
toolCallId: part.id,
|
|
124
|
-
toolName: part.name,
|
|
126
|
+
toolName: toProviderToolName(part.name, opts?.toolNameMap),
|
|
125
127
|
input: part.input,
|
|
126
128
|
});
|
|
127
129
|
}
|
|
@@ -152,7 +154,8 @@ export function engineMessageToAISDK(msg, opts) {
|
|
|
152
154
|
throw new Error(`unknown EngineMessage role: ${msg.role}`);
|
|
153
155
|
}
|
|
154
156
|
export function engineMessagesToAISDK(messages, opts) {
|
|
155
|
-
|
|
157
|
+
const toolNameMap = opts?.toolNameMap ?? createProviderToolNameMap([], messages);
|
|
158
|
+
return backfillEngineMessagesToolResults(messages).flatMap((msg) => engineMessageToAISDK(msg, { ...opts, toolNameMap }));
|
|
156
159
|
}
|
|
157
160
|
// ---------------------------------------------------------------------------
|
|
158
161
|
// AI SDK TextStreamPart → EngineEvent
|
|
@@ -166,7 +169,7 @@ export function engineMessagesToAISDK(messages, opts) {
|
|
|
166
169
|
* We absorb text/reasoning boundaries, forward text/reasoning/tool-input
|
|
167
170
|
* deltas, and keep the terminal `tool-call`, `finish-step`, and `finish` parts.
|
|
168
171
|
*/
|
|
169
|
-
export function aiSdkPartToEngineEvents(part) {
|
|
172
|
+
export function aiSdkPartToEngineEvents(part, toolNameMap) {
|
|
170
173
|
const events = [];
|
|
171
174
|
switch (part?.type) {
|
|
172
175
|
case "text-delta":
|
|
@@ -187,14 +190,14 @@ export function aiSdkPartToEngineEvents(part) {
|
|
|
187
190
|
events.push({
|
|
188
191
|
type: "tool-input-start",
|
|
189
192
|
id: part.id ?? part.toolCallId,
|
|
190
|
-
name: part.toolName,
|
|
193
|
+
name: toEngineToolName(part.toolName, toolNameMap),
|
|
191
194
|
});
|
|
192
195
|
break;
|
|
193
196
|
case "tool-input-delta":
|
|
194
197
|
events.push({
|
|
195
198
|
type: "tool-input-delta",
|
|
196
199
|
id: part.id ?? part.toolCallId,
|
|
197
|
-
name: part.toolName,
|
|
200
|
+
name: toEngineToolName(part.toolName, toolNameMap),
|
|
198
201
|
text: typeof part.delta === "string"
|
|
199
202
|
? part.delta
|
|
200
203
|
: typeof part.text === "string"
|
|
@@ -209,7 +212,7 @@ export function aiSdkPartToEngineEvents(part) {
|
|
|
209
212
|
events.push({
|
|
210
213
|
type: "tool-call",
|
|
211
214
|
id: part.toolCallId,
|
|
212
|
-
name: part.toolName,
|
|
215
|
+
name: toEngineToolName(part.toolName, toolNameMap),
|
|
213
216
|
input: part.input ?? {},
|
|
214
217
|
});
|
|
215
218
|
break;
|
|
@@ -218,7 +221,7 @@ export function aiSdkPartToEngineEvents(part) {
|
|
|
218
221
|
events.push({
|
|
219
222
|
type: "tool-call-error",
|
|
220
223
|
id: part.toolCallId,
|
|
221
|
-
name: part.toolName,
|
|
224
|
+
name: toEngineToolName(part.toolName, toolNameMap),
|
|
222
225
|
input: part.input ?? {},
|
|
223
226
|
error: part.errorText ??
|
|
224
227
|
(part.error instanceof Error
|
|
@@ -313,7 +316,7 @@ function usageEventFromLanguageModelUsage(usage) {
|
|
|
313
316
|
* Reconstruct the assistant message content from an AI SDK v6 `StepResult`.
|
|
314
317
|
* `step.content` is the canonical structured form — iterate it.
|
|
315
318
|
*/
|
|
316
|
-
export function aiSdkStepToAssistantContent(step) {
|
|
319
|
+
export function aiSdkStepToAssistantContent(step, toolNameMap) {
|
|
317
320
|
const parts = [];
|
|
318
321
|
for (const part of step?.content ?? []) {
|
|
319
322
|
if (part.type === "text" && part.text) {
|
|
@@ -333,7 +336,7 @@ export function aiSdkStepToAssistantContent(step) {
|
|
|
333
336
|
parts.push({
|
|
334
337
|
type: "tool-call",
|
|
335
338
|
id: part.toolCallId,
|
|
336
|
-
name: part.toolName,
|
|
339
|
+
name: toEngineToolName(part.toolName, toolNameMap),
|
|
337
340
|
input: part.input,
|
|
338
341
|
});
|
|
339
342
|
}
|
|
@@ -341,7 +344,7 @@ export function aiSdkStepToAssistantContent(step) {
|
|
|
341
344
|
parts.push({
|
|
342
345
|
type: "tool-call",
|
|
343
346
|
id: part.toolCallId,
|
|
344
|
-
name: part.toolName,
|
|
347
|
+
name: toEngineToolName(part.toolName, toolNameMap),
|
|
345
348
|
input: part.input ?? {},
|
|
346
349
|
});
|
|
347
350
|
}
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* (`engineMessagesToAnthropic`).
|
|
14
14
|
*/
|
|
15
15
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
16
|
+
import { type ProviderToolNameMap } from "./tool-name.js";
|
|
16
17
|
import type { EngineTool, EngineMessage, EngineContentPart, EngineEvent } from "./types.js";
|
|
17
|
-
export declare function engineToolToAnthropic(tool: EngineTool): Anthropic.Tool;
|
|
18
|
-
export declare function engineToolsToAnthropic(tools: EngineTool[]): Anthropic.Tool[];
|
|
18
|
+
export declare function engineToolToAnthropic(tool: EngineTool, toolNameMap?: ProviderToolNameMap): Anthropic.Tool;
|
|
19
|
+
export declare function engineToolsToAnthropic(tools: EngineTool[], toolNameMap?: ProviderToolNameMap): Anthropic.Tool[];
|
|
19
20
|
/** JSON.stringify for tool_use inputs; never throws. */
|
|
20
21
|
export declare function stringifyToolUseInputForGateway(input: unknown): string;
|
|
21
22
|
/** Same lead-in as structured-history replay when a tool_result cannot be paired. */
|
|
@@ -41,15 +42,16 @@ export declare function unmatchedToolResultReplayText(part: {
|
|
|
41
42
|
export declare function backfillEngineMessagesToolResults(messages: EngineMessage[]): EngineMessage[];
|
|
42
43
|
export declare function engineMessageToAnthropic(msg: EngineMessage, opts?: {
|
|
43
44
|
builderGateway?: boolean;
|
|
45
|
+
toolNameMap?: ProviderToolNameMap;
|
|
44
46
|
}): Anthropic.MessageParam;
|
|
45
47
|
/** Messages for the Anthropic HTTP API (strict schema — no extra tool_result fields). */
|
|
46
|
-
export declare function engineMessagesToAnthropic(messages: EngineMessage[]): Anthropic.MessageParam[];
|
|
48
|
+
export declare function engineMessagesToAnthropic(messages: EngineMessage[], toolNameMap?: ProviderToolNameMap): Anthropic.MessageParam[];
|
|
47
49
|
/**
|
|
48
50
|
* Messages for the Builder LLM gateway (Gemini-backed). Same Anthropic-shaped
|
|
49
51
|
* envelope, but every `tool_result` includes `tool_name` and `tool_input`.
|
|
50
52
|
*/
|
|
51
|
-
export declare function engineMessagesToBuilderGatewayAnthropic(messages: EngineMessage[]): Anthropic.MessageParam[];
|
|
52
|
-
export declare function anthropicContentToEngine(content: Anthropic.ContentBlock[]): EngineContentPart[];
|
|
53
|
+
export declare function engineMessagesToBuilderGatewayAnthropic(messages: EngineMessage[], toolNameMap?: ProviderToolNameMap): Anthropic.MessageParam[];
|
|
54
|
+
export declare function anthropicContentToEngine(content: Anthropic.ContentBlock[], toolNameMap?: ProviderToolNameMap): EngineContentPart[];
|
|
53
55
|
/**
|
|
54
56
|
* Mutable state threaded across `anthropicChunkToEngineEvents` calls within a
|
|
55
57
|
* single stream. Anthropic's `content_block_delta` chunks carry only the block
|
|
@@ -76,7 +78,7 @@ export declare function createAnthropicChunkStreamState(): AnthropicChunkStreamS
|
|
|
76
78
|
* the engine, so omitting `state` simply drops the progress events without
|
|
77
79
|
* changing tool dispatch.
|
|
78
80
|
*/
|
|
79
|
-
export declare function anthropicChunkToEngineEvents(chunk: any, state?: AnthropicChunkStreamState): EngineEvent[];
|
|
81
|
+
export declare function anthropicChunkToEngineEvents(chunk: any, state?: AnthropicChunkStreamState, toolNameMap?: ProviderToolNameMap): EngineEvent[];
|
|
80
82
|
/**
|
|
81
83
|
* Tool arguments arrive split across an arbitrary number of deltas. Every
|
|
82
84
|
* engine announces a tool call the moment the first delta lands, so a stream
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* that path. The native Anthropic API keeps the strict `tool_result` shape
|
|
13
13
|
* (`engineMessagesToAnthropic`).
|
|
14
14
|
*/
|
|
15
|
+
import { createProviderToolNameMap, toEngineToolName, toProviderToolName, } from "./tool-name.js";
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// EngineTool → Anthropic.Tool
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -61,15 +62,16 @@ function normalizeAnthropicInputSchema(toolName, schema) {
|
|
|
61
62
|
normalized.type = "object";
|
|
62
63
|
return normalized;
|
|
63
64
|
}
|
|
64
|
-
export function engineToolToAnthropic(tool) {
|
|
65
|
+
export function engineToolToAnthropic(tool, toolNameMap) {
|
|
66
|
+
const providerName = toProviderToolName(tool.name, toolNameMap);
|
|
65
67
|
return {
|
|
66
|
-
name:
|
|
68
|
+
name: providerName,
|
|
67
69
|
description: tool.description,
|
|
68
70
|
input_schema: normalizeAnthropicInputSchema(tool.name, tool.inputSchema),
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
|
-
export function engineToolsToAnthropic(tools) {
|
|
72
|
-
return tools.map(engineToolToAnthropic);
|
|
73
|
+
export function engineToolsToAnthropic(tools, toolNameMap = createProviderToolNameMap(tools)) {
|
|
74
|
+
return tools.map((tool) => engineToolToAnthropic(tool, toolNameMap));
|
|
73
75
|
}
|
|
74
76
|
// ---------------------------------------------------------------------------
|
|
75
77
|
// Tool result backfill (Gemini / Builder gateway)
|
|
@@ -287,14 +289,14 @@ export function engineMessageToAnthropic(msg, opts) {
|
|
|
287
289
|
: msg.content.filter(replayableAnthropicPart);
|
|
288
290
|
return {
|
|
289
291
|
role: msg.role,
|
|
290
|
-
content: content.map((p) => enginePartToAnthropic(p, builderGateway)),
|
|
292
|
+
content: content.map((p) => enginePartToAnthropic(p, builderGateway, opts?.toolNameMap)),
|
|
291
293
|
};
|
|
292
294
|
}
|
|
293
295
|
/** Messages for the Anthropic HTTP API (strict schema — no extra tool_result fields). */
|
|
294
|
-
export function engineMessagesToAnthropic(messages) {
|
|
296
|
+
export function engineMessagesToAnthropic(messages, toolNameMap = createProviderToolNameMap([], messages)) {
|
|
295
297
|
const normalized = backfillEngineMessagesToolResults(messages);
|
|
296
298
|
return normalized.flatMap((m) => {
|
|
297
|
-
const translated = engineMessageToAnthropic(m);
|
|
299
|
+
const translated = engineMessageToAnthropic(m, { toolNameMap });
|
|
298
300
|
return translated.content.length > 0 ? [translated] : [];
|
|
299
301
|
});
|
|
300
302
|
}
|
|
@@ -302,11 +304,11 @@ export function engineMessagesToAnthropic(messages) {
|
|
|
302
304
|
* Messages for the Builder LLM gateway (Gemini-backed). Same Anthropic-shaped
|
|
303
305
|
* envelope, but every `tool_result` includes `tool_name` and `tool_input`.
|
|
304
306
|
*/
|
|
305
|
-
export function engineMessagesToBuilderGatewayAnthropic(messages) {
|
|
307
|
+
export function engineMessagesToBuilderGatewayAnthropic(messages, toolNameMap = createProviderToolNameMap([], messages)) {
|
|
306
308
|
const normalized = backfillEngineMessagesToolResults(messages);
|
|
307
|
-
return normalized.map((m) => engineMessageToAnthropic(m, { builderGateway: true }));
|
|
309
|
+
return normalized.map((m) => engineMessageToAnthropic(m, { builderGateway: true, toolNameMap }));
|
|
308
310
|
}
|
|
309
|
-
function enginePartToAnthropic(part, builderGateway) {
|
|
311
|
+
function enginePartToAnthropic(part, builderGateway, toolNameMap) {
|
|
310
312
|
switch (part.type) {
|
|
311
313
|
case "text":
|
|
312
314
|
return { type: "text", text: part.text };
|
|
@@ -339,24 +341,19 @@ function enginePartToAnthropic(part, builderGateway) {
|
|
|
339
341
|
return {
|
|
340
342
|
type: "tool_use",
|
|
341
343
|
id: part.id,
|
|
342
|
-
name: part.name,
|
|
344
|
+
name: toProviderToolName(part.name, toolNameMap),
|
|
343
345
|
input: part.input,
|
|
344
346
|
}; // tool_use is a ContentBlockParam in Anthropic SDK
|
|
345
347
|
case "tool-result": {
|
|
346
348
|
if (builderGateway) {
|
|
347
|
-
const tool_name = part.toolName.trim();
|
|
349
|
+
const tool_name = toProviderToolName(part.toolName.trim(), toolNameMap);
|
|
348
350
|
const tool_input = part.toolInput;
|
|
349
|
-
// Gateway degrade: the Builder gateway multiplexes to non-Anthropic
|
|
350
|
-
// models whose tool_result handling is string-only, so images are
|
|
351
|
-
// dropped here. The content string already carries a `[image: …]`
|
|
352
|
-
// note per image (appended by runToolCall), so the model still knows
|
|
353
|
-
// an image existed and any https URL survives.
|
|
354
351
|
return {
|
|
355
352
|
type: "tool_result",
|
|
356
353
|
tool_use_id: part.toolCallId,
|
|
357
354
|
tool_name,
|
|
358
355
|
tool_input,
|
|
359
|
-
content: part
|
|
356
|
+
content: toolResultContentToAnthropic(part),
|
|
360
357
|
...(part.isError ? { is_error: true } : {}),
|
|
361
358
|
};
|
|
362
359
|
}
|
|
@@ -417,7 +414,7 @@ function toolResultContentToAnthropic(part) {
|
|
|
417
414
|
// ---------------------------------------------------------------------------
|
|
418
415
|
// Anthropic.ContentBlock → EngineContentPart (from final message)
|
|
419
416
|
// ---------------------------------------------------------------------------
|
|
420
|
-
export function anthropicContentToEngine(content) {
|
|
417
|
+
export function anthropicContentToEngine(content, toolNameMap) {
|
|
421
418
|
return content
|
|
422
419
|
.map((block) => {
|
|
423
420
|
if (block.type === "text") {
|
|
@@ -427,7 +424,7 @@ export function anthropicContentToEngine(content) {
|
|
|
427
424
|
return {
|
|
428
425
|
type: "tool-call",
|
|
429
426
|
id: block.id,
|
|
430
|
-
name: block.name,
|
|
427
|
+
name: toEngineToolName(block.name, toolNameMap),
|
|
431
428
|
input: block.input,
|
|
432
429
|
};
|
|
433
430
|
}
|
|
@@ -471,13 +468,15 @@ export function createAnthropicChunkStreamState() {
|
|
|
471
468
|
* the engine, so omitting `state` simply drops the progress events without
|
|
472
469
|
* changing tool dispatch.
|
|
473
470
|
*/
|
|
474
|
-
export function anthropicChunkToEngineEvents(chunk, state) {
|
|
471
|
+
export function anthropicChunkToEngineEvents(chunk, state, toolNameMap) {
|
|
475
472
|
const events = [];
|
|
476
473
|
if (chunk.type === "content_block_start") {
|
|
477
474
|
const block = chunk.content_block;
|
|
478
475
|
if (block?.type === "tool_use") {
|
|
479
476
|
const id = typeof block.id === "string" ? block.id : undefined;
|
|
480
|
-
const name = typeof block.name === "string"
|
|
477
|
+
const name = typeof block.name === "string"
|
|
478
|
+
? toEngineToolName(block.name, toolNameMap)
|
|
479
|
+
: undefined;
|
|
481
480
|
if (state && typeof chunk.index === "number" && id && name) {
|
|
482
481
|
state.toolUseByIndex.set(chunk.index, { id, name });
|
|
483
482
|
}
|
|
@@ -1895,7 +1895,7 @@ function seedReadOnlyToolResultsFromJournal(journal, actions) {
|
|
|
1895
1895
|
})) {
|
|
1896
1896
|
continue;
|
|
1897
1897
|
}
|
|
1898
|
-
cache.set(toolCallCacheKey(entry.tool, entry.input), result);
|
|
1898
|
+
cache.set(toolCallCacheKey(entry.tool, entry.input), { content: result });
|
|
1899
1899
|
}
|
|
1900
1900
|
return cache;
|
|
1901
1901
|
}
|
|
@@ -1945,7 +1945,10 @@ function seedReadOnlyToolResultsFromHistory(messages, actions, seed) {
|
|
|
1945
1945
|
continue;
|
|
1946
1946
|
if (!isReusableReadOnlyToolResult(part))
|
|
1947
1947
|
continue;
|
|
1948
|
-
cache.set(toolCallCacheKey(call.name, call.input),
|
|
1948
|
+
cache.set(toolCallCacheKey(call.name, call.input), {
|
|
1949
|
+
content: part.content,
|
|
1950
|
+
...(part.images?.length ? { images: part.images } : {}),
|
|
1951
|
+
});
|
|
1949
1952
|
}
|
|
1950
1953
|
}
|
|
1951
1954
|
return cache;
|
|
@@ -2024,6 +2027,12 @@ function isReusableReadOnlyToolResult(part) {
|
|
|
2024
2027
|
const lower = part.content.trim().toLowerCase();
|
|
2025
2028
|
if (!lower)
|
|
2026
2029
|
return false;
|
|
2030
|
+
// The ledger persists only the note, not image bytes. Re-execute an image
|
|
2031
|
+
// read when its payload is unavailable so a continuation can restore vision.
|
|
2032
|
+
if (!part.images?.length &&
|
|
2033
|
+
/\[image(?:\s+#\d+|\s*:)[^\n\]]*\battached(?:\s+#\d+|:)[^\n\]]*\]/i.test(part.content)) {
|
|
2034
|
+
return false;
|
|
2035
|
+
}
|
|
2027
2036
|
return !(lower.startsWith("skipped duplicate read-only call to ") ||
|
|
2028
2037
|
lower.startsWith("invalid action parameters for ") ||
|
|
2029
2038
|
lower.startsWith("error running ") ||
|
|
@@ -4701,8 +4710,11 @@ export async function runAgentLoop(opts) {
|
|
|
4701
4710
|
const cacheKey = actionEntry.readOnly === true && actionEntry.dedupe !== false
|
|
4702
4711
|
? toolCallCacheKey(toolCall.name, toolCall.input)
|
|
4703
4712
|
: null;
|
|
4704
|
-
|
|
4705
|
-
|
|
4713
|
+
const cachedResult = cacheKey
|
|
4714
|
+
? readOnlyToolResultCache.get(cacheKey)
|
|
4715
|
+
: undefined;
|
|
4716
|
+
if (cacheKey && cachedResult) {
|
|
4717
|
+
const previousResult = cachedResult.content;
|
|
4706
4718
|
// `contextMessages` (not `messages`) is what the model actually sees
|
|
4707
4719
|
// this iteration — context-xray eviction/summarization and
|
|
4708
4720
|
// observational-memory trimming can drop the earlier result from
|
|
@@ -4744,6 +4756,9 @@ export async function runAgentLoop(opts) {
|
|
|
4744
4756
|
toolName: toolCall.name,
|
|
4745
4757
|
toolInput: wireToolInput,
|
|
4746
4758
|
content: result,
|
|
4759
|
+
...(cachedResult.images?.length
|
|
4760
|
+
? { images: cachedResult.images }
|
|
4761
|
+
: {}),
|
|
4747
4762
|
};
|
|
4748
4763
|
}
|
|
4749
4764
|
if (opts.executionMode === "plan" &&
|
|
@@ -5006,7 +5021,10 @@ export async function runAgentLoop(opts) {
|
|
|
5006
5021
|
if (!isError) {
|
|
5007
5022
|
noteToolCallSucceeded(actionEntry);
|
|
5008
5023
|
if (cacheKey) {
|
|
5009
|
-
readOnlyToolResultCache.set(cacheKey,
|
|
5024
|
+
readOnlyToolResultCache.set(cacheKey, {
|
|
5025
|
+
content: result,
|
|
5026
|
+
...(toolResultImages?.length ? { images: toolResultImages } : {}),
|
|
5027
|
+
});
|
|
5010
5028
|
}
|
|
5011
5029
|
else if (actionEntry.readOnly !== true) {
|
|
5012
5030
|
// A genuine write invalidates all cached reads. A dedupe:false
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
ok?: undefined;
|
|
17
16
|
error: string;
|
|
17
|
+
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -9,10 +9,44 @@
|
|
|
9
9
|
* Values are resolved server-side AFTER the model emits the tool call —
|
|
10
10
|
* the raw secret never enters the model's context.
|
|
11
11
|
*/
|
|
12
|
-
import { collectSecretValues, MAX_EXTENSION_PROXY_RESPONSE_SIZE, normalizeExtensionProxyMethod, readResponseTextWithLimit, redactSecrets, redactString, sanitizeOutboundHeaders, } from "./proxy-security.js";
|
|
12
|
+
import { collectSecretValues, MAX_EXTENSION_PROXY_RESPONSE_SIZE, normalizeExtensionProxyMethod, readResponseBytesWithLimit, readResponseTextWithLimit, redactSecrets, redactString, sanitizeOutboundHeaders, } from "./proxy-security.js";
|
|
13
13
|
import { createSsrfSafeDispatcher, isBlockedExtensionUrlWithDns, } from "./url-safety.js";
|
|
14
14
|
import { formatWebContentResult, parseWebContentSearchOptions, processWebContent, } from "./web-content.js";
|
|
15
15
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
16
|
+
const VISION_IMAGE_CONTENT_TYPES = new Set([
|
|
17
|
+
"image/jpeg",
|
|
18
|
+
"image/png",
|
|
19
|
+
"image/gif",
|
|
20
|
+
"image/webp",
|
|
21
|
+
]);
|
|
22
|
+
const CREDENTIAL_NAME_RE = /(?:^|[-_])(access[-_]?token|api[-_]?key|auth(?:orization)?|credential|expires?|key[-_]?pair[-_]?id|key|password|passwd|policy|secret|session|signature|sig|token|jwt|assertion)(?:$|[-_])/i;
|
|
23
|
+
function uint8ArrayToBase64(bytes) {
|
|
24
|
+
if (typeof Buffer !== "undefined")
|
|
25
|
+
return Buffer.from(bytes).toString("base64");
|
|
26
|
+
let binary = "";
|
|
27
|
+
const chunkSize = 0x8000;
|
|
28
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
29
|
+
binary += String.fromCharCode(...bytes.subarray(i, Math.min(i + chunkSize, bytes.length)));
|
|
30
|
+
}
|
|
31
|
+
return btoa(binary);
|
|
32
|
+
}
|
|
33
|
+
function hasCredentialBearingImageRequest(url, headers) {
|
|
34
|
+
let parsedUrl;
|
|
35
|
+
try {
|
|
36
|
+
parsedUrl = new URL(url);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (parsedUrl.username || parsedUrl.password)
|
|
42
|
+
return true;
|
|
43
|
+
if (parsedUrl.hash)
|
|
44
|
+
return true;
|
|
45
|
+
if (Object.keys(headers).some((name) => CREDENTIAL_NAME_RE.test(name.replace(/([a-z])([A-Z])/g, "$1-$2")))) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return [...parsedUrl.searchParams.keys()].some((name) => CREDENTIAL_NAME_RE.test(name.replace(/([a-z])([A-Z])/g, "$1-$2")));
|
|
49
|
+
}
|
|
16
50
|
/**
|
|
17
51
|
* Headers that mimic a current Chrome on macOS so anti-bot middleware (Cloudflare,
|
|
18
52
|
* PerimeterX, Akamai) treats the request as a real user. We only fill in fields
|
|
@@ -84,7 +118,7 @@ export function createFetchToolEntry(opts = {}) {
|
|
|
84
118
|
},
|
|
85
119
|
responseMode: {
|
|
86
120
|
type: "string",
|
|
87
|
-
description: "How to return the response. Default: auto (HTML pages become clean markdown; JSON/text stays raw). Use raw for exact bytes, markdown/text for extracted readable content, links for just links, metadata for page metadata, or matches with search.",
|
|
121
|
+
description: "How to return the response. Default: auto (HTML pages become clean markdown; JSON/text stays raw; successful public image responses are attached as vision context). Use raw for exact bytes, markdown/text for extracted readable content, links for just links, metadata for page metadata, or matches with search.",
|
|
88
122
|
enum: [
|
|
89
123
|
"auto",
|
|
90
124
|
"raw",
|
|
@@ -261,10 +295,46 @@ export function createFetchToolEntry(opts = {}) {
|
|
|
261
295
|
}
|
|
262
296
|
return `HTTP ${response.status} ${response.statusText}\n\nRedirect: ${redirectUrl ? redactString(redirectUrl, secretValues) : "(none)"}`;
|
|
263
297
|
}
|
|
264
|
-
|
|
298
|
+
const contentType = response.headers.get("content-type")?.split(";")[0].trim() ??
|
|
299
|
+
"text/plain";
|
|
300
|
+
// A fetched image is useful as model context, not as text decoded
|
|
301
|
+
// from arbitrary bytes. Pass bounded bytes through the well-known
|
|
302
|
+
// result-image field so vision providers never refetch this URL.
|
|
265
303
|
const saveToFilePath = typeof args.saveToFile === "string"
|
|
266
304
|
? args.saveToFile.trim()
|
|
267
305
|
: "";
|
|
306
|
+
const hasCredentials = allUsedKeys.length > 0 ||
|
|
307
|
+
hasCredentialBearingImageRequest(resolvedUrl, headers);
|
|
308
|
+
if (!saveToFilePath &&
|
|
309
|
+
method === "GET" &&
|
|
310
|
+
response.ok &&
|
|
311
|
+
VISION_IMAGE_CONTENT_TYPES.has(contentType.toLowerCase()) &&
|
|
312
|
+
resolvedUrl.startsWith("https://") &&
|
|
313
|
+
!hasCredentials) {
|
|
314
|
+
const imageBody = await readResponseBytesWithLimit(response, MAX_EXTENSION_PROXY_RESPONSE_SIZE);
|
|
315
|
+
if (imageBody.truncated) {
|
|
316
|
+
return `HTTP ${response.status} ${response.statusText}\n\nContent-Type: ${contentType}\n\nImage response too large to attach (${imageBody.size} bytes, max ${MAX_EXTENSION_PROXY_RESPONSE_SIZE}).`;
|
|
317
|
+
}
|
|
318
|
+
console.log("[fetch-tool] " +
|
|
319
|
+
method +
|
|
320
|
+
" image → " +
|
|
321
|
+
response.status +
|
|
322
|
+
" (" +
|
|
323
|
+
elapsed +
|
|
324
|
+
"ms, vision image)");
|
|
325
|
+
return {
|
|
326
|
+
status: response.status,
|
|
327
|
+
statusText: response.statusText,
|
|
328
|
+
contentType,
|
|
329
|
+
url: resolvedUrl,
|
|
330
|
+
_agentImages: [
|
|
331
|
+
{
|
|
332
|
+
data: uint8ArrayToBase64(imageBody.bytes),
|
|
333
|
+
mediaType: contentType.toLowerCase(),
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
268
338
|
let body;
|
|
269
339
|
try {
|
|
270
340
|
// When saving to file allow larger reads (20MB), otherwise cap at proxy limit.
|
|
@@ -278,8 +348,6 @@ export function createFetchToolEntry(opts = {}) {
|
|
|
278
348
|
body = "(could not read response body)";
|
|
279
349
|
}
|
|
280
350
|
body = redactString(body, secretValues);
|
|
281
|
-
const contentType = response.headers.get("content-type")?.split(";")[0].trim() ??
|
|
282
|
-
"text/plain";
|
|
283
351
|
let displayBody;
|
|
284
352
|
let processedMode = "raw";
|
|
285
353
|
try {
|
|
@@ -4,6 +4,11 @@ export declare function sanitizeOutboundHeaders(value: unknown): Record<string,
|
|
|
4
4
|
export declare function collectSecretValues(...groups: Array<Array<string> | undefined>): string[];
|
|
5
5
|
export declare function redactSecrets<T>(value: T, secretValues: string[]): T;
|
|
6
6
|
export declare function redactString(text: string, secretValues: string[]): string;
|
|
7
|
+
export declare function readResponseBytesWithLimit(response: Response, maxBytes?: number): Promise<{
|
|
8
|
+
bytes: Uint8Array;
|
|
9
|
+
truncated: boolean;
|
|
10
|
+
size: number;
|
|
11
|
+
}>;
|
|
7
12
|
export declare function readResponseTextWithLimit(response: Response, maxBytes?: number): Promise<{
|
|
8
13
|
text: string;
|
|
9
14
|
truncated: boolean;
|
|
@@ -99,11 +99,12 @@ function redactionCandidates(secret) {
|
|
|
99
99
|
catch { }
|
|
100
100
|
return [...candidates].sort((a, b) => b.length - a.length);
|
|
101
101
|
}
|
|
102
|
-
export async function
|
|
102
|
+
export async function readResponseBytesWithLimit(response, maxBytes = MAX_EXTENSION_PROXY_RESPONSE_SIZE) {
|
|
103
103
|
const contentLength = response.headers.get("content-length");
|
|
104
104
|
if (contentLength && Number(contentLength) > maxBytes) {
|
|
105
|
+
await response.body?.cancel().catch(() => { });
|
|
105
106
|
return {
|
|
106
|
-
|
|
107
|
+
bytes: new Uint8Array(),
|
|
107
108
|
truncated: true,
|
|
108
109
|
size: Number(contentLength),
|
|
109
110
|
};
|
|
@@ -113,13 +114,13 @@ export async function readResponseTextWithLimit(response, maxBytes = MAX_EXTENSI
|
|
|
113
114
|
const buffer = await response.arrayBuffer();
|
|
114
115
|
if (buffer.byteLength > maxBytes) {
|
|
115
116
|
return {
|
|
116
|
-
|
|
117
|
+
bytes: new Uint8Array(),
|
|
117
118
|
truncated: true,
|
|
118
119
|
size: buffer.byteLength,
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
return {
|
|
122
|
-
|
|
123
|
+
bytes: new Uint8Array(buffer),
|
|
123
124
|
truncated: false,
|
|
124
125
|
size: buffer.byteLength,
|
|
125
126
|
};
|
|
@@ -135,23 +136,30 @@ export async function readResponseTextWithLimit(response, maxBytes = MAX_EXTENSI
|
|
|
135
136
|
total += value.byteLength;
|
|
136
137
|
if (total > maxBytes) {
|
|
137
138
|
await reader.cancel().catch(() => { });
|
|
138
|
-
return {
|
|
139
|
-
text: `(response truncated - ${total} bytes, max ${maxBytes})`,
|
|
140
|
-
truncated: true,
|
|
141
|
-
size: total,
|
|
142
|
-
};
|
|
139
|
+
return { bytes: new Uint8Array(), truncated: true, size: total };
|
|
143
140
|
}
|
|
144
141
|
chunks.push(value);
|
|
145
142
|
}
|
|
146
|
-
const
|
|
143
|
+
const bytes = new Uint8Array(total);
|
|
147
144
|
let offset = 0;
|
|
148
145
|
for (const chunk of chunks) {
|
|
149
|
-
|
|
146
|
+
bytes.set(chunk, offset);
|
|
150
147
|
offset += chunk.byteLength;
|
|
151
148
|
}
|
|
149
|
+
return { bytes, truncated: false, size: total };
|
|
150
|
+
}
|
|
151
|
+
export async function readResponseTextWithLimit(response, maxBytes = MAX_EXTENSION_PROXY_RESPONSE_SIZE) {
|
|
152
|
+
const result = await readResponseBytesWithLimit(response, maxBytes);
|
|
153
|
+
if (result.truncated) {
|
|
154
|
+
const contentLength = response.headers.get("content-length");
|
|
155
|
+
const text = contentLength && Number(contentLength) > maxBytes
|
|
156
|
+
? `(response too large - ${contentLength} bytes, max ${maxBytes})`
|
|
157
|
+
: `(response truncated - ${result.size} bytes, max ${maxBytes})`;
|
|
158
|
+
return { text, truncated: true, size: result.size };
|
|
159
|
+
}
|
|
152
160
|
return {
|
|
153
|
-
text: new TextDecoder().decode(
|
|
161
|
+
text: new TextDecoder().decode(result.bytes),
|
|
154
162
|
truncated: false,
|
|
155
|
-
size:
|
|
163
|
+
size: result.size,
|
|
156
164
|
};
|
|
157
165
|
}
|