@springbrand/agent-runtime 0.2.0-alpha.31 → 0.2.0-alpha.34
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/interaction-lifecycle.ts +5 -5
- package/src/kernel/tool-surface.ts +1 -0
- package/src/lib/prompt.ts +16 -12
- package/src/pi/message/contract.ts +1 -1
- package/src/pi/runtime-adapter/index.ts +7 -0
- package/src/pi/runtime-adapter/recovery.ts +19 -22
- package/src/pi/runtime-adapter/transcript.ts +1 -2
- package/src/pi/tool/compiler.ts +1 -1
- package/src/pi/tool/core-host.ts +18 -6
- package/src/pi/turn/tool-recovery.ts +2 -2
- package/src/runtime.ts +12 -5
- package/src/tool-registry.ts +1 -2
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
PiToolInteraction,
|
|
13
13
|
PiToolInteractionCancelReason,
|
|
14
14
|
} from "../pi/runtime-adapter";
|
|
15
|
-
import type {
|
|
15
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
16
16
|
|
|
17
17
|
// #region 类型约定
|
|
18
18
|
|
|
@@ -43,10 +43,10 @@ export interface InteractionRecord extends PiToolInteraction {
|
|
|
43
43
|
* `content` / `details` 由 Tool 自己的 `settle` 映射产出 —— 本流程不解释响应体语义。
|
|
44
44
|
* 取消不带载荷,由纯函数层用固定文案兜底。
|
|
45
45
|
*/
|
|
46
|
-
export
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
export type InteractionSettlementPayload = Omit<
|
|
47
|
+
AgentToolResult<unknown>,
|
|
48
|
+
"terminate"
|
|
49
|
+
>;
|
|
50
50
|
|
|
51
51
|
// 宿主用这些依赖把 interaction 状态机接入数据库、Pi 恢复计算和 Turn 续跑。
|
|
52
52
|
interface InteractionLifecycleOptions<
|
|
@@ -31,6 +31,7 @@ export interface ToolSpec extends Partial<
|
|
|
31
31
|
readonly requiredExecutionLevel: ExecutionLevel;
|
|
32
32
|
/** Require a fresh human decision for every call, regardless of execution level. */
|
|
33
33
|
readonly alwaysRequiresApproval?: boolean;
|
|
34
|
+
/** Preserve adapted Pi result envelopes; only details-only adapters may unwrap them. */
|
|
34
35
|
readonly execute: (
|
|
35
36
|
input: unknown,
|
|
36
37
|
ctx: ToolContext,
|
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,15 +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
|
-
"
|
|
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. " +
|
|
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. " +
|
|
55
59
|
"For web tasks, use web_search for web discovery, current facts, cited research, " +
|
|
56
60
|
"and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
|
|
57
61
|
"API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
|
|
@@ -65,7 +69,7 @@ export const TOOLS =
|
|
|
65
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. " +
|
|
66
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 " +
|
|
67
71
|
"prioritize verification, saving durable results, and the final response. " +
|
|
68
|
-
"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; " +
|
|
69
73
|
"only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
|
|
70
74
|
"code, cite it as file_path:line_number.";
|
|
71
75
|
|
|
@@ -166,5 +170,5 @@ export function assembleSystemPrompt(base?: string | null): string {
|
|
|
166
170
|
* Agent、Runtime 和 Memory 的术语见 `src/index.ts`。
|
|
167
171
|
*/
|
|
168
172
|
export function assembleSubagentPrompt(persona: string): string {
|
|
169
|
-
return [persona, RUNTIME, BEHAVIOR].join("\n\n");
|
|
173
|
+
return [persona, RUNTIME, BEHAVIOR, TOOL_ROUTING].join("\n\n");
|
|
170
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
|
}
|
|
@@ -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
|
*
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
type PiToolRecoveryState,
|
|
16
16
|
} from "../turn";
|
|
17
17
|
import type { ToolResultMessage } from "@earendil-works/pi-ai";
|
|
18
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
19
|
+
import { isEqual } from "lodash-es";
|
|
18
20
|
import {
|
|
19
21
|
aiSdkRecoveryCodec,
|
|
20
22
|
shouldCreditStreamProgress,
|
|
@@ -35,10 +37,7 @@ export interface PiRecoveredToolSettlement {
|
|
|
35
37
|
readonly toolCallId: string;
|
|
36
38
|
readonly toolName: string;
|
|
37
39
|
readonly args: unknown;
|
|
38
|
-
readonly result:
|
|
39
|
-
readonly content: ToolResultMessage["content"];
|
|
40
|
-
readonly details: unknown;
|
|
41
|
-
};
|
|
40
|
+
readonly result: Omit<AgentToolResult<unknown>, "terminate">;
|
|
42
41
|
readonly isError: boolean;
|
|
43
42
|
readonly createdAt: number;
|
|
44
43
|
}
|
|
@@ -75,10 +74,7 @@ export type PiRecoveryCommand =
|
|
|
75
74
|
| {
|
|
76
75
|
readonly kind: "respond";
|
|
77
76
|
readonly response: unknown;
|
|
78
|
-
readonly result:
|
|
79
|
-
readonly content: ToolResultMessage["content"];
|
|
80
|
-
readonly details: unknown;
|
|
81
|
-
};
|
|
77
|
+
readonly result: Omit<AgentToolResult<unknown>, "terminate">;
|
|
82
78
|
}
|
|
83
79
|
| {
|
|
84
80
|
readonly kind: "cancel";
|
|
@@ -89,10 +85,7 @@ export type PiRecoveryCommand =
|
|
|
89
85
|
readonly kind: "record-tool-result";
|
|
90
86
|
readonly toolCallId: string;
|
|
91
87
|
readonly toolName: string;
|
|
92
|
-
readonly result:
|
|
93
|
-
readonly content: ToolResultMessage["content"];
|
|
94
|
-
readonly details: unknown;
|
|
95
|
-
};
|
|
88
|
+
readonly result: AgentToolResult<unknown>;
|
|
96
89
|
readonly isError: boolean;
|
|
97
90
|
readonly timestamp: number;
|
|
98
91
|
readonly needsContinuation?: boolean;
|
|
@@ -226,6 +219,10 @@ function recoveredToolSettlements(
|
|
|
226
219
|
result: {
|
|
227
220
|
content: toolResult.content,
|
|
228
221
|
details: toolResult.details,
|
|
222
|
+
...(toolResult.usage ? { usage: toolResult.usage } : {}),
|
|
223
|
+
...(toolResult.addedToolNames?.length
|
|
224
|
+
? { addedToolNames: toolResult.addedToolNames }
|
|
225
|
+
: {}),
|
|
229
226
|
},
|
|
230
227
|
isError: toolResult.isError,
|
|
231
228
|
createdAt: toolResult.timestamp,
|
|
@@ -248,21 +245,17 @@ function approvalExecutionId(continuationKey: string): string | null {
|
|
|
248
245
|
// 记录必须已经存在 —— 拿不到 toolCallId/toolName 就无法把结果绑回原调用,这时失败关闭而不是编一个 id。
|
|
249
246
|
function interactionToolResult(
|
|
250
247
|
interaction: PiToolInteraction | undefined,
|
|
251
|
-
result:
|
|
252
|
-
readonly content: ToolResultMessage["content"];
|
|
253
|
-
readonly details: unknown;
|
|
254
|
-
},
|
|
248
|
+
result: Omit<AgentToolResult<unknown>, "terminate">,
|
|
255
249
|
timestamp: number,
|
|
256
250
|
): ToolResultMessage {
|
|
257
251
|
if (!interaction) {
|
|
258
252
|
throw new Error("SpringBrand Tool interaction is missing for its response");
|
|
259
253
|
}
|
|
260
254
|
return {
|
|
255
|
+
...result,
|
|
261
256
|
role: "toolResult",
|
|
262
257
|
toolCallId: interaction.toolCallId,
|
|
263
258
|
toolName: interaction.toolName,
|
|
264
|
-
content: result.content,
|
|
265
|
-
details: result.details,
|
|
266
259
|
isError: false,
|
|
267
260
|
timestamp,
|
|
268
261
|
};
|
|
@@ -448,9 +441,9 @@ export function decidePiRecovery(
|
|
|
448
441
|
const existing = state.toolCalls[record.toolCallId];
|
|
449
442
|
if (existing) {
|
|
450
443
|
if (
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
444
|
+
existing.toolName !== record.toolName ||
|
|
445
|
+
existing.retry !== record.retry ||
|
|
446
|
+
!isEqual(existing.input, record.input)
|
|
454
447
|
) {
|
|
455
448
|
throw new Error(
|
|
456
449
|
`Conflicting SpringBrand Tool input for ${record.toolCallId}`,
|
|
@@ -614,12 +607,16 @@ export function decidePiRecovery(
|
|
|
614
607
|
toolName: command.toolName,
|
|
615
608
|
content: command.result.content,
|
|
616
609
|
details: command.result.details,
|
|
610
|
+
...(command.result.usage ? { usage: command.result.usage } : {}),
|
|
611
|
+
...(command.result.addedToolNames?.length
|
|
612
|
+
? { addedToolNames: command.result.addedToolNames }
|
|
613
|
+
: {}),
|
|
617
614
|
isError: command.isError,
|
|
618
615
|
timestamp: command.timestamp,
|
|
619
616
|
};
|
|
620
617
|
const existing = state.toolCalls[command.toolCallId]?.result;
|
|
621
618
|
if (existing) {
|
|
622
|
-
if (
|
|
619
|
+
if (!isEqual(existing, toolResult)) {
|
|
623
620
|
throw new Error(
|
|
624
621
|
`Conflicting SpringBrand Tool result for ${command.toolCallId}`,
|
|
625
622
|
);
|
|
@@ -346,11 +346,10 @@ export class PiRuntimeTranscript {
|
|
|
346
346
|
return this.append(
|
|
347
347
|
`${submissionId}:tool:${settlement.toolCallId}`,
|
|
348
348
|
{
|
|
349
|
+
...settlement.result,
|
|
349
350
|
role: "toolResult",
|
|
350
351
|
toolCallId: settlement.toolCallId,
|
|
351
352
|
toolName: settlement.toolName,
|
|
352
|
-
content: settlement.result.content,
|
|
353
|
-
details: settlement.result.details,
|
|
354
353
|
isError: settlement.isError,
|
|
355
354
|
timestamp: settlement.createdAt,
|
|
356
355
|
},
|
package/src/pi/tool/compiler.ts
CHANGED
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 绑定,只取本仓真正用到的那一面。
|
|
@@ -42,10 +46,18 @@ function codeExecutionTools(tools: ToolRegistry): ToolSet {
|
|
|
42
46
|
aiTool({
|
|
43
47
|
description: spec.description,
|
|
44
48
|
inputSchema: jsonSchema(spec.parameters as never),
|
|
45
|
-
execute: (input, options) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
execute: async (input, options) => {
|
|
50
|
+
const result = await spec.execute(input, {
|
|
51
|
+
toolCallId: options?.toolCallId ?? name,
|
|
52
|
+
signal: options?.abortSignal ?? new AbortController().signal,
|
|
53
|
+
});
|
|
54
|
+
return result != null &&
|
|
55
|
+
typeof result === "object" &&
|
|
56
|
+
"content" in result &&
|
|
57
|
+
Array.isArray((result as { content?: unknown }).content)
|
|
58
|
+
? (result as { details?: unknown }).details ?? result
|
|
59
|
+
: result;
|
|
60
|
+
},
|
|
49
61
|
}),
|
|
50
62
|
]));
|
|
51
63
|
}
|
|
@@ -183,10 +195,10 @@ export function createWorkspaceCodeExecutionFactory(options: {
|
|
|
183
195
|
const port = toCodeExecutionPort(tool, "execute");
|
|
184
196
|
return {
|
|
185
197
|
...port,
|
|
186
|
-
description: port.description.replace(
|
|
198
|
+
description: `${CODEMODE_TOOL_SURFACE_GUIDANCE}\n\n${port.description.replace(
|
|
187
199
|
"- Do not use `fetch` — use connector SDKs.",
|
|
188
200
|
"- Raw `fetch` is available inside `codemode.step(...)`; prefer connector SDKs when one owns the target.",
|
|
189
|
-
)
|
|
201
|
+
)}`,
|
|
190
202
|
};
|
|
191
203
|
},
|
|
192
204
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolResultMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { isEqual } from "lodash-es";
|
|
2
3
|
import { EXECUTION_LEVELS } from "../../lib/execution-level";
|
|
3
4
|
import {
|
|
4
5
|
decidePiToolApproval,
|
|
@@ -444,11 +445,10 @@ function sameToolInput(
|
|
|
444
445
|
{ type: "tool-input" }
|
|
445
446
|
>,
|
|
446
447
|
): boolean {
|
|
447
|
-
// 待确认:JSON.stringify 对对象键顺序敏感;当前写入链会保留输入顺序,重建输入的调用方可能产生语义相同但顺序不同的对象。
|
|
448
448
|
return (
|
|
449
449
|
previous.toolName === milestone.toolName &&
|
|
450
450
|
previous.retry === milestone.retry &&
|
|
451
|
-
|
|
451
|
+
isEqual(previous.input, milestone.input)
|
|
452
452
|
);
|
|
453
453
|
}
|
|
454
454
|
|
package/src/runtime.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -4076,10 +4086,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
4076
4086
|
const settled = spec.settle
|
|
4077
4087
|
? spec.settle(input, response)
|
|
4078
4088
|
: defaultInteractionResult(response);
|
|
4079
|
-
return this.interactions.respond(pending.interactionId, response,
|
|
4080
|
-
content: settled.content,
|
|
4081
|
-
details: settled.details,
|
|
4082
|
-
});
|
|
4089
|
+
return this.interactions.respond(pending.interactionId, response, settled);
|
|
4083
4090
|
}
|
|
4084
4091
|
|
|
4085
4092
|
/**
|
package/src/tool-registry.ts
CHANGED
|
@@ -85,13 +85,12 @@ export function piCandidateToToolSpec(candidate: PiToolCandidate): ToolSpec {
|
|
|
85
85
|
name,
|
|
86
86
|
arguments: input as Record<string, unknown>,
|
|
87
87
|
});
|
|
88
|
-
|
|
88
|
+
return execute(
|
|
89
89
|
ctx.toolCallId,
|
|
90
90
|
validated,
|
|
91
91
|
ctx.signal,
|
|
92
92
|
undefined,
|
|
93
93
|
);
|
|
94
|
-
return result.details ?? result;
|
|
95
94
|
},
|
|
96
95
|
};
|
|
97
96
|
}
|