@springbrand/agent-runtime 0.2.0-alpha.42 → 0.2.0-alpha.43
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/subagent/tools.ts +2 -5
- package/src/adapter/cloudflare/universal-agent/preparation.ts +1 -8
- package/src/adapter/cloudflare/universal-agent/tools.ts +0 -2
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +15 -0
- package/src/index.ts +1 -0
- package/src/kernel/bindings.ts +10 -0
- package/src/layers/context/budget/gate.ts +3 -3
- package/src/layers/orchestration/temporary-agent/workspace.ts +2 -0
- package/src/lib/prompt.ts +3 -8
- package/src/pi/runtime-adapter/execution.ts +4 -3
- package/src/pi/runtime-adapter/models.ts +9 -6
- package/src/pi/runtime-adapter/openrouter-messages.ts +10 -3
- package/src/pi/tool/base.ts +16 -12
- package/src/pi/tool/compiler.ts +15 -6
- package/src/pi/tool/core-host.ts +141 -56
- package/src/pi/tool/core.ts +3 -2
- package/src/pi/tool/declared.ts +3 -2
- package/src/pi/tool/nested-tools.ts +5 -1
- package/src/pi/tool/schedule.ts +12 -12
- package/src/pi/tool/skill.ts +179 -14
- package/src/pi/tool/subagent.ts +2 -0
- package/src/pi/tool/time.ts +1 -1
- package/src/pi/tool/web-fetch.ts +1 -1
- package/src/pi/tool/web-search/web-search.ts +2 -1
- package/src/pi/tool/workspace-revision.ts +2 -1
- package/src/pi/tool/workspace-sandbox.ts +10 -21
- package/src/runtime-assembler.ts +13 -6
- package/src/runtime.ts +28 -0
package/src/pi/tool/core-host.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createWorkspaceStateBackend,
|
|
3
|
+
type StateBackend,
|
|
3
4
|
type WorkspaceFsLike,
|
|
4
5
|
} from "@cloudflare/shell";
|
|
6
|
+
import {
|
|
7
|
+
truncateResponse,
|
|
8
|
+
truncateResult,
|
|
9
|
+
type ProxyToolOutput,
|
|
10
|
+
} from "@cloudflare/codemode";
|
|
5
11
|
import { createBrowserTools } from "@cloudflare/think/tools/browser";
|
|
6
12
|
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
|
|
7
13
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
@@ -20,41 +26,77 @@ import { piCandidatesToAiTools } from "./nested-tools";
|
|
|
20
26
|
// 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
|
|
21
27
|
|
|
22
28
|
const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
|
|
29
|
+
export type WorkspaceAccessMode = "none" | "read" | "write";
|
|
30
|
+
const WRITE_STATE_METHODS = new Set([
|
|
31
|
+
"writeFile",
|
|
32
|
+
"writeFileBytes",
|
|
33
|
+
"appendFile",
|
|
34
|
+
"writeJson",
|
|
35
|
+
"updateJson",
|
|
36
|
+
"mkdir",
|
|
37
|
+
"replaceInFile",
|
|
38
|
+
"replaceInFiles",
|
|
39
|
+
"rm",
|
|
40
|
+
"cp",
|
|
41
|
+
"mv",
|
|
42
|
+
"symlink",
|
|
43
|
+
"createArchive",
|
|
44
|
+
"extractArchive",
|
|
45
|
+
"compressFile",
|
|
46
|
+
"decompressFile",
|
|
47
|
+
"removeTree",
|
|
48
|
+
"copyTree",
|
|
49
|
+
"moveTree",
|
|
50
|
+
"planEdits",
|
|
51
|
+
"applyEditPlan",
|
|
52
|
+
"applyEdits",
|
|
53
|
+
]);
|
|
23
54
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
55
|
+
export function workspaceStateBackend(
|
|
56
|
+
workspace: WorkspacePort,
|
|
57
|
+
mode: WorkspaceAccessMode,
|
|
58
|
+
): StateBackend | undefined {
|
|
59
|
+
if (mode === "none") return undefined;
|
|
60
|
+
const backend = createWorkspaceStateBackend(
|
|
61
|
+
workspace as unknown as WorkspaceFsLike,
|
|
62
|
+
);
|
|
63
|
+
return new Proxy(backend, {
|
|
64
|
+
get(target, property, receiver) {
|
|
65
|
+
if (property === "writeFileBytes") return undefined;
|
|
66
|
+
if (
|
|
67
|
+
mode === "read" &&
|
|
68
|
+
typeof property === "string" &&
|
|
69
|
+
WRITE_STATE_METHODS.has(property)
|
|
70
|
+
) {
|
|
71
|
+
return async () => {
|
|
72
|
+
throw new Error("Workspace state is read-only for this execution");
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const value = Reflect.get(target, property, receiver);
|
|
76
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
77
|
+
},
|
|
78
|
+
}) as StateBackend;
|
|
27
79
|
}
|
|
28
80
|
|
|
29
|
-
|
|
30
|
-
candidates: readonly PiToolCandidate[],
|
|
31
|
-
): string {
|
|
32
|
-
const list = (
|
|
33
|
-
entries: readonly { readonly name: string; readonly label: string }[],
|
|
34
|
-
) => entries.length > 0
|
|
35
|
-
? entries.map(({ name, label }) => directoryEntry(name, label)).join("\n")
|
|
36
|
-
: "- None.";
|
|
81
|
+
const SPRINGBRAND_CODE_MODE_POLICY = `## SpringBrand Workspace and Asset Policy
|
|
37
82
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"There is no Node.js `require`, `process`, package manager, or Python runtime.",
|
|
56
|
-
].join("\n");
|
|
57
|
-
}
|
|
83
|
+
Use state.* for every Workspace filesystem operation. Workspace file methods do not exist under tools.*.
|
|
84
|
+
|
|
85
|
+
state.writeFileBytes is not available. Use state.writeFile or state.appendFile for sandbox-created text.
|
|
86
|
+
|
|
87
|
+
Bundled Skill resources are not Workspace files. Read text with tools.read_skill_resource, run bundled scripts with tools.run_skill_script, and copy templates or assets with tools.materialize_skill_resource.
|
|
88
|
+
|
|
89
|
+
User Attachments are not Workspace files until copied with tools.materialize_attachment.
|
|
90
|
+
|
|
91
|
+
Never create or write data URLs, base64 image strings, or embedded binary payloads in Code Mode.
|
|
92
|
+
|
|
93
|
+
Copy a remote asset into Workspace with tools.materialize_url({ url, destination }). Raw fetch remains available. Do not carry large responses, binary payloads, data URLs, or base64 assets through Code Mode.
|
|
94
|
+
|
|
95
|
+
After materialization, reference the Workspace file by a relative path. Website HTML and CSS must reference relative Workspace assets, never temporary external URLs or data URLs.
|
|
96
|
+
|
|
97
|
+
Keep large content out of the execute result. Write it to Workspace and return only path, mediaType, bytes, summary, and warnings.
|
|
98
|
+
|
|
99
|
+
This execute tool cannot ask the user, inspect a rendered page, capture a screenshot, save or publish a Creation, or authorize an external action. Use the corresponding top-level Tool for those operations.`;
|
|
58
100
|
|
|
59
101
|
/**
|
|
60
102
|
* 宿主的 Browser Rendering 绑定,只取本仓真正用到的那一面。
|
|
@@ -130,6 +172,42 @@ function codeExecutionResult(
|
|
|
130
172
|
};
|
|
131
173
|
}
|
|
132
174
|
|
|
175
|
+
function projectCodeModeResult(details: unknown): unknown {
|
|
176
|
+
if (typeof details !== "object" || details === null) {
|
|
177
|
+
return { status: "error", executionId: "unknown", error: truncateResponse(String(details)) };
|
|
178
|
+
}
|
|
179
|
+
const output = details as ProxyToolOutput;
|
|
180
|
+
const calls = Array.isArray(output.calls) ? output.calls : [];
|
|
181
|
+
const callSummary = calls.length > 0
|
|
182
|
+
? {
|
|
183
|
+
total: calls.length,
|
|
184
|
+
failed: calls.filter(({ state }) => state === "error").length,
|
|
185
|
+
methods: [...new Set(calls.map(({ method }) => method))]
|
|
186
|
+
.map((method) => truncateResponse(method, { maxChars: 256 }))
|
|
187
|
+
.slice(0, 100),
|
|
188
|
+
}
|
|
189
|
+
: undefined;
|
|
190
|
+
const common = {
|
|
191
|
+
status: output.status,
|
|
192
|
+
executionId: output.executionId,
|
|
193
|
+
...(callSummary ? { callSummary } : {}),
|
|
194
|
+
};
|
|
195
|
+
if (output.status === "completed") {
|
|
196
|
+
return { ...common, result: truncateResult(output.result) };
|
|
197
|
+
}
|
|
198
|
+
if (output.status === "paused") {
|
|
199
|
+
return {
|
|
200
|
+
...common,
|
|
201
|
+
pending: output.pending.slice(0, 100).map(({ connector, method, seq }) => ({
|
|
202
|
+
connector: truncateResponse(connector, { maxChars: 256 }),
|
|
203
|
+
method: truncateResponse(method, { maxChars: 256 }),
|
|
204
|
+
seq,
|
|
205
|
+
})),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { ...common, error: truncateResponse(output.error) };
|
|
209
|
+
}
|
|
210
|
+
|
|
133
211
|
// 上游把 Code Mode 类工具交付为 AI SDK tool;本仓只取 description 和 execute 两件,
|
|
134
212
|
// 并在这里就地校验,使装配期缺件立刻失败,而不是等模型调用时才炸。
|
|
135
213
|
function toCodeExecutionPort(
|
|
@@ -246,35 +324,42 @@ export function createWorkspaceCodeExecutionFactory(options: {
|
|
|
246
324
|
readonly loader: WorkerLoader;
|
|
247
325
|
readonly outbound: Fetcher;
|
|
248
326
|
readonly workspace: WorkspacePort;
|
|
327
|
+
readonly workspaceAccessMode: WorkspaceAccessMode;
|
|
249
328
|
}): RuntimeCodeExecutionFactory {
|
|
250
329
|
return {
|
|
251
330
|
create(candidates) {
|
|
252
|
-
const
|
|
331
|
+
const state = workspaceStateBackend(
|
|
332
|
+
options.workspace,
|
|
333
|
+
options.workspaceAccessMode,
|
|
334
|
+
);
|
|
335
|
+
const createPort = (innerResults: AgentToolResult<unknown>[]) => {
|
|
336
|
+
let nestedCallOrdinal = 0;
|
|
337
|
+
const { tool } = createExecuteRuntime({
|
|
338
|
+
ctx: options.ctx,
|
|
339
|
+
loader: options.loader,
|
|
340
|
+
globalOutbound: options.outbound,
|
|
341
|
+
tools: piCandidatesToAiTools(candidates, {
|
|
342
|
+
fallbackToolCallId: (name) =>
|
|
343
|
+
`codemode:${++nestedCallOrdinal}:${name}`,
|
|
344
|
+
onResult: (value) => innerResults.push(value),
|
|
345
|
+
}),
|
|
346
|
+
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
347
|
+
...(state ? { state } : {}),
|
|
348
|
+
name: "execute",
|
|
349
|
+
});
|
|
350
|
+
return toCodeExecutionPort(
|
|
351
|
+
tool,
|
|
352
|
+
"execute",
|
|
353
|
+
(details) => codeExecutionResult(
|
|
354
|
+
projectCodeModeResult(details),
|
|
355
|
+
innerResults,
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
const description = createPort([]).description;
|
|
253
360
|
return {
|
|
254
|
-
description
|
|
255
|
-
|
|
256
|
-
const innerResults: AgentToolResult<unknown>[] = [];
|
|
257
|
-
const { tool } = createExecuteRuntime({
|
|
258
|
-
ctx: options.ctx,
|
|
259
|
-
loader: options.loader,
|
|
260
|
-
globalOutbound: options.outbound,
|
|
261
|
-
tools: piCandidatesToAiTools(candidates, {
|
|
262
|
-
onResult: (value) => innerResults.push(value),
|
|
263
|
-
}),
|
|
264
|
-
description,
|
|
265
|
-
// 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
|
|
266
|
-
timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
|
|
267
|
-
state: createWorkspaceStateBackend(
|
|
268
|
-
options.workspace as unknown as WorkspaceFsLike,
|
|
269
|
-
),
|
|
270
|
-
name: "execute",
|
|
271
|
-
});
|
|
272
|
-
return toCodeExecutionPort(
|
|
273
|
-
tool,
|
|
274
|
-
"execute",
|
|
275
|
-
(details) => codeExecutionResult(details, innerResults),
|
|
276
|
-
).execute(input);
|
|
277
|
-
},
|
|
361
|
+
description: `${description}\n\n${SPRINGBRAND_CODE_MODE_POLICY}`,
|
|
362
|
+
execute: (input) => createPort([]).execute(input),
|
|
278
363
|
};
|
|
279
364
|
},
|
|
280
365
|
};
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -165,7 +165,7 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
|
|
|
165
165
|
|
|
166
166
|
// #region Extension discovery
|
|
167
167
|
|
|
168
|
-
const noParameters = Type.Object({});
|
|
168
|
+
const noParameters = Type.Object({}, { additionalProperties: false });
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
171
|
* 为当前已加载 Extension 列表创建一个 Pi 工具候选项。
|
|
@@ -207,10 +207,11 @@ export function listExtensionsPiToolCandidate(
|
|
|
207
207
|
const executeParameters = Type.Object({
|
|
208
208
|
code: Type.String({
|
|
209
209
|
minLength: 1,
|
|
210
|
+
maxLength: 131_072,
|
|
210
211
|
description:
|
|
211
212
|
"Plain JavaScript async function. TypeScript annotations are not supported.",
|
|
212
213
|
}),
|
|
213
|
-
});
|
|
214
|
+
}, { additionalProperties: false });
|
|
214
215
|
const CODEMODE_EXECUTE_TIMEOUT_MS = 60_000;
|
|
215
216
|
|
|
216
217
|
// 把模型提供的代码交给 Codemode Runtime 执行,并在外层再压一道截止。
|
package/src/pi/tool/declared.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface PiDeclaredToolPolicy<Reply = unknown> {
|
|
|
49
49
|
readonly modelName: string;
|
|
50
50
|
readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
|
|
51
51
|
readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
|
|
52
|
-
readonly
|
|
52
|
+
readonly exposureMode?: PiToolCandidate["exposureMode"];
|
|
53
53
|
readonly retry?: PiToolCandidate["retry"];
|
|
54
54
|
readonly source?: PiToolCandidate["source"];
|
|
55
55
|
readonly summary?: string;
|
|
@@ -105,6 +105,7 @@ export function createPiDeclaredToolCandidate<Reply = unknown>(
|
|
|
105
105
|
parameters: structuredClone(
|
|
106
106
|
declared.inputSchema ?? { type: "object", additionalProperties: true },
|
|
107
107
|
) as AgentTool["parameters"],
|
|
108
|
+
constrainedSampling: false,
|
|
108
109
|
execute: async (toolCallId, input, signal) =>
|
|
109
110
|
project(await call(input as Record<string, unknown>, { toolCallId, signal })),
|
|
110
111
|
};
|
|
@@ -116,7 +117,7 @@ export function createPiDeclaredToolCandidate<Reply = unknown>(
|
|
|
116
117
|
...(policy.requiredExecutionLevelForInput
|
|
117
118
|
? { requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput }
|
|
118
119
|
: {}),
|
|
119
|
-
...(policy.
|
|
120
|
+
...(policy.exposureMode ? { exposureMode: policy.exposureMode } : {}),
|
|
120
121
|
...(policy.retry ? { retry: policy.retry } : {}),
|
|
121
122
|
...(policy.source ? { source: policy.source } : {}),
|
|
122
123
|
};
|
|
@@ -35,7 +35,11 @@ export function piCandidatesToAiTools(
|
|
|
35
35
|
call?.abortSignal ?? new AbortController().signal,
|
|
36
36
|
);
|
|
37
37
|
options.onResult?.(result);
|
|
38
|
-
return result;
|
|
38
|
+
if (result.details !== undefined) return result.details;
|
|
39
|
+
const [only] = result.content;
|
|
40
|
+
return result.content.length === 1 && only?.type === "text"
|
|
41
|
+
? only.text
|
|
42
|
+
: result.content;
|
|
39
43
|
},
|
|
40
44
|
}),
|
|
41
45
|
]));
|
package/src/pi/tool/schedule.ts
CHANGED
|
@@ -18,28 +18,28 @@ const scheduleTriggerParameters = Type.Union([
|
|
|
18
18
|
minimum: 1,
|
|
19
19
|
description: "Fire once, this many seconds from now.",
|
|
20
20
|
}),
|
|
21
|
-
}),
|
|
21
|
+
}, { additionalProperties: false }),
|
|
22
22
|
Type.Object({
|
|
23
23
|
kind: Type.Literal("once-at"),
|
|
24
24
|
at: Type.String({
|
|
25
25
|
description:
|
|
26
26
|
"Fire once at this absolute UTC ISO time, e.g. 2026-07-20T09:00:00Z.",
|
|
27
27
|
}),
|
|
28
|
-
}),
|
|
28
|
+
}, { additionalProperties: false }),
|
|
29
29
|
Type.Object({
|
|
30
30
|
kind: Type.Literal("cron"),
|
|
31
31
|
cron: Type.String({
|
|
32
32
|
description:
|
|
33
33
|
"Repeat on a standard UTC cron, e.g. '0 9 * * *' = daily 09:00 UTC.",
|
|
34
34
|
}),
|
|
35
|
-
}),
|
|
35
|
+
}, { additionalProperties: false }),
|
|
36
36
|
Type.Object({
|
|
37
37
|
kind: Type.Literal("interval"),
|
|
38
38
|
seconds: Type.Integer({
|
|
39
39
|
minimum: 1,
|
|
40
40
|
description: "Repeat every this many seconds.",
|
|
41
41
|
}),
|
|
42
|
-
}),
|
|
42
|
+
}, { additionalProperties: false }),
|
|
43
43
|
], { description: "When to fire." });
|
|
44
44
|
|
|
45
45
|
const scheduleParameters = Type.Object({
|
|
@@ -56,21 +56,21 @@ const scheduleParameters = Type.Object({
|
|
|
56
56
|
description:
|
|
57
57
|
"IANA timezone the clock times in `trigger` should be interpreted in, e.g. 'Asia/Shanghai'. Pass the user's own timezone whenever they state a wall-clock time. Omit only for delays and intervals. Defaults to UTC.",
|
|
58
58
|
})),
|
|
59
|
-
});
|
|
59
|
+
}, { additionalProperties: false });
|
|
60
60
|
|
|
61
|
-
const noParameters = Type.Object({});
|
|
61
|
+
const noParameters = Type.Object({}, { additionalProperties: false });
|
|
62
62
|
const scheduleIdParameters = Type.Object({
|
|
63
63
|
id: Type.String({
|
|
64
64
|
description: "The schedule id, as returned by `list_schedules`.",
|
|
65
65
|
}),
|
|
66
|
-
});
|
|
66
|
+
}, { additionalProperties: false });
|
|
67
67
|
const updateScheduleParameters = Type.Object({
|
|
68
68
|
id: Type.String({ description: "The exact id returned by `list_schedules`." }),
|
|
69
69
|
prompt: Type.Optional(Type.String()),
|
|
70
70
|
trigger: Type.Optional(scheduleTriggerParameters),
|
|
71
71
|
label: Type.Optional(Type.String()),
|
|
72
72
|
timezone: Type.Optional(Type.String()),
|
|
73
|
-
});
|
|
73
|
+
}, { additionalProperties: false });
|
|
74
74
|
const changeScheduleAgentParameters = Type.Object({
|
|
75
75
|
id: Type.String({
|
|
76
76
|
description: "The exact schedule id returned by `list_schedules`.",
|
|
@@ -78,7 +78,7 @@ const changeScheduleAgentParameters = Type.Object({
|
|
|
78
78
|
userAgentId: Type.String({
|
|
79
79
|
description: "The exact User Agent id selected by the user.",
|
|
80
80
|
}),
|
|
81
|
-
});
|
|
81
|
+
}, { additionalProperties: false });
|
|
82
82
|
|
|
83
83
|
function result<T>(details: T): AgentToolResult<T> {
|
|
84
84
|
return {
|
|
@@ -92,19 +92,19 @@ function candidate<T extends TSchema>(
|
|
|
92
92
|
options: Partial<
|
|
93
93
|
Pick<
|
|
94
94
|
PiToolCandidate,
|
|
95
|
-
"alwaysRequiresApproval" | "
|
|
95
|
+
"alwaysRequiresApproval" | "exposureMode" | "owner" | "requiredExecutionLevel" | "summary"
|
|
96
96
|
>
|
|
97
97
|
> = {},
|
|
98
98
|
): PiToolCandidate {
|
|
99
99
|
return {
|
|
100
100
|
owner: options.owner ?? "runtime-base",
|
|
101
|
+
exposureMode: options.exposureMode ?? "direct",
|
|
101
102
|
requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
|
|
102
103
|
source: "action",
|
|
103
104
|
tool,
|
|
104
105
|
...(options.alwaysRequiresApproval
|
|
105
106
|
? { alwaysRequiresApproval: true }
|
|
106
107
|
: {}),
|
|
107
|
-
...(options.direct ? { direct: true } : {}),
|
|
108
108
|
...(options.summary ? { summary: options.summary } : {}),
|
|
109
109
|
};
|
|
110
110
|
}
|
|
@@ -134,7 +134,7 @@ export function schedulePiToolCandidates(
|
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
alwaysRequiresApproval: true,
|
|
137
|
-
|
|
137
|
+
exposureMode: "direct",
|
|
138
138
|
summary: "Create a scheduled task",
|
|
139
139
|
},
|
|
140
140
|
),
|
package/src/pi/tool/skill.ts
CHANGED
|
@@ -6,12 +6,15 @@ import {
|
|
|
6
6
|
type SkillScriptRunner,
|
|
7
7
|
type SkillSource,
|
|
8
8
|
} from "agents/skills";
|
|
9
|
+
import { truncateResponse, truncateResult } from "@cloudflare/codemode";
|
|
9
10
|
import { tool } from "ai";
|
|
10
11
|
import { z } from "zod";
|
|
11
12
|
import type {
|
|
12
13
|
RuntimeSkillScriptPolicy,
|
|
13
14
|
WorkspacePort,
|
|
14
15
|
} from "../../kernel/bindings";
|
|
16
|
+
import { STORAGE_LEAF_MAX_CHARS } from "../../layers/context/budget/gate";
|
|
17
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
15
18
|
import { aiToolToPi } from "./ai-adapter";
|
|
16
19
|
import type { PiToolCandidate } from "./compiler";
|
|
17
20
|
import { piCandidatesToAiTools } from "./nested-tools";
|
|
@@ -48,9 +51,10 @@ type LoadedSkill = NonNullable<Awaited<ReturnType<SkillSource["load"]>>>;
|
|
|
48
51
|
|
|
49
52
|
const SKILL_RESOURCE_READ_GUIDANCE =
|
|
50
53
|
"Bundled Skill resources are not Workspace files. " +
|
|
51
|
-
"
|
|
54
|
+
"Inside execute, use tools.read_skill_resource for text instead of state.* on /skills.";
|
|
52
55
|
const SKILL_RESOURCE_MATERIALIZE_GUIDANCE =
|
|
53
|
-
"
|
|
56
|
+
"Inside execute, use tools.materialize_skill_resource for Workspace assets when available.";
|
|
57
|
+
const SKILL_SCRIPT_OUTPUT_ROOT = "/scratch/skill-output";
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
60
|
* 按体积裁掉超预算的 Skill 资源,并把裁掉的事实写回 Skill 正文。
|
|
@@ -227,6 +231,127 @@ function normalizeRunSkillScriptArguments(input: unknown): unknown {
|
|
|
227
231
|
}
|
|
228
232
|
}
|
|
229
233
|
|
|
234
|
+
function result(details: unknown) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text" as const, text: serializeOutput(details).text }],
|
|
237
|
+
details,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resourceTarget(
|
|
242
|
+
bindings: readonly PiSkillBinding[],
|
|
243
|
+
input: unknown,
|
|
244
|
+
): { binding: PiSkillBinding; path: string } | null {
|
|
245
|
+
if (input === null || typeof input !== "object") return null;
|
|
246
|
+
const target = input as { name?: unknown; path?: unknown };
|
|
247
|
+
if (typeof target.path !== "string") return null;
|
|
248
|
+
if (typeof target.name === "string") {
|
|
249
|
+
const binding = bindings.find(({ name }) => name === target.name);
|
|
250
|
+
return binding ? { binding, path: target.path } : null;
|
|
251
|
+
}
|
|
252
|
+
const [name, ...rest] = target.path.split("/");
|
|
253
|
+
const binding = bindings.find((candidate) => candidate.name === name);
|
|
254
|
+
return binding && rest.length > 0
|
|
255
|
+
? { binding, path: rest.join("/") }
|
|
256
|
+
: null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function materializationRequired(name: string, path: string, bytes?: number) {
|
|
260
|
+
return result({
|
|
261
|
+
status: "materialization_required",
|
|
262
|
+
name,
|
|
263
|
+
path,
|
|
264
|
+
...(bytes === undefined ? {} : { bytes }),
|
|
265
|
+
next: "tools.materialize_skill_resource",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type ScriptOutputFile = {
|
|
270
|
+
path?: unknown;
|
|
271
|
+
content?: unknown;
|
|
272
|
+
encoding?: unknown;
|
|
273
|
+
mimeType?: unknown;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
function scratchSegment(value: string): string {
|
|
277
|
+
const segment = value.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
278
|
+
return !segment || segment === "." || segment === ".." ? "_" : segment;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function outputRelativePath(value: unknown): string {
|
|
282
|
+
const path = String(value ?? "output.txt").replace(/^\/?output\//, "");
|
|
283
|
+
const parts = path.split("/");
|
|
284
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
285
|
+
throw new Error(`Invalid Skill output path: ${path}`);
|
|
286
|
+
}
|
|
287
|
+
return parts.join("/");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function projectSkillScriptResult(
|
|
291
|
+
details: unknown,
|
|
292
|
+
input: unknown,
|
|
293
|
+
toolCallId: string,
|
|
294
|
+
workspace: WorkspacePort | undefined,
|
|
295
|
+
signal: AbortSignal | undefined,
|
|
296
|
+
): Promise<unknown> {
|
|
297
|
+
const record = details !== null && typeof details === "object"
|
|
298
|
+
? details as Record<string, unknown>
|
|
299
|
+
: null;
|
|
300
|
+
const outputFiles = Array.isArray(record?.outputFiles)
|
|
301
|
+
? record.outputFiles as ScriptOutputFile[]
|
|
302
|
+
: [];
|
|
303
|
+
const target = input as { name?: unknown };
|
|
304
|
+
const outputs: Array<{ path: string; bytes: number; mediaType?: string }> = [];
|
|
305
|
+
if (outputFiles.length > 0 && !workspace) {
|
|
306
|
+
throw new Error("Skill output files require Workspace access");
|
|
307
|
+
}
|
|
308
|
+
for (const file of outputFiles) {
|
|
309
|
+
signal?.throwIfAborted();
|
|
310
|
+
const relative = outputRelativePath(file.path);
|
|
311
|
+
const destination = `${SKILL_SCRIPT_OUTPUT_ROOT}/${scratchSegment(String(target.name ?? "skill"))}/${scratchSegment(toolCallId)}/${relative}`;
|
|
312
|
+
const content = String(file.content ?? "");
|
|
313
|
+
const binary = file.encoding === "base64";
|
|
314
|
+
const mediaType = typeof file.mimeType === "string"
|
|
315
|
+
? file.mimeType
|
|
316
|
+
: binary ? "application/octet-stream" : "text/plain";
|
|
317
|
+
let bytes = new Blob([content]).size;
|
|
318
|
+
await serializeWorkspaceMutation(workspace!, destination, async () => {
|
|
319
|
+
const parent = destination.replace(/\/[^/]+$/, "");
|
|
320
|
+
await workspace!.mkdir(parent, { recursive: true });
|
|
321
|
+
if (!binary) {
|
|
322
|
+
await workspace!.writeFile(destination, content, mediaType);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const decoded = resourceBytes({
|
|
326
|
+
path: relative,
|
|
327
|
+
kind: "file",
|
|
328
|
+
encoding: "base64",
|
|
329
|
+
content,
|
|
330
|
+
});
|
|
331
|
+
bytes = decoded.byteLength;
|
|
332
|
+
await workspace!.writeFileBytes(destination, decoded, mediaType);
|
|
333
|
+
});
|
|
334
|
+
outputs.push({ path: destination, bytes, mediaType });
|
|
335
|
+
}
|
|
336
|
+
const scriptResult = record && "result" in record
|
|
337
|
+
? record.result
|
|
338
|
+
: record && ("outputFiles" in record || "logs" in record)
|
|
339
|
+
? Object.fromEntries(Object.entries(record).filter(([key]) =>
|
|
340
|
+
key !== "outputFiles" && key !== "logs"
|
|
341
|
+
))
|
|
342
|
+
: details;
|
|
343
|
+
const logs = Array.isArray(record?.logs)
|
|
344
|
+
? record.logs.map(String)
|
|
345
|
+
: [];
|
|
346
|
+
return {
|
|
347
|
+
result: truncateResult(scriptResult),
|
|
348
|
+
...(logs.length > 0
|
|
349
|
+
? { logs: [truncateResponse(logs.join("\n"))] }
|
|
350
|
+
: {}),
|
|
351
|
+
...(outputs.length > 0 ? { outputs } : {}),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
230
355
|
function resourceBytes(resource: SkillResource): Uint8Array {
|
|
231
356
|
if ((resource.encoding ?? "text") === "text") {
|
|
232
357
|
return new TextEncoder().encode(resource.content);
|
|
@@ -249,7 +374,7 @@ function materializeSkillResourceTool(
|
|
|
249
374
|
description:
|
|
250
375
|
"Copy a complete bundled Skill resource directly into the Workspace without returning its contents to the model. " +
|
|
251
376
|
"Use this instead of read_skill_resource when a template, script, image, font, or other asset must become a Workspace file. " +
|
|
252
|
-
"When multiple resources are needed, copy them in one execute
|
|
377
|
+
"When multiple resources are needed, copy them sequentially in one execute; never start one execute per resource. " +
|
|
253
378
|
"The destination is created or overwritten, including parent directories.",
|
|
254
379
|
inputSchema: z.object({
|
|
255
380
|
name: z.enum(names).describe("Activated Skill name"),
|
|
@@ -332,7 +457,7 @@ export async function skillPiToolCandidates(
|
|
|
332
457
|
}
|
|
333
458
|
if (name === "read_skill_resource") {
|
|
334
459
|
const execute = adapted.execute;
|
|
335
|
-
adapted.execute = (toolCallId, input, signal) => {
|
|
460
|
+
adapted.execute = async (toolCallId, input, signal) => {
|
|
336
461
|
const target = input as { name?: unknown; path?: unknown };
|
|
337
462
|
const parts = typeof target.path === "string"
|
|
338
463
|
? target.path.split("/")
|
|
@@ -343,32 +468,72 @@ export async function skillPiToolCandidates(
|
|
|
343
468
|
parts.length === 2 &&
|
|
344
469
|
parts[1] === "SKILL.md")
|
|
345
470
|
) {
|
|
346
|
-
return
|
|
471
|
+
return {
|
|
347
472
|
content: [{ type: "text", text: SKILL_ENTRY_READ_GUIDANCE }],
|
|
348
473
|
details: SKILL_ENTRY_READ_GUIDANCE,
|
|
349
|
-
}
|
|
474
|
+
};
|
|
350
475
|
}
|
|
351
|
-
|
|
476
|
+
const resolved = resourceTarget(bindings, input);
|
|
477
|
+
if (resolved) {
|
|
478
|
+
const skill = await resolved.binding.source
|
|
479
|
+
.load(resolved.binding.name)
|
|
480
|
+
.catch(() => null);
|
|
481
|
+
const descriptor = skill?.resources?.find(({ path }) =>
|
|
482
|
+
path === resolved.path
|
|
483
|
+
);
|
|
484
|
+
if (
|
|
485
|
+
typeof descriptor?.size === "number" &&
|
|
486
|
+
descriptor.size > STORAGE_LEAF_MAX_CHARS
|
|
487
|
+
) {
|
|
488
|
+
return materializationRequired(
|
|
489
|
+
resolved.binding.name,
|
|
490
|
+
resolved.path,
|
|
491
|
+
descriptor.size,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const output = await execute(toolCallId, input, signal);
|
|
496
|
+
if (
|
|
497
|
+
resolved &&
|
|
498
|
+
typeof output.details === "string" &&
|
|
499
|
+
output.details.length > STORAGE_LEAF_MAX_CHARS
|
|
500
|
+
) {
|
|
501
|
+
return materializationRequired(
|
|
502
|
+
resolved.binding.name,
|
|
503
|
+
resolved.path,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
return output;
|
|
352
507
|
};
|
|
353
508
|
}
|
|
354
509
|
if (name === "run_skill_script") {
|
|
510
|
+
adapted.description +=
|
|
511
|
+
" Large artifacts must be written directly to Workspace, not returned or logged; scratch outputs are materialized to Workspace and returned only as file references.";
|
|
355
512
|
adapted.prepareArguments = normalizeRunSkillScriptArguments;
|
|
513
|
+
const execute = adapted.execute;
|
|
514
|
+
adapted.execute = async (toolCallId, input, signal) => {
|
|
515
|
+
const output = await execute(toolCallId, input, signal);
|
|
516
|
+
return result(await projectSkillScriptResult(
|
|
517
|
+
output.details,
|
|
518
|
+
input,
|
|
519
|
+
toolCallId,
|
|
520
|
+
options.workspace,
|
|
521
|
+
signal,
|
|
522
|
+
));
|
|
523
|
+
};
|
|
356
524
|
}
|
|
357
525
|
return {
|
|
358
526
|
owner: "runtime-skill",
|
|
359
|
-
requiredExecutionLevel:
|
|
360
|
-
name === "run_skill_script" || name === "materialize_skill_resource"
|
|
361
|
-
? "high"
|
|
362
|
-
: "safe",
|
|
527
|
+
requiredExecutionLevel: "safe",
|
|
363
528
|
// activate_skill 返回的是 Skill 指令本身,是模型接下来所有动作的依据。把它外置
|
|
364
529
|
// 成文件,模型手里就只剩一个路径,必须再取一次才能知道该做什么 —— 而典型
|
|
365
530
|
// SKILL.md 正好落在会触发外置的区间。指令必须当场到手。
|
|
366
531
|
...(name === "activate_skill"
|
|
367
532
|
? { outputBudget: { kind: "structure" as const } }
|
|
368
533
|
: {}),
|
|
369
|
-
...(name === "
|
|
370
|
-
? {
|
|
371
|
-
: {}),
|
|
534
|
+
...(name === "activate_skill"
|
|
535
|
+
? { exposureMode: "direct" as const }
|
|
536
|
+
: { exposureMode: "codemode" as const }),
|
|
372
537
|
tool: adapted,
|
|
373
538
|
};
|
|
374
539
|
});
|
package/src/pi/tool/subagent.ts
CHANGED
|
@@ -180,6 +180,7 @@ export function subagentPiToolCandidates(
|
|
|
180
180
|
};
|
|
181
181
|
return {
|
|
182
182
|
owner: `subagent:${type.name}`,
|
|
183
|
+
exposureMode: "direct",
|
|
183
184
|
requiredExecutionLevel: "safe",
|
|
184
185
|
tool,
|
|
185
186
|
};
|
|
@@ -262,6 +263,7 @@ export function subagentPiToolCandidates(
|
|
|
262
263
|
};
|
|
263
264
|
candidates.push({
|
|
264
265
|
owner: "subagent:background",
|
|
266
|
+
exposureMode: "direct",
|
|
265
267
|
requiredExecutionLevel: "low",
|
|
266
268
|
summary: "Dispatch a background sub-agent",
|
|
267
269
|
source: "action",
|
package/src/pi/tool/time.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { PiToolCandidate } from "./compiler";
|
|
|
4
4
|
|
|
5
5
|
export const GET_TIME_TOOL_NAME = "get_time";
|
|
6
6
|
|
|
7
|
-
const parameters = Type.Object({});
|
|
7
|
+
const parameters = Type.Object({}, { additionalProperties: false });
|
|
8
8
|
|
|
9
9
|
/** Product-neutral current-time Tool for Host registries. */
|
|
10
10
|
export function getTimePiToolCandidate(owner = "utilities"): PiToolCandidate {
|