@springbrand/agent-runtime 0.2.0-alpha.13 → 0.2.0-alpha.14
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/adapter/cloudflare/universal-agent/preparation.ts +2 -2
- package/src/adapter/cloudflare/universal-agent/tools.ts +3 -6
- package/src/index.ts +2 -2
- package/src/kernel/submission-lifecycle.ts +18 -5
- package/src/layers/context/budget/gate.ts +99 -0
- package/src/lib/prompt.ts +4 -3
- package/src/pi/message/projection.ts +2 -2
- package/src/pi/runtime-adapter/assembly.ts +2 -2
- package/src/pi/runtime-adapter/execution.ts +47 -15
- package/src/pi/runtime-adapter/index.ts +1 -0
- package/src/pi/runtime-adapter/models.ts +1 -1
- package/src/pi/runtime-adapter/recovery.ts +17 -17
- package/src/pi/runtime-adapter/transcript.ts +2 -2
- package/src/pi/tool/base.ts +113 -27
- package/src/pi/tool/compiler.ts +66 -6
- package/src/pi/tool/core-host.ts +50 -27
- package/src/pi/tool/core.ts +1 -13
- package/src/pi/tool/skill.ts +78 -3
- package/src/pi/tool/workspace-sandbox.ts +4 -4
- package/src/pi/turn/tool-recovery.ts +7 -7
- package/src/runtime-agent.ts +8 -3
- package/src/runtime-assembler.ts +45 -8
- package/src/runtime.ts +58 -20
- package/src/tool-registry.ts +8 -1
package/src/pi/tool/base.ts
CHANGED
|
@@ -15,47 +15,122 @@ import {
|
|
|
15
15
|
import { webSearchPiToolCandidate } from "./web-search";
|
|
16
16
|
import type { WebSearch } from "./web-search/api";
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const askUserQuestion = Type.Object({
|
|
19
19
|
question: Type.String({
|
|
20
|
+
minLength: 1,
|
|
20
21
|
description: "The question to put to the user.",
|
|
21
22
|
}),
|
|
22
|
-
options: Type.Array(Type.String(), {
|
|
23
|
+
options: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
|
|
23
24
|
minItems: 2,
|
|
24
25
|
maxItems: 6,
|
|
26
|
+
uniqueItems: true,
|
|
25
27
|
description: "2-6 mutually distinct options for the user to choose from.",
|
|
26
|
-
}),
|
|
28
|
+
})),
|
|
27
29
|
multiSelect: Type.Optional(Type.Boolean({
|
|
28
30
|
description:
|
|
29
31
|
"Whether the user may select more than one option. Defaults to false.",
|
|
30
32
|
})),
|
|
33
|
+
allowCustom: Type.Optional(Type.Boolean({
|
|
34
|
+
description:
|
|
35
|
+
"Whether the user may add free text. Defaults to false with options and true without options.",
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const askUserParameters = Type.Object({
|
|
40
|
+
questions: Type.Array(askUserQuestion, {
|
|
41
|
+
minItems: 1,
|
|
42
|
+
maxItems: 4,
|
|
43
|
+
description:
|
|
44
|
+
"All user decisions needed to continue. Ask them together in one call.",
|
|
45
|
+
}),
|
|
31
46
|
});
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
49
|
* `ask_user` 的客户端响应体。
|
|
35
50
|
*
|
|
36
|
-
* `
|
|
51
|
+
* `answers` 与 Tool 输入的 `questions` 按下标一一对应。
|
|
37
52
|
* 刻意用结构化数组而不是拼好的字符串 —— 选项本身可能含分隔符,拼了再拆是有损的。
|
|
38
53
|
*/
|
|
39
54
|
const askUserResponse = Type.Object({
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
answers: Type.Array(Type.Object({
|
|
56
|
+
selections: Type.Array(Type.String()),
|
|
57
|
+
text: Type.Optional(Type.String()),
|
|
58
|
+
})),
|
|
42
59
|
});
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
type AskUserInput = Static<typeof askUserParameters>;
|
|
62
|
+
type AskUserResponse = Static<typeof askUserResponse>;
|
|
63
|
+
|
|
64
|
+
function recordOf(value: unknown): Record<string, unknown> | null {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
66
|
+
? value as Record<string, unknown>
|
|
67
|
+
: null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function questionsOf(value: unknown): AskUserInput["questions"] | null {
|
|
71
|
+
const questions = recordOf(value)?.questions;
|
|
72
|
+
if (!Array.isArray(questions) || questions.length < 1 || questions.length > 4) {
|
|
73
|
+
return null;
|
|
52
74
|
}
|
|
53
|
-
|
|
75
|
+
for (const value of questions) {
|
|
76
|
+
const question = recordOf(value);
|
|
77
|
+
if (!question || typeof question.question !== "string" || !question.question) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
question.multiSelect !== undefined &&
|
|
82
|
+
typeof question.multiSelect !== "boolean"
|
|
83
|
+
) return null;
|
|
84
|
+
if (
|
|
85
|
+
question.allowCustom !== undefined &&
|
|
86
|
+
typeof question.allowCustom !== "boolean"
|
|
87
|
+
) return null;
|
|
88
|
+
if (question.options !== undefined) {
|
|
89
|
+
if (
|
|
90
|
+
!Array.isArray(question.options) ||
|
|
91
|
+
question.options.length < 2 ||
|
|
92
|
+
question.options.length > 6 ||
|
|
93
|
+
!question.options.every((option) =>
|
|
94
|
+
typeof option === "string" && option.length > 0
|
|
95
|
+
) ||
|
|
96
|
+
new Set(question.options).size !== question.options.length
|
|
97
|
+
) return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return questions as AskUserInput["questions"];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
|
|
104
|
+
// 且校验位于 Worker 响应信任边界,不应为每次回答编译 schema。
|
|
105
|
+
function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
106
|
+
const questions = questionsOf(input);
|
|
107
|
+
const answers = recordOf(value)?.answers;
|
|
108
|
+
if (!questions || !Array.isArray(answers) || answers.length !== questions.length) {
|
|
54
109
|
return false;
|
|
55
110
|
}
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
|
|
111
|
+
|
|
112
|
+
return questions.every((question, index) => {
|
|
113
|
+
const answer = recordOf(answers[index]);
|
|
114
|
+
if (!answer || !Array.isArray(answer.selections)) return false;
|
|
115
|
+
if (!answer.selections.every((item) => typeof item === "string")) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (answer.text !== undefined && typeof answer.text !== "string") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const options = question.options ?? [];
|
|
123
|
+
const text = typeof answer.text === "string" ? answer.text : "";
|
|
124
|
+
if (text.length > 2_000) return false;
|
|
125
|
+
if (answer.selections.some((selection) => !options.includes(selection))) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (new Set(answer.selections).size !== answer.selections.length) return false;
|
|
129
|
+
if (!question.multiSelect && answer.selections.length > 1) return false;
|
|
130
|
+
const allowCustom = options.length === 0 || question.allowCustom === true;
|
|
131
|
+
if (!allowCustom && text.trim()) return false;
|
|
132
|
+
return answer.selections.length > 0 || text.trim().length > 0;
|
|
133
|
+
});
|
|
59
134
|
}
|
|
60
135
|
|
|
61
136
|
const suggestFollowupsParameters = Type.Object({
|
|
@@ -162,7 +237,7 @@ export function basePiToolCandidates(
|
|
|
162
237
|
name: "ask_user",
|
|
163
238
|
label: "Ask user",
|
|
164
239
|
description:
|
|
165
|
-
"Ask
|
|
240
|
+
"Ask all 1-4 user questions needed to continue in one call. Each question may offer 2-6 options, allow multiple selections, and/or allow a custom answer. 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.",
|
|
166
241
|
parameters: askUserParameters,
|
|
167
242
|
// 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
|
|
168
243
|
// 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
|
|
@@ -174,18 +249,29 @@ export function basePiToolCandidates(
|
|
|
174
249
|
}),
|
|
175
250
|
interaction: {
|
|
176
251
|
validateResponse: isAskUserResponse,
|
|
177
|
-
settle: (
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
252
|
+
settle: (input, response) => {
|
|
253
|
+
const questions = (input as AskUserInput).questions;
|
|
254
|
+
const responseAnswers = (response as AskUserResponse).answers;
|
|
255
|
+
const answers = questions.map((question, index) => {
|
|
256
|
+
const answer = responseAnswers[index]!;
|
|
257
|
+
return {
|
|
258
|
+
question: question.question,
|
|
259
|
+
selections: answer.selections,
|
|
260
|
+
...(answer.text !== undefined ? { text: answer.text.trim() } : {}),
|
|
261
|
+
};
|
|
262
|
+
});
|
|
183
263
|
return {
|
|
184
264
|
content: [{
|
|
185
265
|
type: "text",
|
|
186
|
-
text: `The user answered
|
|
266
|
+
text: `The user answered:\n${answers.map((answer, index) => {
|
|
267
|
+
const parts = [
|
|
268
|
+
...answer.selections,
|
|
269
|
+
...(answer.text ? [answer.text] : []),
|
|
270
|
+
];
|
|
271
|
+
return `${index + 1}. ${answer.question}\n ${parts.join(" / ")}`;
|
|
272
|
+
}).join("\n")}`,
|
|
187
273
|
}],
|
|
188
|
-
details:
|
|
274
|
+
details: { answers },
|
|
189
275
|
};
|
|
190
276
|
},
|
|
191
277
|
},
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -4,7 +4,16 @@ import type {
|
|
|
4
4
|
AgentToolResult,
|
|
5
5
|
} from "@earendil-works/pi-agent-core";
|
|
6
6
|
import type { ApprovalReceipt } from "../../kernel/receipts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
boundDurableToolOutput,
|
|
9
|
+
type BudgetPolicy,
|
|
10
|
+
spillDurableToolOutput,
|
|
11
|
+
} from "../../layers/context/budget/gate";
|
|
12
|
+
import {
|
|
13
|
+
type ArtifactRef,
|
|
14
|
+
serializeOutput,
|
|
15
|
+
type SpillWorkspace,
|
|
16
|
+
} from "../../lib/artifacts";
|
|
8
17
|
import {
|
|
9
18
|
type ExecutionLevel,
|
|
10
19
|
} from "../../lib/execution-level";
|
|
@@ -24,8 +33,8 @@ import {
|
|
|
24
33
|
* 会让下一次模型调用非法。
|
|
25
34
|
*/
|
|
26
35
|
export interface PiToolInteractionSpec {
|
|
27
|
-
/**
|
|
28
|
-
readonly validateResponse: (response: unknown) => boolean;
|
|
36
|
+
/** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
|
|
37
|
+
readonly validateResponse: (input: unknown, response: unknown) => boolean;
|
|
29
38
|
/** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
|
|
30
39
|
readonly settle?: (
|
|
31
40
|
input: unknown,
|
|
@@ -54,11 +63,14 @@ export interface PiToolCandidate {
|
|
|
54
63
|
readonly summary?: string;
|
|
55
64
|
readonly source?: ApprovalReceipt["source"];
|
|
56
65
|
readonly interaction?: PiToolInteractionSpec;
|
|
66
|
+
/** Large payloads spill by default; protocol-shaped results opt into structure. */
|
|
67
|
+
readonly outputBudget?: BudgetPolicy;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/** 控制候选 Pi 工具如何被编译为可执行工具集。 */
|
|
60
71
|
export interface CompilePiToolsOptions {
|
|
61
72
|
readonly governance?: PiToolGovernance;
|
|
73
|
+
readonly workspace?: SpillWorkspace;
|
|
62
74
|
readonly settle: (
|
|
63
75
|
call: SettledPiToolCall,
|
|
64
76
|
) => void | Promise<void>;
|
|
@@ -200,6 +212,52 @@ function emitToolTelemetry(
|
|
|
200
212
|
}
|
|
201
213
|
}
|
|
202
214
|
|
|
215
|
+
// Most Pi adapters keep the original Tool value in details and its serialized
|
|
216
|
+
// model form in one text block. Spill that original once instead of duplicating
|
|
217
|
+
// it inside an AgentToolResult JSON envelope.
|
|
218
|
+
function spillPayload(result: AgentToolResult<unknown>): unknown {
|
|
219
|
+
const text = result.content.length === 1 && result.content[0]?.type === "text"
|
|
220
|
+
? result.content[0].text
|
|
221
|
+
: undefined;
|
|
222
|
+
if (text !== undefined && serializeOutput(result.details).text === text) {
|
|
223
|
+
return result.details;
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function artifactResult(
|
|
229
|
+
result: AgentToolResult<unknown>,
|
|
230
|
+
ref: ArtifactRef,
|
|
231
|
+
): AgentToolResult<unknown> {
|
|
232
|
+
return {
|
|
233
|
+
...result,
|
|
234
|
+
content: [{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: [
|
|
237
|
+
ref.note,
|
|
238
|
+
`Path: ${ref.path}`,
|
|
239
|
+
`Bytes: ${ref.bytes}`,
|
|
240
|
+
`Preview:\n${ref.preview}`,
|
|
241
|
+
].join("\n"),
|
|
242
|
+
}],
|
|
243
|
+
details: ref,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function durableResult(
|
|
248
|
+
result: AgentToolResult<unknown>,
|
|
249
|
+
candidate: PiToolCandidate,
|
|
250
|
+
workspace: SpillWorkspace | undefined,
|
|
251
|
+
): Promise<AgentToolResult<unknown>> {
|
|
252
|
+
if ((candidate.outputBudget ?? { kind: "spill" }).kind === "spill") {
|
|
253
|
+
const ref = await spillDurableToolOutput(spillPayload(result), {
|
|
254
|
+
workspace,
|
|
255
|
+
});
|
|
256
|
+
if (ref) return artifactResult(result, ref);
|
|
257
|
+
}
|
|
258
|
+
return boundDurableToolOutput(result) as AgentToolResult<unknown>;
|
|
259
|
+
}
|
|
260
|
+
|
|
203
261
|
// 记录一次工具失败并在 Turn 达到上限时打开断路器。
|
|
204
262
|
// 受治理 execute 方法在工具或结算失败后调用。
|
|
205
263
|
// 同时记录单输入次数和 Turn 总数,是为了既阻止原样重试又防止换参数无限失败。
|
|
@@ -341,9 +399,11 @@ function governedTool(
|
|
|
341
399
|
|
|
342
400
|
let result: AgentToolResult<unknown>;
|
|
343
401
|
try {
|
|
344
|
-
result =
|
|
402
|
+
result = await durableResult(
|
|
345
403
|
await execute(toolCallId, args, signal, onUpdate),
|
|
346
|
-
|
|
404
|
+
candidate,
|
|
405
|
+
options.workspace,
|
|
406
|
+
);
|
|
347
407
|
} catch (cause) {
|
|
348
408
|
const message = errorMessage(cause);
|
|
349
409
|
const result = boundDurableToolOutput({
|
|
@@ -442,7 +502,7 @@ export function compilePiTools(
|
|
|
442
502
|
options: CompilePiToolsOptions,
|
|
443
503
|
): AgentTool<any, any>[] {
|
|
444
504
|
if (!options?.settle) {
|
|
445
|
-
throw new Error("
|
|
505
|
+
throw new Error("SpringBrand Tool settlement port is required");
|
|
446
506
|
}
|
|
447
507
|
return candidates.map((candidate) => governedTool(candidate, options));
|
|
448
508
|
}
|
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -3,10 +3,14 @@ import {
|
|
|
3
3
|
type WorkspaceFsLike,
|
|
4
4
|
} from "@cloudflare/shell";
|
|
5
5
|
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
|
|
6
|
+
import { jsonSchema, tool as aiTool, type ToolSet } from "ai";
|
|
6
7
|
import type {
|
|
7
|
-
RuntimeCodeExecutionPort,
|
|
8
8
|
WorkspacePort,
|
|
9
9
|
} from "../../kernel/bindings";
|
|
10
|
+
import type {
|
|
11
|
+
RuntimeCodeExecutionFactory,
|
|
12
|
+
ToolRegistry,
|
|
13
|
+
} from "../../tool-registry";
|
|
10
14
|
|
|
11
15
|
// 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
|
|
12
16
|
|
|
@@ -19,36 +23,55 @@ const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
|
|
|
19
23
|
*
|
|
20
24
|
* Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
|
|
21
25
|
*/
|
|
22
|
-
|
|
26
|
+
function codeExecutionTools(tools: ToolRegistry): ToolSet {
|
|
27
|
+
return Object.fromEntries(Object.entries(tools).map(([name, spec]) => [
|
|
28
|
+
name,
|
|
29
|
+
aiTool({
|
|
30
|
+
description: spec.description,
|
|
31
|
+
inputSchema: jsonSchema(spec.parameters as never),
|
|
32
|
+
execute: (input, options) => spec.execute(input, {
|
|
33
|
+
toolCallId: options?.toolCallId ?? name,
|
|
34
|
+
signal: options?.abortSignal ?? new AbortController().signal,
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createWorkspaceCodeExecutionFactory(options: {
|
|
23
41
|
readonly ctx: DurableObjectState;
|
|
24
42
|
readonly loader: WorkerLoader;
|
|
25
43
|
readonly outbound: Fetcher;
|
|
26
44
|
readonly workspace: WorkspacePort;
|
|
27
|
-
}):
|
|
28
|
-
const { tool } = createExecuteRuntime({
|
|
29
|
-
ctx: options.ctx,
|
|
30
|
-
loader: options.loader,
|
|
31
|
-
globalOutbound: options.outbound,
|
|
32
|
-
// 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
|
|
33
|
-
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
34
|
-
state: createWorkspaceStateBackend(
|
|
35
|
-
options.workspace as unknown as WorkspaceFsLike,
|
|
36
|
-
),
|
|
37
|
-
name: "execute",
|
|
38
|
-
});
|
|
39
|
-
const { description, execute } = tool;
|
|
40
|
-
if (typeof description !== "string" || description.length === 0) {
|
|
41
|
-
throw new Error("Think createExecuteRuntime returned a tool without a description");
|
|
42
|
-
}
|
|
43
|
-
if (typeof execute !== "function") {
|
|
44
|
-
throw new Error("Think createExecuteRuntime returned a non-executable tool");
|
|
45
|
-
}
|
|
45
|
+
}): RuntimeCodeExecutionFactory {
|
|
46
46
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
create(tools) {
|
|
48
|
+
const { tool } = createExecuteRuntime({
|
|
49
|
+
ctx: options.ctx,
|
|
50
|
+
loader: options.loader,
|
|
51
|
+
globalOutbound: options.outbound,
|
|
52
|
+
tools: codeExecutionTools(tools),
|
|
53
|
+
// 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
|
|
54
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
55
|
+
state: createWorkspaceStateBackend(
|
|
56
|
+
options.workspace as unknown as WorkspaceFsLike,
|
|
57
|
+
),
|
|
58
|
+
name: "execute",
|
|
59
|
+
});
|
|
60
|
+
const { description, execute } = tool;
|
|
61
|
+
if (typeof description !== "string" || description.length === 0) {
|
|
62
|
+
throw new Error("Think createExecuteRuntime returned a tool without a description");
|
|
63
|
+
}
|
|
64
|
+
if (typeof execute !== "function") {
|
|
65
|
+
throw new Error("Think createExecuteRuntime returned a non-executable tool");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
description,
|
|
69
|
+
execute: (input) => Promise.resolve(execute(input, {
|
|
70
|
+
toolCallId: "execute",
|
|
71
|
+
messages: [],
|
|
72
|
+
context: undefined,
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
},
|
|
53
76
|
};
|
|
54
77
|
}
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -12,10 +12,6 @@ import { serializeOutput } from "../../lib/artifacts";
|
|
|
12
12
|
import type { PiLoadedExtension } from "../assembly/extensions";
|
|
13
13
|
import { aiToolToPi } from "./ai-adapter";
|
|
14
14
|
import type { PiToolCandidate } from "./compiler";
|
|
15
|
-
import {
|
|
16
|
-
toolRegistryFromPiCandidates,
|
|
17
|
-
type ToolRegistry,
|
|
18
|
-
} from "../../tool-registry";
|
|
19
15
|
|
|
20
16
|
// 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
|
|
21
17
|
|
|
@@ -164,19 +160,11 @@ export function codeExecutionPiToolCandidate(
|
|
|
164
160
|
return {
|
|
165
161
|
owner: "core:codemode",
|
|
166
162
|
requiredExecutionLevel: "high",
|
|
163
|
+
outputBudget: { kind: "structure" },
|
|
167
164
|
source: "codemode",
|
|
168
165
|
summary: "Run JavaScript with network and configured connector access",
|
|
169
166
|
tool,
|
|
170
167
|
};
|
|
171
168
|
}
|
|
172
169
|
|
|
173
|
-
/** 从 Codemode Runtime Port 生成 `execute` Tool。 */
|
|
174
|
-
export function createCodeExecutionTool(
|
|
175
|
-
runtime: RuntimeCodeExecutionPort,
|
|
176
|
-
): ToolRegistry {
|
|
177
|
-
return toolRegistryFromPiCandidates([
|
|
178
|
-
codeExecutionPiToolCandidate(runtime),
|
|
179
|
-
]);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
170
|
// #endregion
|
package/src/pi/tool/skill.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runner as createSkillScriptRunner,
|
|
3
3
|
SkillRegistry,
|
|
4
|
+
type SkillResource,
|
|
4
5
|
type SkillScriptRequest,
|
|
5
6
|
type SkillScriptRunner,
|
|
6
7
|
type SkillSource,
|
|
7
8
|
} from "agents/skills";
|
|
8
|
-
import { jsonSchema, type ToolSet } from "ai";
|
|
9
|
+
import { jsonSchema, tool, type ToolSet } from "ai";
|
|
10
|
+
import { z } from "zod";
|
|
9
11
|
import type {
|
|
10
12
|
RuntimeSkillScriptPolicy,
|
|
11
13
|
WorkspacePort,
|
|
12
14
|
} from "../../kernel/bindings";
|
|
13
15
|
import { aiToolToPi } from "./ai-adapter";
|
|
14
16
|
import type { PiToolCandidate } from "./compiler";
|
|
17
|
+
import { serializeWorkspaceMutation } from "./workspace-sandbox";
|
|
15
18
|
|
|
16
19
|
/** A configured Skill source and its script capabilities. */
|
|
17
20
|
export interface PiSkillBinding {
|
|
@@ -103,11 +106,72 @@ const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
|
|
|
103
106
|
activate_skill: "Activate Skill",
|
|
104
107
|
read_skill_resource: "Read Skill resource",
|
|
105
108
|
run_skill_script: "Run Skill script",
|
|
109
|
+
materialize_skill_resource: "Materialize Skill resource",
|
|
106
110
|
};
|
|
107
111
|
|
|
108
112
|
const SKILL_ENTRY_READ_GUIDANCE =
|
|
109
113
|
"SKILL.md contains the Skill instructions; use activate_skill instead.";
|
|
110
114
|
|
|
115
|
+
function resourceBytes(resource: SkillResource): Uint8Array {
|
|
116
|
+
if ((resource.encoding ?? "text") === "text") {
|
|
117
|
+
return new TextEncoder().encode(resource.content);
|
|
118
|
+
}
|
|
119
|
+
const binary = atob(resource.content);
|
|
120
|
+
const bytes = new Uint8Array(binary.length);
|
|
121
|
+
for (let index = 0; index < binary.length; index++) {
|
|
122
|
+
bytes[index] = binary.charCodeAt(index);
|
|
123
|
+
}
|
|
124
|
+
return bytes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function materializeSkillResourceTool(
|
|
128
|
+
bindings: readonly PiSkillBinding[],
|
|
129
|
+
workspace: WorkspacePort,
|
|
130
|
+
) {
|
|
131
|
+
const names = bindings.map(({ name }) => name) as [string, ...string[]];
|
|
132
|
+
const byName = new Map(bindings.map((binding) => [binding.name, binding]));
|
|
133
|
+
return tool({
|
|
134
|
+
description:
|
|
135
|
+
"Copy a complete bundled Skill resource directly into the Workspace without returning its contents to the model. " +
|
|
136
|
+
"Use this instead of read_skill_resource when a template, script, image, font, or other asset must become a Workspace file. " +
|
|
137
|
+
"The destination is created or overwritten, including parent directories.",
|
|
138
|
+
inputSchema: z.object({
|
|
139
|
+
name: z.enum(names).describe("Activated Skill name"),
|
|
140
|
+
path: z.string().min(1).describe("Bundled resource path listed by activate_skill"),
|
|
141
|
+
destination: z.string().min(1).describe("Absolute destination path in the Workspace"),
|
|
142
|
+
}),
|
|
143
|
+
execute: async ({ name, path, destination }, { abortSignal }) => {
|
|
144
|
+
abortSignal?.throwIfAborted();
|
|
145
|
+
const source = byName.get(name)?.source;
|
|
146
|
+
if (!source?.readResource) {
|
|
147
|
+
throw new Error(`Skill \"${name}\" has no readable resources.`);
|
|
148
|
+
}
|
|
149
|
+
const resource = await source.readResource(name, path);
|
|
150
|
+
if (!resource) throw new Error(`Resource not found: ${name}/${path}`);
|
|
151
|
+
abortSignal?.throwIfAborted();
|
|
152
|
+
const bytes = resourceBytes(resource);
|
|
153
|
+
|
|
154
|
+
await serializeWorkspaceMutation(workspace, destination, async () => {
|
|
155
|
+
abortSignal?.throwIfAborted();
|
|
156
|
+
const parent = destination.replace(/\/[^/]+$/, "");
|
|
157
|
+
if (parent && parent !== "/") {
|
|
158
|
+
await workspace.mkdir(parent, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
await workspace.writeFileBytes(destination, bytes, resource.mimeType);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
path: resource.path,
|
|
166
|
+
destination,
|
|
167
|
+
bytesWritten: bytes.byteLength,
|
|
168
|
+
encoding: resource.encoding ?? "text",
|
|
169
|
+
...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
111
175
|
/** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
|
|
112
176
|
export async function skillPiToolCandidates(
|
|
113
177
|
bindings: readonly PiSkillBinding[],
|
|
@@ -121,7 +185,15 @@ export async function skillPiToolCandidates(
|
|
|
121
185
|
);
|
|
122
186
|
await registry.load();
|
|
123
187
|
|
|
124
|
-
|
|
188
|
+
const tools = registry.tools();
|
|
189
|
+
if (options.workspace) {
|
|
190
|
+
tools.materialize_skill_resource = materializeSkillResourceTool(
|
|
191
|
+
bindings,
|
|
192
|
+
options.workspace,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Object.entries(tools).map(([name, tool]) => {
|
|
125
197
|
const adapted = aiToolToPi(name, tool, {
|
|
126
198
|
label: SKILL_TOOL_LABELS[name] ?? name,
|
|
127
199
|
...(name === "read_skill_resource"
|
|
@@ -154,7 +226,10 @@ export async function skillPiToolCandidates(
|
|
|
154
226
|
}
|
|
155
227
|
return {
|
|
156
228
|
owner: "runtime-skill",
|
|
157
|
-
requiredExecutionLevel:
|
|
229
|
+
requiredExecutionLevel:
|
|
230
|
+
name === "run_skill_script" || name === "materialize_skill_resource"
|
|
231
|
+
? "high"
|
|
232
|
+
: "safe",
|
|
158
233
|
tool: adapted,
|
|
159
234
|
};
|
|
160
235
|
});
|
|
@@ -111,7 +111,7 @@ const mutationQueues = new WeakMap<
|
|
|
111
111
|
Map<string, Promise<void>>
|
|
112
112
|
>();
|
|
113
113
|
|
|
114
|
-
function
|
|
114
|
+
export function serializeWorkspaceMutation<T>(
|
|
115
115
|
workspace: WorkspacePort,
|
|
116
116
|
path: string,
|
|
117
117
|
run: () => Promise<T>,
|
|
@@ -193,7 +193,7 @@ export function workspacePiToolCandidates(
|
|
|
193
193
|
signal?.throwIfAborted();
|
|
194
194
|
running(onUpdate, "Writing the Workspace file.");
|
|
195
195
|
|
|
196
|
-
return
|
|
196
|
+
return serializeWorkspaceMutation(workspace, path, async () => {
|
|
197
197
|
// 排队期间可能已经被取消,拿到闸之后必须重新确认,否则会写一个已放弃的结果。
|
|
198
198
|
signal?.throwIfAborted();
|
|
199
199
|
return writeTool.execute(
|
|
@@ -221,7 +221,7 @@ export function workspacePiToolCandidates(
|
|
|
221
221
|
running(onUpdate, "Editing the Workspace file.");
|
|
222
222
|
// 读-比对-写整段都在闸内:闸只包住最后那次 writeFile 的话,基准内容仍然可能在
|
|
223
223
|
// 比对之后被别的调用换掉,丢写照旧发生。
|
|
224
|
-
return
|
|
224
|
+
return serializeWorkspaceMutation(workspace, params.path, async () => {
|
|
225
225
|
signal?.throwIfAborted();
|
|
226
226
|
const output = await editTool.execute(
|
|
227
227
|
toolCallId,
|
|
@@ -267,7 +267,7 @@ export function workspacePiToolCandidates(
|
|
|
267
267
|
if (typeof path !== "string") {
|
|
268
268
|
return removeTool.execute(toolCallId, params, signal, onUpdate);
|
|
269
269
|
}
|
|
270
|
-
return
|
|
270
|
+
return serializeWorkspaceMutation(workspace, path, () =>
|
|
271
271
|
removeTool.execute(toolCallId, params, signal, onUpdate),
|
|
272
272
|
);
|
|
273
273
|
},
|
|
@@ -429,7 +429,7 @@ function assertIdentity(
|
|
|
429
429
|
state.assemblyRevision !== milestone.assemblyRevision)
|
|
430
430
|
) {
|
|
431
431
|
throw new Error(
|
|
432
|
-
"
|
|
432
|
+
"SpringBrand recovery milestone does not belong to the active Turn revision",
|
|
433
433
|
);
|
|
434
434
|
}
|
|
435
435
|
}
|
|
@@ -496,7 +496,7 @@ export function replayPiToolRecovery(
|
|
|
496
496
|
const previous = tools[milestone.toolCallId];
|
|
497
497
|
if (previous && !sameToolInput(previous, milestone)) {
|
|
498
498
|
throw new Error(
|
|
499
|
-
`Conflicting
|
|
499
|
+
`Conflicting SpringBrand Tool input for ${milestone.toolCallId}`,
|
|
500
500
|
);
|
|
501
501
|
}
|
|
502
502
|
tools[milestone.toolCallId] = previous ?? {
|
|
@@ -891,7 +891,7 @@ export function applyRecoveredPiApprovalDecision(
|
|
|
891
891
|
): RecoveredPiApprovalDecision {
|
|
892
892
|
const approval = state.approvals[input.executionId];
|
|
893
893
|
if (!approval) {
|
|
894
|
-
throw new Error(`Unknown
|
|
894
|
+
throw new Error(`Unknown SpringBrand Tool approval ${input.executionId}`);
|
|
895
895
|
}
|
|
896
896
|
const transition = decidePiToolApproval(
|
|
897
897
|
approval,
|
|
@@ -908,7 +908,7 @@ export function applyRecoveredPiApprovalDecision(
|
|
|
908
908
|
return { milestones: [], outcome: transition.outcome };
|
|
909
909
|
}
|
|
910
910
|
if (!state.turnId || !state.assemblyRevision) {
|
|
911
|
-
throw new Error("
|
|
911
|
+
throw new Error("SpringBrand Tool approval is missing its Turn revision");
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
const identity: RecoveryIdentity = {
|
|
@@ -951,7 +951,7 @@ export function recordRecoveredPiToolInteraction(
|
|
|
951
951
|
interaction: PiToolInteraction,
|
|
952
952
|
): PiToolRecoveryMilestone[] {
|
|
953
953
|
if (!state.turnId || !state.assemblyRevision) {
|
|
954
|
-
throw new Error("
|
|
954
|
+
throw new Error("SpringBrand Tool interaction is missing its Turn revision");
|
|
955
955
|
}
|
|
956
956
|
return [{
|
|
957
957
|
version: 1,
|
|
@@ -989,7 +989,7 @@ export function applyRecoveredPiToolInteractionSettlement(
|
|
|
989
989
|
): RecoveredPiToolInteractionSettlement {
|
|
990
990
|
const interaction = state.interactions[input.interactionId];
|
|
991
991
|
if (!interaction) {
|
|
992
|
-
throw new Error(`Unknown
|
|
992
|
+
throw new Error(`Unknown SpringBrand Tool interaction ${input.interactionId}`);
|
|
993
993
|
}
|
|
994
994
|
const transition = input.kind === "respond"
|
|
995
995
|
? respondPiToolInteraction(interaction, {
|
|
@@ -1008,7 +1008,7 @@ export function applyRecoveredPiToolInteractionSettlement(
|
|
|
1008
1008
|
return { milestones: [], outcome };
|
|
1009
1009
|
}
|
|
1010
1010
|
if (!state.turnId || !state.assemblyRevision) {
|
|
1011
|
-
throw new Error("
|
|
1011
|
+
throw new Error("SpringBrand Tool interaction is missing its Turn revision");
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
1014
|
const identity: RecoveryIdentity = {
|
package/src/runtime-agent.ts
CHANGED
|
@@ -652,9 +652,14 @@ export function defineRuntimeAgent<
|
|
|
652
652
|
const policy = assembly.surfacePolicy;
|
|
653
653
|
return {
|
|
654
654
|
...assembly,
|
|
655
|
-
bindings:
|
|
656
|
-
|
|
657
|
-
|
|
655
|
+
bindings: {
|
|
656
|
+
...(assembly.bindings?.workspace
|
|
657
|
+
? { workspace: assembly.bindings.workspace }
|
|
658
|
+
: {}),
|
|
659
|
+
...(assembly.bindings?.codeExecution
|
|
660
|
+
? { codeExecution: assembly.bindings.codeExecution }
|
|
661
|
+
: {}),
|
|
662
|
+
},
|
|
658
663
|
memoryProfile: {
|
|
659
664
|
enabled: false,
|
|
660
665
|
memoryTokens: 2_000,
|