@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.2
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/db/schema.ts +10 -1
- package/src/db/submission.repo.ts +34 -1
- package/src/index.ts +9 -11
- package/src/kernel/bindings.ts +27 -6
- package/src/kernel/extensions.ts +1 -1
- package/src/kernel/profile.ts +3 -4
- package/src/kernel/recoverable-chat-agent.ts +82 -4
- package/src/kernel/state.ts +4 -0
- package/src/kernel/submission-lifecycle.ts +3 -2
- package/src/layers/orchestration/temporary-agent/runner.ts +0 -65
- package/src/lib/prompt.ts +1 -1
- package/src/pi/assembly/context.ts +1 -3
- package/src/pi/assembly/snapshot.ts +17 -9
- package/src/pi/runtime-adapter/assembly.ts +25 -86
- package/src/pi/runtime-adapter/execution.ts +107 -12
- package/src/pi/runtime-adapter/index.ts +8 -3
- package/src/pi/runtime-adapter/models.ts +221 -29
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +112 -2
- package/src/pi/tool/compiler.ts +5 -32
- package/src/pi/tool/core-host.ts +28 -30
- package/src/pi/tool/core.ts +25 -122
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/schedule.ts +46 -2
- package/src/pi/tool/skill.ts +112 -420
- package/src/pi/tool/web-fetch.ts +282 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/workspace-sandbox.ts +92 -258
- package/src/plugins.ts +358 -531
- package/src/runtime.ts +195 -44
package/src/pi/tool/skill.ts
CHANGED
|
@@ -1,455 +1,147 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentTool,
|
|
3
|
-
AgentToolResult,
|
|
4
|
-
} from "@earendil-works/pi-agent-core";
|
|
5
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
6
1
|
import {
|
|
7
2
|
runner as createSkillScriptRunner,
|
|
8
|
-
|
|
9
|
-
type
|
|
3
|
+
SkillRegistry,
|
|
4
|
+
type SkillScriptRequest,
|
|
5
|
+
type SkillScriptRunner,
|
|
10
6
|
type SkillSource,
|
|
11
7
|
} from "agents/skills";
|
|
12
|
-
import type
|
|
8
|
+
import { jsonSchema, type ToolSet } from "ai";
|
|
9
|
+
import type {
|
|
10
|
+
RuntimeSkillScriptPolicy,
|
|
11
|
+
WorkspacePort,
|
|
12
|
+
} from "../../kernel/bindings";
|
|
13
|
+
import { aiToolToPi } from "./ai-adapter";
|
|
13
14
|
import type { PiToolCandidate } from "./compiler";
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* 一个已经过 Host 授权、可以交给 Pi 使用的 Skill 绑定。
|
|
17
|
-
*
|
|
18
|
-
* Worker 的 Skill Plugin 负责创建它;调用方应让 `name`、`source` 和脚本权限
|
|
19
|
-
* 来自同一份 Runtime 配置,避免工具目录与实际读取权限脱节。
|
|
20
|
-
*
|
|
21
|
-
* `Host`、`Pi` 与 `Runtime` 等核心术语见 `../../index.ts`。
|
|
22
|
-
*/
|
|
16
|
+
/** An authorized Skill source and its script capabilities. */
|
|
23
17
|
export interface PiSkillBinding {
|
|
24
|
-
/** Runtime 配置中启用的唯一 Skill 名称。 */
|
|
25
18
|
readonly name: string;
|
|
26
|
-
/** 按名称延迟加载说明和资源的 Agents SDK Skill 来源。 */
|
|
27
19
|
readonly source: SkillSource;
|
|
28
|
-
/**
|
|
29
|
-
* Isolated execution policy for this Skill's bundled scripts. Without it the
|
|
30
|
-
* binding contributes no `run_skill_script` capability.
|
|
31
|
-
*/
|
|
32
20
|
readonly script?: RuntimeSkillScriptPolicy;
|
|
33
21
|
}
|
|
34
22
|
|
|
35
|
-
/**
|
|
23
|
+
/** Host capabilities available while creating Skill tools. */
|
|
36
24
|
export interface SkillPiToolOptions {
|
|
37
|
-
/**
|
|
38
|
-
* Worker Loader used to sandbox Skill scripts. Omit it and
|
|
39
|
-
* `run_skill_script` is not contributed at all, which keeps the read-only
|
|
40
|
-
* Skill surface available to hosts that cannot run isolates.
|
|
41
|
-
*/
|
|
42
25
|
readonly loader?: WorkerLoader;
|
|
26
|
+
readonly workspace?: WorkspacePort;
|
|
27
|
+
readonly tools?: readonly PiToolCandidate[];
|
|
43
28
|
}
|
|
44
29
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
description:
|
|
63
|
-
"Normalized relative resource path listed by activate_skill.",
|
|
64
|
-
}),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const runScriptParameters = Type.Object({
|
|
68
|
-
name: Type.String({
|
|
69
|
-
minLength: 1,
|
|
70
|
-
maxLength: 128,
|
|
71
|
-
description: "Name from the AUTHORIZED SKILLS catalog.",
|
|
72
|
-
}),
|
|
73
|
-
path: Type.String({
|
|
74
|
-
minLength: 1,
|
|
75
|
-
maxLength: 4_096,
|
|
76
|
-
description:
|
|
77
|
-
'Script resource path under "scripts/" as listed by activate_skill.',
|
|
78
|
-
}),
|
|
79
|
-
input: Type.Optional(
|
|
80
|
-
Type.Unknown({ description: "JSON input passed to the script." }),
|
|
81
|
-
),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// 只接受已安装 Agents SDK runner 能识别的脚本扩展名。
|
|
85
|
-
const SCRIPT_EXTENSIONS = new Set([
|
|
86
|
-
".js",
|
|
87
|
-
".mjs",
|
|
88
|
-
".ts",
|
|
89
|
-
".tsx",
|
|
90
|
-
".py",
|
|
91
|
-
".sh",
|
|
92
|
-
".bash",
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
// 作用:按模型给出的名称找到已启用的 Skill 绑定。
|
|
96
|
-
// 调用:激活、读资源和运行脚本三个工具在访问 SkillSource 前调用。
|
|
97
|
-
// 原因:先在本地授权列表中查找可以默认拒绝未知名称,
|
|
98
|
-
// 不能让模型直接枚举或访问任意来源。
|
|
99
|
-
function bindingFor(
|
|
100
|
-
bindings: readonly PiSkillBinding[],
|
|
101
|
-
name: string,
|
|
102
|
-
): PiSkillBinding {
|
|
103
|
-
const binding = bindings.find((candidate) => candidate.name === name);
|
|
104
|
-
if (!binding) throw new Error(`Skill is not enabled: ${name}`);
|
|
105
|
-
return binding;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 作用:确认 Skill 资源使用规范化的相对路径。
|
|
109
|
-
// 调用:读取资源前后,以及脚本路径校验的第一步调用。
|
|
110
|
-
// 原因:同时检查请求路径和 Source 回传路径,
|
|
111
|
-
// 防止绝对路径、空段和父目录跳转越过 Skill 边界。
|
|
112
|
-
function validateResourcePath(path: string): void {
|
|
113
|
-
if (
|
|
114
|
-
path.startsWith("/") ||
|
|
115
|
-
path.includes("\0") ||
|
|
116
|
-
path.split("/").some(
|
|
117
|
-
(part) => part === "" || part === "." || part === "..",
|
|
118
|
-
)
|
|
119
|
-
) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
`Skill resource path must be a normalized relative path: ${path}`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 作用:确认资源路径确实指向 runner 支持的 scripts 文件。
|
|
127
|
-
// 调用:运行脚本前分别校验模型请求和 Source 回传的路径。
|
|
128
|
-
// 原因:Agents SDK 的脚本执行是显式开启的能力,
|
|
129
|
-
// 只允许当前 runner 已支持的 JavaScript、TypeScript、Python 和 Bash 扩展名。
|
|
130
|
-
function validateScriptPath(path: string): void {
|
|
131
|
-
validateResourcePath(path);
|
|
132
|
-
if (!path.startsWith("scripts/")) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
`Skill script path must start with "scripts/": ${path}`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
const dot = path.lastIndexOf(".");
|
|
138
|
-
const extension = dot === -1 ? "" : path.slice(dot).toLowerCase();
|
|
139
|
-
if (!SCRIPT_EXTENSIONS.has(extension)) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`Unsupported skill script extension "${
|
|
142
|
-
extension || "(none)"
|
|
143
|
-
}" for ${path}`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 作用:转义写入 Skill 结果标签属性的文本。
|
|
149
|
-
// 调用:activationResult、resourceResult 和脚本结果包装名称与路径时调用。
|
|
150
|
-
// 原因:这些值来自配置或资源描述,
|
|
151
|
-
// 必须避免引号和尖括号破坏供模型扫读的标签边界。
|
|
152
|
-
function attribute(value: string): string {
|
|
153
|
-
return value
|
|
154
|
-
.replaceAll("&", "&")
|
|
155
|
-
.replaceAll('"', """)
|
|
156
|
-
.replaceAll("<", "<")
|
|
157
|
-
.replaceAll(">", ">");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 作用:把 Skill 的资源清单整理成人能直接扫读的列表。
|
|
161
|
-
// 调用:activationResult 返回 Skill 正文时调用。
|
|
162
|
-
// 原因:激活阶段只公布描述信息,不提前读取每个资源,
|
|
163
|
-
// 保持 Agents SDK 的按需加载模型。
|
|
164
|
-
function resourceList(skill: SkillContent): string {
|
|
165
|
-
if (!skill.resources?.length) return "No bundled resources.";
|
|
166
|
-
return skill.resources.map((resource) => {
|
|
167
|
-
const encoding = resource.encoding ?? "text";
|
|
168
|
-
const size = resource.size === undefined
|
|
169
|
-
? ""
|
|
170
|
-
: `, ${resource.size} bytes`;
|
|
171
|
-
const mime = resource.mimeType ? `, ${resource.mimeType}` : "";
|
|
172
|
-
return `- ${resource.path} (${resource.kind}, ${encoding}${mime}${size})`;
|
|
173
|
-
}).join("\n");
|
|
30
|
+
function scriptTools(
|
|
31
|
+
candidates: readonly PiToolCandidate[],
|
|
32
|
+
names: readonly string[],
|
|
33
|
+
): ToolSet {
|
|
34
|
+
const byName = new Map(
|
|
35
|
+
candidates.map((candidate) => [candidate.tool.name, candidate.tool]),
|
|
36
|
+
);
|
|
37
|
+
return Object.fromEntries(names.flatMap((name) => {
|
|
38
|
+
const candidate = byName.get(name);
|
|
39
|
+
if (!candidate) return [];
|
|
40
|
+
return [[name, {
|
|
41
|
+
description: candidate.description,
|
|
42
|
+
inputSchema: jsonSchema(candidate.parameters as never),
|
|
43
|
+
execute: async (input: unknown) =>
|
|
44
|
+
(await candidate.execute(`skill-script:${name}`, input)).details,
|
|
45
|
+
}]];
|
|
46
|
+
})) as ToolSet;
|
|
174
47
|
}
|
|
175
48
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
function activationResult(
|
|
181
|
-
skill: SkillContent,
|
|
182
|
-
): AgentToolResult<unknown> {
|
|
183
|
-
const version = skill.version
|
|
184
|
-
? ` version="${attribute(skill.version)}"`
|
|
185
|
-
: "";
|
|
49
|
+
// A binding authorizes one configured name even if its backing source happens
|
|
50
|
+
// to contain more Skills. Source failures are also sanitized at this boundary.
|
|
51
|
+
function authorizedSource(binding: PiSkillBinding): SkillSource {
|
|
52
|
+
const { name, source } = binding;
|
|
186
53
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`<skill_content name="${attribute(skill.name)}"${version}>`,
|
|
191
|
-
skill.body.trim(),
|
|
192
|
-
"</skill_content>",
|
|
193
|
-
"",
|
|
194
|
-
"Bundled resources:",
|
|
195
|
-
resourceList(skill),
|
|
196
|
-
].join("\n"),
|
|
197
|
-
}],
|
|
198
|
-
details: {
|
|
199
|
-
name: skill.name,
|
|
200
|
-
...(skill.version ? { version: skill.version } : {}),
|
|
201
|
-
resources: skill.resources ?? [],
|
|
54
|
+
id: `${source.id}:${name}`,
|
|
55
|
+
get fingerprint() {
|
|
56
|
+
return `${source.fingerprint}:${name}`;
|
|
202
57
|
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// 作用:把一个已读取的 Skill 资源包装成 Pi 工具结果。
|
|
207
|
-
// 调用:read_skill_resource 完成路径和来源核对后调用。
|
|
208
|
-
// 原因:显式保留种类、编码和 MIME 等元数据,
|
|
209
|
-
// 避免模型从正文猜测资源格式。
|
|
210
|
-
function resourceResult(
|
|
211
|
-
name: string,
|
|
212
|
-
resource: SkillResource,
|
|
213
|
-
): AgentToolResult<unknown> {
|
|
214
|
-
const encoding = resource.encoding ?? "text";
|
|
215
|
-
const mime = resource.mimeType
|
|
216
|
-
? ` mimeType="${attribute(resource.mimeType)}"`
|
|
217
|
-
: "";
|
|
218
|
-
return {
|
|
219
|
-
content: [{
|
|
220
|
-
type: "text",
|
|
221
|
-
text: [
|
|
222
|
-
`<skill_resource name="${attribute(name)}" path="${
|
|
223
|
-
attribute(resource.path)
|
|
224
|
-
}" kind="${resource.kind}" encoding="${encoding}"${mime}>`,
|
|
225
|
-
resource.content,
|
|
226
|
-
"</skill_resource>",
|
|
227
|
-
].join("\n"),
|
|
228
|
-
}],
|
|
229
|
-
details: {
|
|
230
|
-
name,
|
|
231
|
-
path: resource.path,
|
|
232
|
-
kind: resource.kind,
|
|
233
|
-
encoding,
|
|
234
|
-
...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
|
|
235
|
-
...(resource.size === undefined
|
|
236
|
-
? {}
|
|
237
|
-
: { size: resource.size }),
|
|
58
|
+
async list() {
|
|
59
|
+
return (await source.list()).filter((skill) => skill.name === name);
|
|
238
60
|
},
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* 为已启用的 Skill 创建按需加载说明、资源和可选脚本的 Pi 候选工具。
|
|
244
|
-
*
|
|
245
|
-
* Worker 的 Skill Plugin 在贡献 Runtime 能力时调用它。调用方只传入已授权绑定;
|
|
246
|
-
* 没有绑定时不公开工具,没有 Worker Loader 或脚本策略时不公开脚本执行工具。
|
|
247
|
-
*
|
|
248
|
-
* 这里的 “Skill” 是按需加载的任务说明包,“Source” 负责列出和读取包,“资源”
|
|
249
|
-
* 是包内文件,“runner” 是 Agents SDK 借助 Worker Loader 创建的隔离脚本执行器。
|
|
250
|
-
* 激活只读取说明和资源清单,资源正文继续按路径读取,避免把整个 Skill 库常驻
|
|
251
|
-
* 系统上下文。脚本执行仍是显式、按绑定授权的高风险能力,不能降级成普通读取。
|
|
252
|
-
* Agents SDK 目前仍把 Skill 脚本标为 experimental,升级依赖时要重新核对 runner
|
|
253
|
-
* 的选项和请求结构,不能把当前形状当成稳定协议。
|
|
254
|
-
*
|
|
255
|
-
* `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
|
|
256
|
-
*/
|
|
257
|
-
export function skillPiToolCandidates(
|
|
258
|
-
bindings: readonly PiSkillBinding[],
|
|
259
|
-
options: SkillPiToolOptions = {},
|
|
260
|
-
): PiToolCandidate[] {
|
|
261
|
-
if (bindings.length === 0) return [];
|
|
262
|
-
|
|
263
|
-
const activate: AgentTool<typeof activateParameters> = {
|
|
264
|
-
name: "activate_skill",
|
|
265
|
-
label: "Activate Skill",
|
|
266
|
-
description:
|
|
267
|
-
"Load the instructions and bundled-resource inventory for an enabled Skill. Use this when the user's task matches an AUTHORIZED SKILLS entry.",
|
|
268
|
-
parameters: activateParameters,
|
|
269
|
-
// 作用:按名称加载一个已启用 Skill 的说明和资源清单。
|
|
270
|
-
// 调用:Pi 在任务命中系统上下文里的 Skill 摘要后调用。
|
|
271
|
-
// 原因:先经过本地绑定授权,再隐藏 Source 原始异常,
|
|
272
|
-
// 防止私有存储细节进入模型错误消息。
|
|
273
|
-
async execute(_toolCallId, { name }, signal) {
|
|
274
|
-
signal?.throwIfAborted();
|
|
275
|
-
const binding = bindingFor(bindings, name);
|
|
276
|
-
let skill: SkillContent | null;
|
|
61
|
+
async load(requested) {
|
|
62
|
+
if (requested !== name) return null;
|
|
277
63
|
try {
|
|
278
|
-
skill = await
|
|
64
|
+
const skill = await source.load(requested);
|
|
65
|
+
return skill?.name === name ? skill : null;
|
|
279
66
|
} catch (cause) {
|
|
280
67
|
throw new Error(`Skill unavailable: ${name}`, { cause });
|
|
281
68
|
}
|
|
282
|
-
if (!skill || skill.name !== name) {
|
|
283
|
-
throw new Error(`Skill not found: ${name}`);
|
|
284
|
-
}
|
|
285
|
-
return activationResult(skill);
|
|
286
69
|
},
|
|
70
|
+
...(source.readResource
|
|
71
|
+
? {
|
|
72
|
+
async readResource(requested: string, path: string) {
|
|
73
|
+
if (requested !== name) return null;
|
|
74
|
+
try {
|
|
75
|
+
const resource = await source.readResource!(requested, path);
|
|
76
|
+
return resource?.path === path ? resource : null;
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Skill resource unavailable: ${name}/${path}`,
|
|
80
|
+
{ cause },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
...(source.refresh ? { refresh: () => source.refresh!() } : {}),
|
|
287
87
|
};
|
|
88
|
+
}
|
|
288
89
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const binding = bindingFor(bindings, name);
|
|
303
|
-
if (!binding.source.readResource) {
|
|
304
|
-
throw new Error(`Skill has no readable resources: ${name}`);
|
|
305
|
-
}
|
|
306
|
-
let resource: SkillResource | null;
|
|
307
|
-
try {
|
|
308
|
-
resource = await binding.source.readResource(name, path);
|
|
309
|
-
} catch (cause) {
|
|
310
|
-
throw new Error(
|
|
311
|
-
`Skill resource unavailable: ${name}/${path}`,
|
|
312
|
-
{ cause },
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
if (!resource || resource.path !== path) {
|
|
316
|
-
throw new Error(`Skill resource not found: ${name}/${path}`);
|
|
90
|
+
function scriptRunner(
|
|
91
|
+
bindings: readonly PiSkillBinding[],
|
|
92
|
+
options: SkillPiToolOptions,
|
|
93
|
+
): SkillScriptRunner | null {
|
|
94
|
+
if (!options.loader || !bindings.some((binding) => binding.script)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const byName = new Map(bindings.map((binding) => [binding.name, binding]));
|
|
98
|
+
return {
|
|
99
|
+
async run(request: SkillScriptRequest) {
|
|
100
|
+
const policy = byName.get(request.skill.name)?.script;
|
|
101
|
+
if (!policy) {
|
|
102
|
+
throw new Error(`Skill cannot run scripts: ${request.skill.name}`);
|
|
317
103
|
}
|
|
318
|
-
|
|
319
|
-
|
|
104
|
+
const workspace = policy.workspace !== "none" && options.workspace
|
|
105
|
+
? policy.workspace
|
|
106
|
+
: "none";
|
|
107
|
+
return createSkillScriptRunner({
|
|
108
|
+
loader: options.loader!,
|
|
109
|
+
network: policy.network === "full",
|
|
110
|
+
workspace,
|
|
111
|
+
...(workspace !== "none" && options.workspace
|
|
112
|
+
? { workspaceInstance: options.workspace }
|
|
113
|
+
: {}),
|
|
114
|
+
tools: scriptTools(options.tools ?? [], policy.tools),
|
|
115
|
+
}).run(request);
|
|
320
116
|
},
|
|
321
117
|
};
|
|
118
|
+
}
|
|
322
119
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
tool: activate,
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
owner: "runtime-skill",
|
|
332
|
-
authorized: true,
|
|
333
|
-
requiredExecutionLevel: "safe",
|
|
334
|
-
tool: read,
|
|
335
|
-
},
|
|
336
|
-
];
|
|
337
|
-
|
|
338
|
-
// Script execution needs both an isolate loader and at least one binding that
|
|
339
|
-
// declares a script policy. The upstream runner enforces the capability gates;
|
|
340
|
-
// this wrapper owns argument validation and throws on failure so the shared
|
|
341
|
-
// Tool governance keeps counting real errors.
|
|
342
|
-
const loader = options.loader;
|
|
343
|
-
const scriptable = bindings.some((binding) => binding.script);
|
|
344
|
-
if (loader && scriptable) {
|
|
345
|
-
const runScript: AgentTool<typeof runScriptParameters> = {
|
|
346
|
-
name: "run_skill_script",
|
|
347
|
-
label: "Run Skill script",
|
|
348
|
-
description:
|
|
349
|
-
"Run a bundled script from an enabled Skill. Use this only when the Skill's instructions tell you to run it.",
|
|
350
|
-
parameters: runScriptParameters,
|
|
351
|
-
// 作用:把一个已授权 Skill 自带的脚本交给 Agents SDK 隔离 runner 执行。
|
|
352
|
-
// 调用:Pi 只在 Skill 说明明确要求执行脚本时调用。
|
|
353
|
-
// 原因:加载器、绑定策略、资源描述和实际资源缺一不可,
|
|
354
|
-
// 逐层复核可避免把普通文件或未授权能力交给 runner。
|
|
355
|
-
async execute(_toolCallId, { name, path, input }, signal) {
|
|
356
|
-
signal?.throwIfAborted();
|
|
357
|
-
validateScriptPath(path);
|
|
358
|
-
const binding = bindingFor(bindings, name);
|
|
359
|
-
const policy = binding.script;
|
|
360
|
-
if (!policy) {
|
|
361
|
-
throw new Error(`Skill cannot run scripts: ${name}`);
|
|
362
|
-
}
|
|
363
|
-
if (!binding.source.readResource) {
|
|
364
|
-
throw new Error(`Skill has no readable resources: ${name}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
let skill: SkillContent | null;
|
|
368
|
-
try {
|
|
369
|
-
skill = await binding.source.load(name);
|
|
370
|
-
} catch (cause) {
|
|
371
|
-
throw new Error(`Skill unavailable: ${name}`, { cause });
|
|
372
|
-
}
|
|
373
|
-
if (!skill || skill.name !== name) {
|
|
374
|
-
throw new Error(`Skill not found: ${name}`);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const descriptor = skill.resources?.find(
|
|
378
|
-
(resource) => resource.path === path,
|
|
379
|
-
);
|
|
380
|
-
if (!descriptor) {
|
|
381
|
-
throw new Error(`Skill script not found: ${name}/${path}`);
|
|
382
|
-
}
|
|
383
|
-
if (descriptor.kind !== "script") {
|
|
384
|
-
throw new Error(
|
|
385
|
-
`Skill resource is not a script: ${name}/${path}`,
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
let resource: SkillResource | null;
|
|
390
|
-
try {
|
|
391
|
-
resource = await binding.source.readResource(name, path);
|
|
392
|
-
} catch (cause) {
|
|
393
|
-
throw new Error(
|
|
394
|
-
`Skill script unavailable: ${name}/${path}`,
|
|
395
|
-
{ cause },
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
if (!resource || resource.path !== path) {
|
|
399
|
-
throw new Error(`Skill script not found: ${name}/${path}`);
|
|
400
|
-
}
|
|
401
|
-
validateScriptPath(resource.path);
|
|
402
|
-
if ((resource.encoding ?? "text") !== "text") {
|
|
403
|
-
throw new Error(
|
|
404
|
-
`Skill script must be text, got ${resource.encoding}: ${name}/${path}`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
signal?.throwIfAborted();
|
|
409
|
-
// TODO(待确认):agents@0.19.0 runner 需要 request.resources 才能识别
|
|
410
|
-
// precompiled 脚本并填充 ctx.files 或 /skill;这里只传了目标脚本资源。
|
|
411
|
-
// TODO(待确认):策略允许 Workspace 时还需 workspaceInstance;当前只传
|
|
412
|
-
// workspace 权限;按已安装 runner 与官方文档,
|
|
413
|
-
// 读写能力可能仍不可用。
|
|
414
|
-
// TODO(待确认):RuntimeSkillScriptPolicy.tools 已被保存,但这里没有把
|
|
415
|
-
// 对应工具集传给 runner;按官方文档,ctx.tools 可能始终为空。
|
|
416
|
-
const output = await createSkillScriptRunner({
|
|
417
|
-
loader,
|
|
418
|
-
network: policy.network === "full",
|
|
419
|
-
workspace: policy.workspace,
|
|
420
|
-
}).run({
|
|
421
|
-
skill,
|
|
422
|
-
path,
|
|
423
|
-
source: resource.content,
|
|
424
|
-
input: input ?? {},
|
|
425
|
-
});
|
|
120
|
+
const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
|
|
121
|
+
activate_skill: "Activate Skill",
|
|
122
|
+
read_skill_resource: "Read Skill resource",
|
|
123
|
+
run_skill_script: "Run Skill script",
|
|
124
|
+
};
|
|
426
125
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}">`,
|
|
434
|
-
typeof output === "string"
|
|
435
|
-
? output
|
|
436
|
-
: JSON.stringify(output, null, 2) ?? "",
|
|
437
|
-
"</skill_script>",
|
|
438
|
-
].join("\n"),
|
|
439
|
-
}],
|
|
440
|
-
details: { name, path, output },
|
|
441
|
-
};
|
|
442
|
-
},
|
|
443
|
-
};
|
|
444
|
-
candidates.push({
|
|
445
|
-
owner: "runtime-skill",
|
|
446
|
-
authorized: true,
|
|
447
|
-
// Scripts execute host-provided code in an isolate, so this is never a
|
|
448
|
-
// safe read like activate/read_resource.
|
|
449
|
-
requiredExecutionLevel: "high",
|
|
450
|
-
tool: runScript,
|
|
451
|
-
});
|
|
452
|
-
}
|
|
126
|
+
/** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
|
|
127
|
+
export async function skillPiToolCandidates(
|
|
128
|
+
bindings: readonly PiSkillBinding[],
|
|
129
|
+
options: SkillPiToolOptions = {},
|
|
130
|
+
): Promise<PiToolCandidate[]> {
|
|
131
|
+
if (bindings.length === 0) return [];
|
|
453
132
|
|
|
454
|
-
|
|
133
|
+
const registry = new SkillRegistry(
|
|
134
|
+
bindings.map(authorizedSource),
|
|
135
|
+
scriptRunner(bindings, options),
|
|
136
|
+
);
|
|
137
|
+
await registry.load();
|
|
138
|
+
|
|
139
|
+
return Object.entries(registry.tools()).map(([name, tool]) => ({
|
|
140
|
+
owner: "runtime-skill",
|
|
141
|
+
authorized: true,
|
|
142
|
+
requiredExecutionLevel: name === "run_skill_script" ? "high" : "safe",
|
|
143
|
+
tool: aiToolToPi(name, tool, {
|
|
144
|
+
label: SKILL_TOOL_LABELS[name] ?? name,
|
|
145
|
+
}),
|
|
146
|
+
}));
|
|
455
147
|
}
|