@springbrand/agent-runtime 0.2.0-alpha.30 → 0.2.0-alpha.32
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/package.json +1 -1
- package/src/kernel/bindings.ts +1 -1
- package/src/lib/prompt.ts +16 -10
- package/src/pi/message/contract.ts +1 -1
- package/src/pi/runtime-adapter/assembly.ts +1 -0
- package/src/pi/runtime-adapter/index.ts +7 -0
- package/src/pi/runtime-adapter/models.ts +9 -10
- package/src/pi/runtime-adapter/openrouter-messages.ts +127 -0
- package/src/pi/runtime-adapter/test-fixtures/openrouter-tool-search.sse +45 -0
- package/src/pi/tool/compiler.ts +3 -0
- package/src/pi/tool/core-host.ts +6 -2
- package/src/pi/tool/web-search/api.ts +1 -1
- package/src/runtime-assembler.ts +17 -3
- package/src/runtime-definition.ts +2 -3
- package/src/runtime.ts +12 -2
- package/src/tool-registry.ts +1 -0
package/package.json
CHANGED
package/src/kernel/bindings.ts
CHANGED
package/src/lib/prompt.ts
CHANGED
|
@@ -18,8 +18,7 @@ export const PERSONA =
|
|
|
18
18
|
export const RUNTIME =
|
|
19
19
|
"Runtime: this agent runs on Cloudflare Workers. When execute is present, its Code Mode Dynamic Worker is your " +
|
|
20
20
|
"instrument — code there runs with outbound network access (fetch), your " +
|
|
21
|
-
"workspace filesystem (state.*), and
|
|
22
|
-
"in one execute instead of making consecutive top-level Tool calls. Use it for raw or customized HTTP " +
|
|
21
|
+
"workspace filesystem (state.*), and the tools.* methods listed in its own description. Use it for raw or customized HTTP " +
|
|
23
22
|
"requests, parsing a payload, hitting several known endpoints, or computing over a file. Write plain " +
|
|
24
23
|
"JavaScript — the sandbox evaluates " +
|
|
25
24
|
"it directly, so TypeScript type annotations (`: number`, `as Type`) are a syntax error, and there " +
|
|
@@ -33,7 +32,9 @@ export const BEHAVIOR =
|
|
|
33
32
|
"When uncertain, investigate before answering rather than guess or confirm a belief. Match response " +
|
|
34
33
|
"length to the task — a short question gets a short answer; skip filler preamble and don't restate " +
|
|
35
34
|
"what you just did. Be proactive when asked to *do* something (take the needed follow-up actions too), " +
|
|
36
|
-
"but when the user only asks *how* to approach something, answer first and don't jump into changes."
|
|
35
|
+
"but when the user only asks *how* to approach something, answer first and don't jump into changes. " +
|
|
36
|
+
"A failed Tool call produced no requested result. Never infer completion or fabricate URLs, files, identifiers, or business results " +
|
|
37
|
+
"from a failed call's arguments or identifiers; correct the Tool route or report the failure.";
|
|
37
38
|
|
|
38
39
|
// The main Agent alone owns user-visible planning.
|
|
39
40
|
export const PLANNING =
|
|
@@ -43,13 +44,18 @@ export const PLANNING =
|
|
|
43
44
|
"one step in_progress at a time and don't batch completions. Do not make a plan for a single trivial " +
|
|
44
45
|
"step or a purely conversational reply — just do it.";
|
|
45
46
|
|
|
47
|
+
const TOOL_ROUTING =
|
|
48
|
+
"Tools: execute can call only the tools.* methods explicitly listed in its description; that list is exhaustive. Never guess a tools.* method. " +
|
|
49
|
+
"If a required top-level Tool is not visible, do not use execute. Call top-level Tool Search with that exact name, then call the discovered Tool directly. " +
|
|
50
|
+
"codemode.search searches only methods already installed inside execute; it cannot discover deferred top-level Tools.";
|
|
51
|
+
|
|
46
52
|
// Tool-selection guidance mirrors the actual approval and network boundaries.
|
|
47
53
|
export const TOOLS =
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent calls. " +
|
|
52
|
-
"
|
|
54
|
+
TOOL_ROUTING +
|
|
55
|
+
" Relatedness, repetition, or multiple calls never overrides this availability rule. " +
|
|
56
|
+
"When every required Tool is available inside execute, use one execute for repeated or related calls, branching, or repetition. Inside execute, " +
|
|
57
|
+
"use state.* for workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent calls. " +
|
|
58
|
+
"Top-level-only Tools remain Direct even when called repeatedly. " +
|
|
53
59
|
"For web tasks, use web_search for web discovery, current facts, cited research, " +
|
|
54
60
|
"and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
|
|
55
61
|
"API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
|
|
@@ -63,7 +69,7 @@ export const TOOLS =
|
|
|
63
69
|
"When execute is present, make related file changes in one execute with state.*; when execute is absent, use one write or bash operation instead of serial edits. " +
|
|
64
70
|
"Do not re-plan or explain between consecutive tool calls. When a run is within the last five model turns, stop expanding scope and " +
|
|
65
71
|
"prioritize verification, saving durable results, and the final response. " +
|
|
66
|
-
"When related Tool calls can run independently and execute is present, run them inside that execute rather than as parallel top-level calls; " +
|
|
72
|
+
"When related Tool calls can run independently, all are available inside execute, and execute is present, run them inside that execute rather than as parallel top-level calls; " +
|
|
67
73
|
"only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
|
|
68
74
|
"code, cite it as file_path:line_number.";
|
|
69
75
|
|
|
@@ -164,5 +170,5 @@ export function assembleSystemPrompt(base?: string | null): string {
|
|
|
164
170
|
* Agent、Runtime 和 Memory 的术语见 `src/index.ts`。
|
|
165
171
|
*/
|
|
166
172
|
export function assembleSubagentPrompt(persona: string): string {
|
|
167
|
-
return [persona, RUNTIME, BEHAVIOR].join("\n\n");
|
|
173
|
+
return [persona, RUNTIME, BEHAVIOR, TOOL_ROUTING].join("\n\n");
|
|
168
174
|
}
|
|
@@ -4,7 +4,7 @@ export type UIChatTrigger = "submit-message" | "regenerate-message";
|
|
|
4
4
|
|
|
5
5
|
/** A user-selected Runtime capability persisted with the visible UIMessage. */
|
|
6
6
|
export interface RequestedCapability {
|
|
7
|
-
readonly kind: "skill" | "plan";
|
|
7
|
+
readonly kind: "skill" | "tool" | "plan";
|
|
8
8
|
readonly name: string;
|
|
9
9
|
readonly label: string;
|
|
10
10
|
}
|
|
@@ -256,6 +256,7 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
|
|
|
256
256
|
...(candidate.alwaysRequiresApproval
|
|
257
257
|
? { alwaysRequiresApproval: true }
|
|
258
258
|
: {}),
|
|
259
|
+
...(candidate.deferLoading ? { deferLoading: true } : {}),
|
|
259
260
|
...(candidate.source ? { source: candidate.source } : {}),
|
|
260
261
|
retry: piToolRetryPolicy(candidate),
|
|
261
262
|
}))
|
|
@@ -18,6 +18,7 @@ export {
|
|
|
18
18
|
import {
|
|
19
19
|
pinPiRuntime,
|
|
20
20
|
preparePiRuntime,
|
|
21
|
+
readPreparedPiRuntime,
|
|
21
22
|
type PinPiRuntimeOptions,
|
|
22
23
|
type PinnedPiRuntime,
|
|
23
24
|
type PreparePiRuntimeOptions,
|
|
@@ -143,6 +144,12 @@ export class PiRuntimeAdapter {
|
|
|
143
144
|
return preparePiRuntime(options, this.owner);
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
hasTool(prepared: PreparedPiRuntime, name: string): boolean {
|
|
148
|
+
return readPreparedPiRuntime(prepared, this.owner).candidates.some(
|
|
149
|
+
(candidate) => candidate.tool.name === name,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
146
153
|
/**
|
|
147
154
|
* 用已经通过加载检查的快照启用当前部署声明的模型目录。
|
|
148
155
|
*
|
|
@@ -10,9 +10,7 @@ import {
|
|
|
10
10
|
type Model,
|
|
11
11
|
type MutableModels,
|
|
12
12
|
} from "@earendil-works/pi-ai";
|
|
13
|
-
import {
|
|
14
|
-
anthropicMessagesApi,
|
|
15
|
-
} from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
13
|
+
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
16
14
|
import {
|
|
17
15
|
openAICompletionsApi,
|
|
18
16
|
} from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
@@ -48,10 +46,11 @@ import type {
|
|
|
48
46
|
RuntimeModelProtocol,
|
|
49
47
|
RuntimeProviderPort,
|
|
50
48
|
} from "../../kernel/bindings";
|
|
49
|
+
import { openRouterMessagesApi } from "./openrouter-messages";
|
|
51
50
|
|
|
52
51
|
const CATALOGS = {
|
|
53
52
|
"openai-chat": openaiProvider().getModels(),
|
|
54
|
-
"openrouter-
|
|
53
|
+
"openrouter-messages": openrouterProvider().getModels(),
|
|
55
54
|
"anthropic-messages": anthropicProvider().getModels(),
|
|
56
55
|
"google-generative-ai": googleProvider().getModels(),
|
|
57
56
|
"openai-codex-responses": openaiCodexProvider().getModels(),
|
|
@@ -560,8 +559,8 @@ function providerId(endpoint: RuntimeModelEndpoint, index: number): string {
|
|
|
560
559
|
function apiFor(protocol: RuntimeModelProtocol): Api {
|
|
561
560
|
switch (protocol) {
|
|
562
561
|
case "openai-chat":
|
|
563
|
-
case "openrouter-chat":
|
|
564
562
|
return "openai-completions";
|
|
563
|
+
case "openrouter-messages":
|
|
565
564
|
case "anthropic-messages":
|
|
566
565
|
return "anthropic-messages";
|
|
567
566
|
case "google-generative-ai":
|
|
@@ -574,8 +573,9 @@ function apiFor(protocol: RuntimeModelProtocol): Api {
|
|
|
574
573
|
function piApiFor(protocol: RuntimeModelProtocol) {
|
|
575
574
|
switch (protocol) {
|
|
576
575
|
case "openai-chat":
|
|
577
|
-
case "openrouter-chat":
|
|
578
576
|
return openAICompletionsApi();
|
|
577
|
+
case "openrouter-messages":
|
|
578
|
+
return openRouterMessagesApi();
|
|
579
579
|
case "anthropic-messages":
|
|
580
580
|
return anthropicMessagesApi();
|
|
581
581
|
case "google-generative-ai":
|
|
@@ -619,12 +619,11 @@ function configuredModel(
|
|
|
619
619
|
api: apiFor(endpoint.protocol),
|
|
620
620
|
provider: providerId(endpoint, index),
|
|
621
621
|
baseUrl: endpoint.baseURL,
|
|
622
|
-
...(endpoint.protocol === "openrouter-
|
|
622
|
+
...(endpoint.protocol === "openrouter-messages"
|
|
623
623
|
? {
|
|
624
624
|
compat: {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
sessionAffinityFormat: "openrouter" as const,
|
|
625
|
+
supportsEagerToolInputStreaming: false,
|
|
626
|
+
supportsToolReferences: true,
|
|
628
627
|
...(openRouterProviderPin
|
|
629
628
|
? {
|
|
630
629
|
openRouterRouting: {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Context,
|
|
3
|
+
Model,
|
|
4
|
+
ProviderStreams,
|
|
5
|
+
StreamOptions,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
7
|
+
import {
|
|
8
|
+
anthropicMessagesApi,
|
|
9
|
+
} from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
10
|
+
|
|
11
|
+
type Payload = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
function record(value: unknown): Payload | undefined {
|
|
14
|
+
return value !== null && typeof value === "object"
|
|
15
|
+
? value as Payload
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function decoratePayload(
|
|
20
|
+
payload: unknown,
|
|
21
|
+
model: Model<"anthropic-messages">,
|
|
22
|
+
context: Context,
|
|
23
|
+
sessionId?: string,
|
|
24
|
+
): unknown {
|
|
25
|
+
const body = record(payload);
|
|
26
|
+
if (!body) throw new Error("OpenRouter Messages payload must be an object");
|
|
27
|
+
|
|
28
|
+
const deferredNames = new Set(
|
|
29
|
+
context.tools?.filter((tool) => tool.deferLoading).map((tool) => tool.name),
|
|
30
|
+
);
|
|
31
|
+
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
32
|
+
const immediate: unknown[] = [];
|
|
33
|
+
const deferred: unknown[] = [];
|
|
34
|
+
for (const tool of tools) {
|
|
35
|
+
const definition = record(tool);
|
|
36
|
+
if (definition && typeof definition.name === "string" && deferredNames.has(definition.name)) {
|
|
37
|
+
deferred.push({ ...definition, defer_loading: true });
|
|
38
|
+
} else {
|
|
39
|
+
immediate.push(tool);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (deferred.length > 0) {
|
|
44
|
+
const choice = record(body.tool_choice);
|
|
45
|
+
if (
|
|
46
|
+
body.tool_choice !== undefined &&
|
|
47
|
+
body.tool_choice !== "auto" &&
|
|
48
|
+
choice?.type !== "auto"
|
|
49
|
+
) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"tool_choice conflicts with openrouter:tool_search; omit it or use auto",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...body,
|
|
58
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
59
|
+
...(model.compat?.openRouterRouting
|
|
60
|
+
? { provider: model.compat.openRouterRouting }
|
|
61
|
+
: {}),
|
|
62
|
+
...(deferred.length > 0
|
|
63
|
+
? {
|
|
64
|
+
tools: [
|
|
65
|
+
{
|
|
66
|
+
type: "openrouter:tool_search",
|
|
67
|
+
parameters: { max_results: 5 },
|
|
68
|
+
},
|
|
69
|
+
...immediate,
|
|
70
|
+
...deferred,
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
: {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function openRouterOptions(
|
|
78
|
+
model: Model<"anthropic-messages">,
|
|
79
|
+
context: Context,
|
|
80
|
+
options: StreamOptions | undefined,
|
|
81
|
+
): StreamOptions {
|
|
82
|
+
const apiKey = options?.apiKey;
|
|
83
|
+
const headers = Object.fromEntries(
|
|
84
|
+
Object.entries(options?.headers ?? {}).filter(([name]) =>
|
|
85
|
+
!["authorization", "x-api-key"].includes(name.toLowerCase())
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
return {
|
|
89
|
+
...options,
|
|
90
|
+
apiKey: undefined,
|
|
91
|
+
headers: {
|
|
92
|
+
...headers,
|
|
93
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
94
|
+
"x-api-key": null,
|
|
95
|
+
},
|
|
96
|
+
onPayload: async (payload, activeModel) => {
|
|
97
|
+
const callerPayload = await options?.onPayload?.(payload, activeModel) ?? payload;
|
|
98
|
+
return decoratePayload(callerPayload, model, context, options?.sessionId);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function openRouterMessagesApi(): ProviderStreams {
|
|
104
|
+
const anthropic = anthropicMessagesApi();
|
|
105
|
+
return {
|
|
106
|
+
stream: (model, context, options) =>
|
|
107
|
+
anthropic.stream(
|
|
108
|
+
model,
|
|
109
|
+
context,
|
|
110
|
+
openRouterOptions(
|
|
111
|
+
model as Model<"anthropic-messages">,
|
|
112
|
+
context,
|
|
113
|
+
options,
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
streamSimple: (model, context, options) =>
|
|
117
|
+
anthropic.streamSimple(
|
|
118
|
+
model,
|
|
119
|
+
context,
|
|
120
|
+
openRouterOptions(
|
|
121
|
+
model as Model<"anthropic-messages">,
|
|
122
|
+
context,
|
|
123
|
+
options,
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
event: message_start
|
|
2
|
+
data: {"type":"message_start","message":{"id":"msg_tool_search","type":"message","role":"assistant","model":"anthropic/claude-sonnet-5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":12,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}
|
|
3
|
+
|
|
4
|
+
event: content_block_start
|
|
5
|
+
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}}
|
|
6
|
+
|
|
7
|
+
event: content_block_delta
|
|
8
|
+
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"I need the hidden status tool."}}
|
|
9
|
+
|
|
10
|
+
event: content_block_delta
|
|
11
|
+
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"signed-thinking"}}
|
|
12
|
+
|
|
13
|
+
event: content_block_stop
|
|
14
|
+
data: {"type":"content_block_stop","index":0}
|
|
15
|
+
|
|
16
|
+
event: content_block_start
|
|
17
|
+
data: {"type":"content_block_start","index":1,"content_block":{"type":"server_tool_use","id":"srvtoolu_search_1","name":"tool_search_tool_regex","input":{}}}
|
|
18
|
+
|
|
19
|
+
event: content_block_delta
|
|
20
|
+
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"pattern\":\"status\"}"}}
|
|
21
|
+
|
|
22
|
+
event: content_block_stop
|
|
23
|
+
data: {"type":"content_block_stop","index":1}
|
|
24
|
+
|
|
25
|
+
event: content_block_start
|
|
26
|
+
data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_search_tool_result","tool_use_id":"srvtoolu_search_1","content":{"type":"tool_search_tool_search_result","tool_references":[{"type":"tool_reference","tool_name":"deferred_status"}]}}}
|
|
27
|
+
|
|
28
|
+
event: content_block_stop
|
|
29
|
+
data: {"type":"content_block_stop","index":2}
|
|
30
|
+
|
|
31
|
+
event: content_block_start
|
|
32
|
+
data: {"type":"content_block_start","index":3,"content_block":{"type":"tool_use","id":"toolu_status_1","name":"deferred_status","input":{}}}
|
|
33
|
+
|
|
34
|
+
event: content_block_delta
|
|
35
|
+
data: {"type":"content_block_delta","index":3,"delta":{"type":"input_json_delta","partial_json":"{\"scope\":\"runtime\"}"}}
|
|
36
|
+
|
|
37
|
+
event: content_block_stop
|
|
38
|
+
data: {"type":"content_block_stop","index":3}
|
|
39
|
+
|
|
40
|
+
event: message_delta
|
|
41
|
+
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":24}}
|
|
42
|
+
|
|
43
|
+
event: message_stop
|
|
44
|
+
data: {"type":"message_stop"}
|
|
45
|
+
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface PiToolInteractionSpec {
|
|
|
46
46
|
export interface PiToolCandidate {
|
|
47
47
|
readonly owner: string;
|
|
48
48
|
readonly tool: AgentTool<any, any>;
|
|
49
|
+
/** @internal Send the complete schema but hide it until provider Tool Search finds it. */
|
|
50
|
+
readonly deferLoading?: true;
|
|
49
51
|
/** Keep this Tool Direct-only instead of also offering it through Code Mode. */
|
|
50
52
|
readonly direct?: true;
|
|
51
53
|
/** Offer this Tool only through Code Mode, never as a top-level Tool. */
|
|
@@ -314,6 +316,7 @@ function governedTool(
|
|
|
314
316
|
|
|
315
317
|
return {
|
|
316
318
|
...candidate.tool,
|
|
319
|
+
...(candidate.deferLoading ? { deferLoading: true as const } : {}),
|
|
317
320
|
// 执行一次完整的受治理 Pi 工具调用。
|
|
318
321
|
// Pi 工具循环选中编译后的工具时调用,调用方应传入稳定 toolCall id 供结算去重。
|
|
319
322
|
// 输出限额必须发生在 settle 之前,否则 Durable Object 会持久化一份与模型最终所见不同的过大结果。
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -18,6 +18,10 @@ import type {
|
|
|
18
18
|
// 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
|
|
19
19
|
|
|
20
20
|
const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
|
|
21
|
+
const CODEMODE_TOOL_SURFACE_GUIDANCE =
|
|
22
|
+
"The `tools.*` Available list below is exhaustive, not examples. Inside execute, call only methods in that list; " +
|
|
23
|
+
"never guess or construct a `tools.*` method name. If a required Tool is absent, leave execute and call it at the top level, " +
|
|
24
|
+
"using top-level Tool Search first when it is deferred. `codemode.search` cannot add methods to `tools.*`.";
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* 宿主的 Browser Rendering 绑定,只取本仓真正用到的那一面。
|
|
@@ -183,10 +187,10 @@ export function createWorkspaceCodeExecutionFactory(options: {
|
|
|
183
187
|
const port = toCodeExecutionPort(tool, "execute");
|
|
184
188
|
return {
|
|
185
189
|
...port,
|
|
186
|
-
description: port.description.replace(
|
|
190
|
+
description: `${CODEMODE_TOOL_SURFACE_GUIDANCE}\n\n${port.description.replace(
|
|
187
191
|
"- Do not use `fetch` — use connector SDKs.",
|
|
188
192
|
"- Raw `fetch` is available inside `codemode.step(...)`; prefer connector SDKs when one owns the target.",
|
|
189
|
-
)
|
|
193
|
+
)}`,
|
|
190
194
|
};
|
|
191
195
|
},
|
|
192
196
|
};
|
|
@@ -1119,7 +1119,7 @@ export interface WebSearchOptions {
|
|
|
1119
1119
|
|
|
1120
1120
|
function apiFor(endpoint: RuntimeModelEndpoint): NativeApi {
|
|
1121
1121
|
switch (endpoint.protocol) {
|
|
1122
|
-
case "openrouter-
|
|
1122
|
+
case "openrouter-messages":
|
|
1123
1123
|
return "openrouter-responses";
|
|
1124
1124
|
case "google-generative-ai":
|
|
1125
1125
|
return "google-generative-ai";
|
package/src/runtime-assembler.ts
CHANGED
|
@@ -191,9 +191,17 @@ async function createToolSurface(
|
|
|
191
191
|
const deny = new Set(input.policy?.denyPolicy?.deny ?? []);
|
|
192
192
|
const allowsTool = input.policy?.allowsTool;
|
|
193
193
|
const allowsExtension = input.policy?.allowsExtension;
|
|
194
|
+
const defersTool = input.policy?.defersTool;
|
|
194
195
|
const visible = (candidate: PiToolCandidate) =>
|
|
195
196
|
!deny.has(candidate.tool.name) &&
|
|
196
197
|
allowsTool?.(candidate.tool.name) !== false;
|
|
198
|
+
const classify = (candidate: PiToolCandidate): PiToolCandidate =>
|
|
199
|
+
Object.freeze({
|
|
200
|
+
...candidate,
|
|
201
|
+
...(defersTool?.(candidate.tool.name) === true
|
|
202
|
+
? { deferLoading: true as const }
|
|
203
|
+
: { deferLoading: undefined }),
|
|
204
|
+
});
|
|
197
205
|
// 平台没有浏览器能力时注册空集:宁可没有这个 Tool,也不注册一个必然失败的 Tool 误导模型。
|
|
198
206
|
// `create()` 推迟到确认这个名字真的可见之后,被 deny 的装配不白建一个浏览器连接器。
|
|
199
207
|
// `direct` 使它只走 Direct 调用,不再被并进 `execute` 的工具集——两个 Code Mode 类工具互相嵌套
|
|
@@ -259,7 +267,7 @@ async function createToolSurface(
|
|
|
259
267
|
deny.has("execute") ||
|
|
260
268
|
allowsTool?.("execute") === false
|
|
261
269
|
) {
|
|
262
|
-
return Object.freeze(directVisible);
|
|
270
|
+
return Object.freeze(directVisible.map(classify));
|
|
263
271
|
}
|
|
264
272
|
const mergeable = finalized.filter(
|
|
265
273
|
(candidate) =>
|
|
@@ -280,7 +288,7 @@ async function createToolSurface(
|
|
|
280
288
|
"Use a loop or Promise.all when copying multiple resources.\n\n"
|
|
281
289
|
: "";
|
|
282
290
|
return Object.freeze([
|
|
283
|
-
|
|
291
|
+
classify({
|
|
284
292
|
...codeExecutionPiToolCandidate(
|
|
285
293
|
{
|
|
286
294
|
...codeExecution,
|
|
@@ -289,7 +297,7 @@ async function createToolSurface(
|
|
|
289
297
|
),
|
|
290
298
|
codeExecutionTools,
|
|
291
299
|
}),
|
|
292
|
-
...directVisible,
|
|
300
|
+
...directVisible.map(classify),
|
|
293
301
|
]);
|
|
294
302
|
},
|
|
295
303
|
}),
|
|
@@ -831,12 +839,18 @@ export async function assembleRuntimeSnapshot<
|
|
|
831
839
|
...(settings.toolPolicy?.allowsExtension
|
|
832
840
|
? { allowsExtension: settings.toolPolicy.allowsExtension }
|
|
833
841
|
: {}),
|
|
842
|
+
...(settings.toolPolicy?.defersTool
|
|
843
|
+
? { defersTool: settings.toolPolicy.defersTool }
|
|
844
|
+
: {}),
|
|
834
845
|
...(toolAssembly.surfacePolicy?.allowsTool
|
|
835
846
|
? { allowsTool: toolAssembly.surfacePolicy.allowsTool }
|
|
836
847
|
: {}),
|
|
837
848
|
...(toolAssembly.surfacePolicy?.allowsExtension
|
|
838
849
|
? { allowsExtension: toolAssembly.surfacePolicy.allowsExtension }
|
|
839
850
|
: {}),
|
|
851
|
+
...(toolAssembly.surfacePolicy?.defersTool
|
|
852
|
+
? { defersTool: toolAssembly.surfacePolicy.defersTool }
|
|
853
|
+
: {}),
|
|
840
854
|
}
|
|
841
855
|
: undefined;
|
|
842
856
|
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
RuntimeAgentPlanningContext,
|
|
29
29
|
} from "./runtime-agent-context";
|
|
30
30
|
import type { AgentTelemetryBinding } from "./telemetry/contract";
|
|
31
|
+
import type { ToolSurfaceSelectionPolicy } from "./tool-registry";
|
|
31
32
|
|
|
32
33
|
export interface RuntimeProfileContribution {
|
|
33
34
|
readonly model: string;
|
|
@@ -48,10 +49,8 @@ export interface RuntimeExtensionContribution {
|
|
|
48
49
|
readonly extension: RuntimeExtensionConfig;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export interface RuntimeToolSurfacePolicy {
|
|
52
|
+
export interface RuntimeToolSurfacePolicy extends ToolSurfaceSelectionPolicy {
|
|
52
53
|
readonly denyPolicy?: RuntimeDenyPolicy;
|
|
53
|
-
readonly allowsTool?: (name: string) => boolean;
|
|
54
|
-
readonly allowsExtension?: (extension: RuntimeExtensionConfig) => boolean;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
export type { RuntimeAgentContext, RuntimeAssemblyContext } from "./runtime-agent-context";
|
package/src/runtime.ts
CHANGED
|
@@ -147,7 +147,7 @@ export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
|
|
|
147
147
|
// 那种情况下 5 次 × 240s ≈ 20 分钟的空转纯属折磨用户。3 次约 12 分钟封顶。
|
|
148
148
|
export const CHAT_STALL_MAX_ATTEMPTS = 3;
|
|
149
149
|
const CHAT_RECOVERY_TERMINAL_MESSAGE =
|
|
150
|
-
"
|
|
150
|
+
"Recovery failed after multiple attempts. This generation has stopped and your progress has been saved. Send a new message to continue.";
|
|
151
151
|
|
|
152
152
|
type RuntimeEventOutboxPayload =
|
|
153
153
|
| { readonly type: "model-usage"; readonly event: RuntimeModelUsageEvent }
|
|
@@ -360,7 +360,7 @@ function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
|
|
|
360
360
|
}
|
|
361
361
|
const capability = value as Record<string, unknown>;
|
|
362
362
|
if (
|
|
363
|
-
(capability.kind !== "skill" && capability.kind !== "plan") ||
|
|
363
|
+
(capability.kind !== "skill" && capability.kind !== "tool" && capability.kind !== "plan") ||
|
|
364
364
|
typeof capability.name !== "string" ||
|
|
365
365
|
!capability.name.trim() ||
|
|
366
366
|
typeof capability.label !== "string" ||
|
|
@@ -1352,6 +1352,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
1352
1352
|
);
|
|
1353
1353
|
continue;
|
|
1354
1354
|
}
|
|
1355
|
+
if (capability.kind === "tool") {
|
|
1356
|
+
if (!this.pi.hasTool(this.preparedPi(), capability.name)) {
|
|
1357
|
+
throw new Error(`Requested Tool is not installed: ${capability.name}`);
|
|
1358
|
+
}
|
|
1359
|
+
context.push(
|
|
1360
|
+
`requested capability: tool/${capability.name}`,
|
|
1361
|
+
`required action: call tool "${capability.name}" when handling the task`,
|
|
1362
|
+
);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1355
1365
|
if (capability.name !== "plan") {
|
|
1356
1366
|
throw new Error(`Unknown plan capability: ${capability.name}`);
|
|
1357
1367
|
}
|
package/src/tool-registry.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type {
|
|
|
27
27
|
export interface ToolSurfaceSelectionPolicy {
|
|
28
28
|
readonly allowsTool?: (name: string) => boolean;
|
|
29
29
|
readonly allowsExtension?: (extension: RuntimeExtensionConfig) => boolean;
|
|
30
|
+
readonly defersTool?: (name: string) => boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function emptyToolRegistry(): ToolRegistry {
|