@springbrand/agent-runtime 0.2.0-alpha.29 → 0.2.0-alpha.31
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 +24 -21
- package/src/pi/message/projection.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +1 -0
- package/src/pi/runtime-adapter/execution.ts +1 -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/runtime-adapter/transcript.ts +3 -0
- package/src/pi/tool/base.ts +3 -3
- package/src/pi/tool/compiler.ts +3 -0
- package/src/pi/tool/core-host.ts +18 -4
- package/src/pi/tool/web-search/api.ts +1 -1
- package/src/runtime-agent-context.ts +2 -0
- package/src/runtime-agent.ts +14 -1
- package/src/runtime-assembler.ts +17 -3
- package/src/runtime-definition.ts +2 -3
- package/src/runtime.ts +1 -1
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/tool-registry.ts +1 -0
package/package.json
CHANGED
package/src/kernel/bindings.ts
CHANGED
package/src/lib/prompt.ts
CHANGED
|
@@ -50,6 +50,8 @@ export const TOOLS =
|
|
|
50
50
|
"or branching or repetition MUST use one execute. Do not make repeated top-level calls for that work. Inside execute, use state.* for " +
|
|
51
51
|
"workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent calls. " +
|
|
52
52
|
"Tools available only at the top level must stay Direct. " +
|
|
53
|
+
"Some Tools may be deferred and absent from the initial list. When Tool Search is available, before saying that a requested capability or named Tool is unavailable, use the available Tool Search once. " +
|
|
54
|
+
"If the user names a Tool, search that exact name by itself first. Searchable categories may include workspace and files, browser and web, interaction and planning, memory and scheduling, Skills, Extensions, MCP, Plugins, and Host capabilities; discover them only when needed. " +
|
|
53
55
|
"For web tasks, use web_search for web discovery, current facts, cited research, " +
|
|
54
56
|
"and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
|
|
55
57
|
"API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
|
|
@@ -67,28 +69,29 @@ export const TOOLS =
|
|
|
67
69
|
"only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
|
|
68
70
|
"code, cite it as file_path:line_number.";
|
|
69
71
|
|
|
70
|
-
//
|
|
71
|
-
// failure mode is
|
|
72
|
+
// A real browser is the only way to observe what a page actually does, and the
|
|
73
|
+
// shared failure mode is a browser tool that returns nothing and looks clean.
|
|
74
|
+
//
|
|
75
|
+
// This section names no browser tool and no protocol. A Host may expose the
|
|
76
|
+
// kernel's own `browser_execute`, its own narrower browser tools, or none at
|
|
77
|
+
// all — a section that spelled out one tool's mechanics would be teaching a
|
|
78
|
+
// tool that is not there for every Host but the first. How to drive a specific
|
|
79
|
+
// browser tool belongs to that tool's own description, which the model sees if
|
|
80
|
+
// and only if the tool is registered.
|
|
72
81
|
export const BROWSER =
|
|
73
|
-
"Browser:
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"After you write or change a web page in the workspace, open it once with
|
|
78
|
-
"the work done, and say in your reply what you checked and what you saw. " +
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"and 'securitypolicyviolation' into one page global; then navigate, poll Runtime.evaluate for " +
|
|
85
|
-
"document.readyState === 'complete' (you cannot await an event), then Runtime.evaluate that global back with " +
|
|
86
|
-
"returnByValue. When visual appearance matters, call Page.captureScreenshot and return " +
|
|
87
|
-
"`{ image: { mimeType: 'image/png', data: screenshot.data }, observations }`; the Runtime projects that image " +
|
|
88
|
-
"back to you, so evaluate the visible hierarchy, spacing, clipping and overlap against the request as well as " +
|
|
89
|
-
"the textual diagnostics. Issue CDP calls sequentially — never Promise.all — because call order is recorded for replay. " +
|
|
82
|
+
"Browser: a real browser is for the one thing execute cannot do — observe what a page actually does: " +
|
|
83
|
+
"rendered result, console output, runtime exceptions, failed resource loads, CSP blocks, and state after an " +
|
|
84
|
+
"interaction. Everything else stays with execute: computation, files, and ordinary HTTP requests (fetching " +
|
|
85
|
+
"HTML is execute's job, not the browser's). " +
|
|
86
|
+
"After you write or change a web page in the workspace, open it once with the browser tools you have before " +
|
|
87
|
+
"you call the work done, and say in your reply what you checked and what you saw. " +
|
|
88
|
+
"Drive each browser tool the way its own description tells you to; the failure mode they share is coming back " +
|
|
89
|
+
"empty and looking clean, so read every result for what it does not contain and never report a page as " +
|
|
90
|
+
"verified on the strength of a result that observed nothing. When visual appearance matters, capture the page " +
|
|
91
|
+
"and judge the visible hierarchy, spacing, clipping and overlap against the request alongside the textual " +
|
|
92
|
+
"diagnostics. " +
|
|
90
93
|
"Fix what you find and re-check; when the page comes back clean, stop — do not re-verify a page that is " +
|
|
91
|
-
"already clean. If
|
|
94
|
+
"already clean. If you have no browser tool, say plainly that you could not verify the page in a browser " +
|
|
92
95
|
"instead of implying you did.";
|
|
93
96
|
|
|
94
97
|
// Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
|
|
@@ -103,7 +106,7 @@ export const FILES =
|
|
|
103
106
|
|
|
104
107
|
// User-interaction actions are called only when their UI semantics are useful.
|
|
105
108
|
export const INTERACTION =
|
|
106
|
-
"Interaction: When progress requires user decisions, put all 1-
|
|
109
|
+
"Interaction: When progress requires user decisions, put all 1-10 necessary questions in one ask_user call " +
|
|
107
110
|
"instead of asking each question separately or writing 'please choose A/B/C' as text. Use 2-6 options for " +
|
|
108
111
|
"a bounded choice; for required free text, omit the options field entirely — never pass an empty or single-item options array; " +
|
|
109
112
|
"continue the same turn when its result arrives. " +
|
|
@@ -148,6 +148,7 @@ interface StreamingToolInput extends PendingToolInput {
|
|
|
148
148
|
export class PiChunkEncoder {
|
|
149
149
|
private readonly messageId: string;
|
|
150
150
|
private readonly startedAt?: number;
|
|
151
|
+
private readonly turnId?: string;
|
|
151
152
|
private started = false;
|
|
152
153
|
private finished = false;
|
|
153
154
|
private firstTurnStarted = false;
|
|
@@ -162,9 +163,14 @@ export class PiChunkEncoder {
|
|
|
162
163
|
{ toolName: string; input: Record<string, unknown> }
|
|
163
164
|
>();
|
|
164
165
|
|
|
165
|
-
constructor(options: {
|
|
166
|
+
constructor(options: {
|
|
167
|
+
messageId: string;
|
|
168
|
+
startedAt?: number;
|
|
169
|
+
turnId?: string;
|
|
170
|
+
}) {
|
|
166
171
|
this.messageId = options.messageId;
|
|
167
172
|
this.startedAt = options.startedAt;
|
|
173
|
+
this.turnId = options.turnId;
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
start(): UIMessageChunk[] {
|
|
@@ -181,6 +187,7 @@ export class PiChunkEncoder {
|
|
|
181
187
|
messageMetadata: {
|
|
182
188
|
createdAt: this.startedAt,
|
|
183
189
|
turnStatus: "running",
|
|
190
|
+
...(this.turnId ? { turnId: this.turnId } : {}),
|
|
184
191
|
},
|
|
185
192
|
}),
|
|
186
193
|
},
|
|
@@ -452,6 +459,7 @@ export class PiChunkEncoder {
|
|
|
452
459
|
completedAt: message.timestamp,
|
|
453
460
|
turnDurationMs: Math.max(0, message.timestamp - this.startedAt),
|
|
454
461
|
turnStatus,
|
|
462
|
+
...(this.turnId ? { turnId: this.turnId } : {}),
|
|
455
463
|
...(publicError &&
|
|
456
464
|
(message.stopReason !== "aborted" ||
|
|
457
465
|
message.errorMessage !== USER_STOP_REASON)
|
|
@@ -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
|
}))
|
|
@@ -663,6 +663,7 @@ export class PreparedPiTurnAdapter {
|
|
|
663
663
|
this.encoder = new PiChunkEncoder({
|
|
664
664
|
messageId: options.submission.messageId,
|
|
665
665
|
startedAt: options.submission.startedAt,
|
|
666
|
+
turnId: options.submission.id,
|
|
666
667
|
});
|
|
667
668
|
const bindCandidate = (
|
|
668
669
|
candidate: PiToolCandidate,
|
|
@@ -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
|
+
|
|
@@ -649,6 +649,7 @@ export class PiRuntimeTranscript {
|
|
|
649
649
|
role: "assistant",
|
|
650
650
|
parts: [],
|
|
651
651
|
metadata: {
|
|
652
|
+
turnId: submissionId,
|
|
652
653
|
createdAt: submission.createdAt,
|
|
653
654
|
completedAt,
|
|
654
655
|
turnDurationMs: Math.max(
|
|
@@ -706,6 +707,7 @@ export class PiRuntimeTranscript {
|
|
|
706
707
|
const createdAt = submission?.createdAt ?? entry.createdAt;
|
|
707
708
|
const completedAt = submission?.completedAt ?? entry.createdAt;
|
|
708
709
|
const metadata = {
|
|
710
|
+
turnId: entry.submissionId,
|
|
709
711
|
createdAt,
|
|
710
712
|
completedAt,
|
|
711
713
|
turnDurationMs: Math.max(
|
|
@@ -753,6 +755,7 @@ export class PiRuntimeTranscript {
|
|
|
753
755
|
? entry.id
|
|
754
756
|
: submission?.assistantMessageId ?? entry.id,
|
|
755
757
|
metadata: {
|
|
758
|
+
turnId: entry.submissionId,
|
|
756
759
|
createdAt: submission?.createdAt ?? entry.createdAt,
|
|
757
760
|
completedAt,
|
|
758
761
|
...(completedAt === undefined
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -40,7 +40,7 @@ const askUserQuestion = Type.Object({
|
|
|
40
40
|
const askUserParameters = Type.Object({
|
|
41
41
|
questions: Type.Array(askUserQuestion, {
|
|
42
42
|
minItems: 1,
|
|
43
|
-
maxItems:
|
|
43
|
+
maxItems: 10,
|
|
44
44
|
description:
|
|
45
45
|
"All user decisions needed to continue. Ask them together in one call.",
|
|
46
46
|
}),
|
|
@@ -70,7 +70,7 @@ function recordOf(value: unknown): Record<string, unknown> | null {
|
|
|
70
70
|
|
|
71
71
|
function questionsOf(value: unknown): AskUserInput["questions"] | null {
|
|
72
72
|
const questions = recordOf(value)?.questions;
|
|
73
|
-
if (!Array.isArray(questions) || questions.length < 1 || questions.length >
|
|
73
|
+
if (!Array.isArray(questions) || questions.length < 1 || questions.length > 10) {
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
for (const value of questions) {
|
|
@@ -238,7 +238,7 @@ export function basePiToolCandidates(
|
|
|
238
238
|
name: "ask_user",
|
|
239
239
|
label: "Ask user",
|
|
240
240
|
description:
|
|
241
|
-
"Ask all 1-
|
|
241
|
+
"Ask all 1-10 user questions needed to continue in one call. For a bounded choice, provide 2-6 distinct options. For required free text, omit the options field entirely; never pass an empty, single-item, or more-than-6-item options array. Do not make one call per question or write plain text like 'please pick A / B / C'. The answers return as this tool's result, so continue the same turn after calling it.",
|
|
242
242
|
parameters: askUserParameters,
|
|
243
243
|
// 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
|
|
244
244
|
// 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
|
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
|
@@ -73,9 +73,7 @@ function toCodeExecutionPort(
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
// 放进静态 system prompt 前缀会让它每轮都变,破坏 prompt cache。
|
|
78
|
-
// 怎么用 CDP 才不会静默读空,是与 Session 无关的固定动作,住在静态提示词里。
|
|
76
|
+
// preview URL 是 per-Session 的:放进静态 system prompt 前缀会让它每轮都变,破坏 prompt cache。
|
|
79
77
|
function previewNote(base: string): string {
|
|
80
78
|
return "Open the current Workspace app with this exact preview URL, unchanged: " +
|
|
81
79
|
`${base} Keep its path and query string intact; do not append a workspace file path. ` +
|
|
@@ -83,6 +81,9 @@ function previewNote(base: string): string {
|
|
|
83
81
|
"file:// cannot reach the workspace, and there is no other preview host — do not guess one.";
|
|
84
82
|
}
|
|
85
83
|
|
|
84
|
+
// 怎么用 CDP 才不会静默读空,住在这里而不是静态提示词里:它只对 `browser_execute` 成立。
|
|
85
|
+
// 一个 Host 可以 deny 它、换成自己的浏览器工具,那时静态提示词里的这套配方就是在教一个
|
|
86
|
+
// 不存在的工具;工具描述只在工具真被注册时才出现,所以这份知识跟着工具走。
|
|
86
87
|
function browserToolDescription(): string {
|
|
87
88
|
return "Run JavaScript against a live browser over the Chrome DevTools Protocol through the `cdp` connector. " +
|
|
88
89
|
"Use the bare lexical identifiers `cdp` and `codemode`. They are not properties of `globalThis` and are not importable modules. " +
|
|
@@ -92,7 +93,20 @@ function browserToolDescription(): string {
|
|
|
92
93
|
"Create targets, attach, navigate, interact, inspect diagnostics, and capture screenshots within the same call. " +
|
|
93
94
|
"Use `cdp.spec()` to discover commands, `cdp.send({ method, params })` for target-scoped commands, " +
|
|
94
95
|
"and `cdp.attachToTarget({ targetId })` before page-scoped commands. " +
|
|
95
|
-
"Issue browser calls sequentially
|
|
96
|
+
"Issue browser calls sequentially — never Promise.all — because call order is recorded for replay. " +
|
|
97
|
+
"The `cdp` connector is request/response only: it delivers no CDP events. Enabling Runtime/Log/Network and " +
|
|
98
|
+
"then reading `cdp.getDebugLog()` returns method names with no payload — a page full of errors reads as clean. " +
|
|
99
|
+
"So the fixed shape for collecting diagnostics is: create a target and attach for a sessionId; BEFORE " +
|
|
100
|
+
"Page.navigate, install a page-side collector via Page.addScriptToEvaluateOnNewDocument that buffers " +
|
|
101
|
+
"console.*, a capture-phase window 'error' listener (it catches both uncaught exceptions and failed " +
|
|
102
|
+
"<script>/<img>/<link> loads), 'unhandledrejection' and 'securitypolicyviolation' into one page global; then " +
|
|
103
|
+
"navigate, poll Runtime.evaluate for document.readyState === 'complete' (you cannot await an event), then " +
|
|
104
|
+
"Runtime.evaluate that global back with returnByValue. " +
|
|
105
|
+
"When visual appearance matters, call Page.captureScreenshot and return " +
|
|
106
|
+
"`{ image: { mimeType: 'image/png', data: screenshot.data }, observations }`; this tool projects that image " +
|
|
107
|
+
"back to you. Every call's result is recorded whole for replay, and one full-page PNG can exceed that " +
|
|
108
|
+
"record's size limit and fail the entire call — capture the viewport rather than the full page, and return " +
|
|
109
|
+
"one screenshot per call, not several.";
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
/**
|
|
@@ -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";
|
|
@@ -60,6 +60,8 @@ export interface RuntimeAgentConfigContext<
|
|
|
60
60
|
readonly parentPath: readonly RuntimeAgentPathStep[];
|
|
61
61
|
/** Runtime 根据 Agent facet 父子关系自动判定,不是外部配置项。 */
|
|
62
62
|
readonly role: RuntimeAgentRole;
|
|
63
|
+
/** 向当前 Agent facet 的已连接客户端发送实时消息。 */
|
|
64
|
+
broadcast(message: string): void;
|
|
63
65
|
/**
|
|
64
66
|
* 通过当前 Agent 调用一个受 SDK 管理的子 Agent Tool。
|
|
65
67
|
*
|
package/src/runtime-agent.ts
CHANGED
|
@@ -87,6 +87,8 @@ export interface LoadedRuntime<Config> {
|
|
|
87
87
|
export interface RuntimeConfigUpdate<Change> {
|
|
88
88
|
readonly changed: boolean;
|
|
89
89
|
readonly change: Change;
|
|
90
|
+
/** Restore any committed config state when the replacement Runtime cannot load. */
|
|
91
|
+
readonly rollback?: () => Promise<void>;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
interface RuntimeAgentDefinitionBase<
|
|
@@ -482,7 +484,17 @@ export function defineRuntimeAgent<
|
|
|
482
484
|
try {
|
|
483
485
|
await this.ensureConfig(undefined, true);
|
|
484
486
|
return { change: result.change, runtime: "reloaded" };
|
|
485
|
-
} catch {
|
|
487
|
+
} catch (reloadError) {
|
|
488
|
+
if (result.rollback) {
|
|
489
|
+
try {
|
|
490
|
+
await result.rollback();
|
|
491
|
+
} catch (rollbackError) {
|
|
492
|
+
throw new AggregateError(
|
|
493
|
+
[reloadError, rollbackError],
|
|
494
|
+
"Runtime Config rollback failed",
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
486
498
|
return { change: result.change, runtime: "reload-failed" };
|
|
487
499
|
}
|
|
488
500
|
}
|
|
@@ -824,6 +836,7 @@ export function defineRuntimeAgent<
|
|
|
824
836
|
get role() {
|
|
825
837
|
return agent.role;
|
|
826
838
|
},
|
|
839
|
+
broadcast: (message) => agent.broadcast(message),
|
|
827
840
|
...(agent.role === "primary"
|
|
828
841
|
? {
|
|
829
842
|
updateConfig: (command: Command) =>
|
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 }
|
|
@@ -68,6 +68,6 @@ export const SPRINGBRAND_WORKER_WEBSITE_SKILL = {
|
|
|
68
68
|
"2. Create or update the complete project rather than only an entry HTML file.",
|
|
69
69
|
"3. Build when the project requires a build step.",
|
|
70
70
|
"4. Preview the Website through the available Workspace or Space capability.",
|
|
71
|
-
"5.
|
|
71
|
+
"5. Open the previewed Website with the browser tools you have and check runtime errors, failed resources, CSP violations, responsive layout, clipping, overlap, and visual hierarchy. Capture a screenshot when visual appearance matters, fix discovered problems, and verify once more.",
|
|
72
72
|
].join("\n"),
|
|
73
73
|
};
|
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 {
|