@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.10
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 +7 -7
- package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
- package/src/adapter/cloudflare/agent.ts +1682 -0
- package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
- package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +37 -42
- package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
- package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/{db → adapter/cloudflare}/schema.ts +28 -80
- package/src/adapter/cloudflare/subagent/definition.ts +63 -33
- package/src/adapter/cloudflare/subagent/runner.ts +174 -120
- package/src/adapter/cloudflare/subagent/tools.ts +13 -14
- package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
- package/src/adapter/cloudflare/universal-agent/tools.ts +10 -12
- package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
- package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
- package/src/config/activation.ts +140 -0
- package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
- package/src/{runtime-assembler.ts → config/assembler.ts} +45 -46
- package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
- package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
- package/src/{runtime-definition.ts → config/definition.ts} +13 -13
- package/src/config/harness-runtime.ts +190 -0
- package/src/config/index.ts +14 -0
- package/src/config/model-guidance.ts +21 -0
- package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +60 -19
- package/src/{kernel/bindings.ts → config/ports.ts} +32 -16
- package/src/{kernel → config}/profile.ts +7 -0
- package/src/{kernel → config}/public-contracts.ts +6 -6
- package/src/{kernel → config}/runtime-load.ts +1 -1
- package/src/{pi/assembly/context.ts → context/assembly.ts} +5 -207
- package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
- package/src/context/extension/index.ts +3 -0
- package/src/context/index.ts +6 -0
- package/src/{layers/context → context/memory}/budget/gate.ts +21 -5
- package/src/conversation/harness-resume.ts +288 -0
- package/src/conversation/harness-stream.ts +99 -0
- package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
- package/src/conversation/index.ts +6 -0
- package/src/{pi → conversation}/message/contract.ts +3 -0
- package/src/{pi → conversation}/message/conversion.ts +75 -0
- package/src/{pi → conversation}/message/index.ts +1 -0
- package/src/{pi → conversation}/message/model-input.ts +1 -1
- package/src/{pi → conversation}/message/projection.ts +113 -30
- package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
- package/src/conversation/state.ts +196 -0
- package/src/harness/index.ts +7 -0
- package/src/harness/session-harness.ts +562 -0
- package/src/harness/submission-operation.ts +49 -0
- package/src/index.ts +38 -43
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/mcp.ts +1 -1
- package/src/lib/prompt.ts +7 -6
- package/src/lib/token-estimate.ts +8 -0
- package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -17
- package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
- package/src/{pi/session → session/cloudflare}/migration.ts +404 -14
- package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
- package/src/session/index.ts +39 -0
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +286 -0
- package/src/{kernel/receipts.ts → submission/contracts.ts} +56 -2
- package/src/submission/delivery.ts +212 -0
- package/src/submission/finalization.ts +111 -0
- package/src/submission/index.ts +9 -0
- package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
- package/src/{db/submission.repo.ts → submission/repository.ts} +117 -114
- package/src/submission/steer-delivery.ts +167 -0
- package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/contract.ts +12 -0
- package/src/telemetry/coordinator.ts +65 -0
- package/src/telemetry/delivery.ts +2 -2
- package/src/telemetry/index.ts +5 -0
- package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
- package/src/telemetry/recorder.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +12 -0
- package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
- package/src/telemetry/turn-events.ts +279 -0
- package/src/tools/README.md +44 -0
- package/src/{pi/tool/ai-adapter.ts → tools/adapters/ai-to-pi.ts} +6 -9
- package/src/{pi/tool → tools/adapters}/declared.ts +6 -2
- package/src/{pi/tool/nested-tools.ts → tools/adapters/pi-to-ai.ts} +2 -2
- package/src/tools/assistant/ask-user.ts +317 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/{pi/tool → tools/assistant}/time.ts +1 -1
- package/src/{pi/tool/core-host.ts → tools/codemode/adapter.ts} +50 -49
- package/src/{pi/tool/core.ts → tools/codemode/index.ts} +35 -91
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +32 -0
- package/src/{pi/tool → tools/mcp}/gateway.ts +19 -19
- package/src/{pi/tool/mcp.ts → tools/mcp/index.ts} +5 -5
- package/src/tools/runtime/approval/index.ts +1 -0
- package/src/{db/approval.repo.ts → tools/runtime/approval/repository.ts} +22 -41
- package/src/tools/runtime/compiler.ts +264 -0
- package/src/tools/runtime/contracts.ts +117 -0
- package/src/tools/runtime/decision-gate.ts +337 -0
- package/src/tools/runtime/human-decisions.ts +127 -0
- package/src/tools/runtime/interaction/index.ts +1 -0
- package/src/{db/interaction.repo.ts → tools/runtime/interaction/repository.ts} +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/{kernel/submission-authority.ts → tools/subagent/authority.ts} +31 -31
- package/src/{pi/tool/subagent.ts → tools/subagent/index.ts} +33 -36
- package/src/{db/agent-tool.repo.ts → tools/subagent/repository.ts} +1 -1
- package/src/{kernel/subagent-runtime.ts → tools/subagent/runtime.ts} +1 -1
- package/src/tools/subagent/temporary/approvals.ts +123 -0
- package/src/{layers/orchestration/temporary-agent/core.ts → tools/subagent/temporary/coordinator.ts} +41 -49
- package/src/tools/subagent/temporary/core.ts +53 -0
- package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/runner.ts +1 -1
- package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/workspace.ts +1 -4
- package/src/{pi/tool/web-fetch.ts → tools/web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1320 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/{pi/tool/web-search/web-search.ts → tools/web/search/search.ts} +5 -7
- package/src/{pi/tool/workspace-sandbox.ts → tools/workspace/index.ts} +12 -272
- package/src/{pi/tool/workspace-revision.ts → tools/workspace/revision.ts} +2 -2
- package/src/db/milestone.repo.ts +0 -104
- package/src/db/settlement.repo.ts +0 -104
- package/src/kernel/approval-lifecycle.ts +0 -638
- package/src/kernel/interaction-lifecycle.ts +0 -433
- package/src/kernel/recoverable-chat-agent.ts +0 -1020
- package/src/kernel/state.ts +0 -91
- package/src/pi/assembly/index.ts +0 -19
- package/src/pi/runtime-adapter/execution.ts +0 -1163
- package/src/pi/runtime-adapter/index.ts +0 -293
- package/src/pi/runtime-adapter/recovery.ts +0 -982
- package/src/pi/session/index.ts +0 -24
- package/src/pi/tool/base.ts +0 -550
- package/src/pi/tool/compiler.ts +0 -483
- package/src/pi/tool/index.ts +0 -38
- package/src/pi/tool/schedule.ts +0 -261
- package/src/pi/tool/skill.ts +0 -565
- package/src/pi/tool/web-search/api.ts +0 -1318
- package/src/pi/tool/web-search/index.ts +0 -2
- package/src/pi/tool/workspace-state-connector.ts +0 -146
- package/src/pi/turn/approval.ts +0 -181
- package/src/pi/turn/index.ts +0 -82
- package/src/pi/turn/interaction.ts +0 -181
- package/src/runtime.ts +0 -4760
- package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
- package/src/{kernel → config}/degradation.ts +0 -0
- package/src/{kernel → config}/runtime-config.ts +0 -0
- package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
- package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
- package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
- package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
- package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
- package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
- package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/contract.ts +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/extract/index.ts +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/fanout/index.ts +0 -0
- package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/registry.ts +1 -1
- /package/src/{workspace-versioning.ts → tools/workspace/versioning.ts} +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { RuntimeSandboxPort, WorkspacePort } from "../../config/ports";
|
|
4
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
5
|
+
import { result, running } from "../runtime/result";
|
|
6
|
+
|
|
7
|
+
/** 从 {@link WorkspacePort} 生成标准 workspace 文件 Tool 集(read / write / edit …)。 */
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const sandboxCommandParameters = {
|
|
12
|
+
command: Type.String({
|
|
13
|
+
minLength: 1,
|
|
14
|
+
maxLength: 32_768,
|
|
15
|
+
description: "Shell command to run in the isolated Linux Sandbox.",
|
|
16
|
+
}),
|
|
17
|
+
cwd: Type.Optional(Type.String({
|
|
18
|
+
maxLength: 4_096,
|
|
19
|
+
description: "Sandbox working directory. Omit to use the Sandbox default.",
|
|
20
|
+
})),
|
|
21
|
+
stdin: Type.Optional(Type.String({
|
|
22
|
+
maxLength: 65_536,
|
|
23
|
+
description: "Optional text to pass to the command on standard input.",
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const sandboxExecParameters = Type.Object({
|
|
28
|
+
...sandboxCommandParameters,
|
|
29
|
+
timeoutMs: Type.Optional(
|
|
30
|
+
Type.Integer({
|
|
31
|
+
minimum: 1,
|
|
32
|
+
maximum: 60_000,
|
|
33
|
+
description: "Maximum command runtime in milliseconds, from 1 to 60000.",
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
}, { additionalProperties: false });
|
|
37
|
+
|
|
38
|
+
const sandboxStartParameters = Type.Object(
|
|
39
|
+
sandboxCommandParameters,
|
|
40
|
+
{ additionalProperties: false },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const sandboxProcessParameters = Type.Object({
|
|
44
|
+
id: Type.String({
|
|
45
|
+
minLength: 1,
|
|
46
|
+
maxLength: 128,
|
|
47
|
+
description: "Process ID returned by sandbox_start_process.",
|
|
48
|
+
}),
|
|
49
|
+
}, { additionalProperties: false });
|
|
50
|
+
|
|
51
|
+
const sandboxPublishParameters = Type.Object({
|
|
52
|
+
paths: Type.Array(Type.String({
|
|
53
|
+
minLength: 1,
|
|
54
|
+
maxLength: 4_096,
|
|
55
|
+
description: "Sandbox file path to publish.",
|
|
56
|
+
}), {
|
|
57
|
+
minItems: 1,
|
|
58
|
+
maxItems: 100,
|
|
59
|
+
description: "Files to copy from the temporary Sandbox into the persistent Workspace.",
|
|
60
|
+
}),
|
|
61
|
+
}, { additionalProperties: false });
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 为现有 RuntimeSandboxPort 创建 Pi 命令、进程和文件发布工具。
|
|
65
|
+
*
|
|
66
|
+
* Worker 的 Sandbox Plugin 在容器能力和持久 Workspace 都可用时调用它。调用方
|
|
67
|
+
* 应传入已绑定当前会话的 Port;前台命令适合一次性执行,后台进程适合需要持续
|
|
68
|
+
* 运行的服务,二者不能互换结果语义。
|
|
69
|
+
*
|
|
70
|
+
* 这里的 “Sandbox” 指 Cloudflare Sandbox SDK 提供的隔离 Linux 容器;本层只
|
|
71
|
+
* 面向项目自己的 Port。命令限制、cwd 校验、工作区装载、超时、取消、清理和
|
|
72
|
+
* 冲突安全发布仍在 Port 内。容器文件不会自动写回 Workspace,只有 publish
|
|
73
|
+
* 工具显式选中的文件才进入持久视图;执行、启动、停止和发布继续走高风险审批。
|
|
74
|
+
*
|
|
75
|
+
* `Pi`、`Port` 与“候选工具”等项目核心术语见 `src/index.ts`。
|
|
76
|
+
*/
|
|
77
|
+
export function sandboxPiToolCandidates(
|
|
78
|
+
sandbox: RuntimeSandboxPort,
|
|
79
|
+
): PiToolCandidate[] {
|
|
80
|
+
const exec: AgentTool<typeof sandboxExecParameters> = {
|
|
81
|
+
name: "sandbox_exec",
|
|
82
|
+
label: "Run command",
|
|
83
|
+
description:
|
|
84
|
+
"Run a foreground command in this Chat's isolated Linux Sandbox. Files stay temporary until sandbox_publish_files is called.",
|
|
85
|
+
parameters: sandboxExecParameters,
|
|
86
|
+
// 作用:在 Sandbox 中运行一次前台命令并等待完整退出结果。
|
|
87
|
+
// 调用:Pi 执行构建、测试或其他会结束的命令时调用。
|
|
88
|
+
// 原因:输入和 AbortSignal 原样交给 Port,返回值只投影公开字段,
|
|
89
|
+
// 避免容器内部句柄进入对话。
|
|
90
|
+
async execute(_toolCallId, input, signal, onUpdate) {
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
running(onUpdate, "Running the Sandbox command.");
|
|
93
|
+
const output = await sandbox.exec(input, signal);
|
|
94
|
+
return result({
|
|
95
|
+
success: output.success,
|
|
96
|
+
stdout: output.stdout,
|
|
97
|
+
stderr: output.stderr,
|
|
98
|
+
exitCode: output.exitCode,
|
|
99
|
+
stdoutTruncated: output.stdoutTruncated,
|
|
100
|
+
stderrTruncated: output.stderrTruncated,
|
|
101
|
+
workspacePersistence: "explicit_publish_required",
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const startProcess: AgentTool<typeof sandboxStartParameters> = {
|
|
107
|
+
name: "sandbox_start_process",
|
|
108
|
+
label: "Start process",
|
|
109
|
+
description:
|
|
110
|
+
"Start a long-running background process in this Chat's isolated Linux Sandbox.",
|
|
111
|
+
parameters: sandboxStartParameters,
|
|
112
|
+
// 作用:在 Sandbox 中启动一个长期运行的后台进程并返回进程标识。
|
|
113
|
+
// 调用:Pi 启动开发服务器、监听器或其他不能等待退出的程序时调用。
|
|
114
|
+
// 原因:Cloudflare 官方接口把长期进程与 exec 分开;
|
|
115
|
+
// 这里只公开后续查日志和停止所需字段。
|
|
116
|
+
async execute(_toolCallId, input, signal, onUpdate) {
|
|
117
|
+
signal?.throwIfAborted();
|
|
118
|
+
running(onUpdate, "Starting the Sandbox process.");
|
|
119
|
+
const process = await sandbox.startProcess(input);
|
|
120
|
+
return result({
|
|
121
|
+
id: process.id,
|
|
122
|
+
command: process.command,
|
|
123
|
+
status: process.status,
|
|
124
|
+
...(process.exitCode === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: { exitCode: process.exitCode }),
|
|
127
|
+
workspacePersistence: "explicit_publish_required",
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const processLogs: AgentTool<typeof sandboxProcessParameters> = {
|
|
133
|
+
name: "sandbox_process_logs",
|
|
134
|
+
label: "Read process logs",
|
|
135
|
+
description:
|
|
136
|
+
"Read bounded stdout and stderr logs for one background Sandbox process.",
|
|
137
|
+
parameters: sandboxProcessParameters,
|
|
138
|
+
// 作用:读取一个 Sandbox 后台进程当前的有界日志。
|
|
139
|
+
// 调用:Pi 拿到 startProcess 返回的 id 后检查启动或运行状态时调用。
|
|
140
|
+
// 原因:日志截断由 Port 统一处理并显式回传标记,
|
|
141
|
+
// 避免适配层再次缓存不断增长的输出。
|
|
142
|
+
async execute(_toolCallId, { id }, signal, onUpdate) {
|
|
143
|
+
signal?.throwIfAborted();
|
|
144
|
+
running(onUpdate, "Reading the Sandbox process logs.");
|
|
145
|
+
const logs = await sandbox.getProcessLogs(id);
|
|
146
|
+
return result({
|
|
147
|
+
id: logs.id,
|
|
148
|
+
stdout: logs.stdout,
|
|
149
|
+
stderr: logs.stderr,
|
|
150
|
+
stdoutTruncated: logs.stdoutTruncated,
|
|
151
|
+
stderrTruncated: logs.stderrTruncated,
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const stopProcess: AgentTool<typeof sandboxProcessParameters> = {
|
|
157
|
+
name: "sandbox_stop_process",
|
|
158
|
+
label: "Stop process",
|
|
159
|
+
description: "Stop one background Sandbox process.",
|
|
160
|
+
parameters: sandboxProcessParameters,
|
|
161
|
+
// 作用:停止一个由当前 Sandbox 管理的后台进程。
|
|
162
|
+
// 调用:Pi 不再需要该进程或需要清理运行环境时调用。
|
|
163
|
+
// 原因:只把进程 id 交给 Port,
|
|
164
|
+
// 进程归属检查和实际清理由 Sandbox 边界统一负责。
|
|
165
|
+
async execute(_toolCallId, { id }, signal, onUpdate) {
|
|
166
|
+
signal?.throwIfAborted();
|
|
167
|
+
running(onUpdate, "Stopping the Sandbox process.");
|
|
168
|
+
const output = await sandbox.stopProcess(id);
|
|
169
|
+
return result({ stopped: output.stopped });
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const publishFiles: AgentTool<typeof sandboxPublishParameters> = {
|
|
174
|
+
name: "sandbox_publish_files",
|
|
175
|
+
label: "Publish files",
|
|
176
|
+
description:
|
|
177
|
+
"Publish named files from the temporary Sandbox into the scoped Workspace. Shared files remain read-only.",
|
|
178
|
+
parameters: sandboxPublishParameters,
|
|
179
|
+
// 作用:把明确列出的 Sandbox 文件发布到持久 Workspace。
|
|
180
|
+
// 调用:Pi 确认命令产物需要保留到会话文件区时调用。
|
|
181
|
+
// 原因:显式发布避免临时容器状态被误当成已持久化;
|
|
182
|
+
// 冲突、共享写保护和失败码继续由 Port 决定。
|
|
183
|
+
async execute(_toolCallId, { paths }, signal, onUpdate) {
|
|
184
|
+
signal?.throwIfAborted();
|
|
185
|
+
running(onUpdate, "Publishing Sandbox files to the Workspace.");
|
|
186
|
+
const output = await sandbox.publishFiles(paths);
|
|
187
|
+
return result({
|
|
188
|
+
files: output.files,
|
|
189
|
+
skipped: output.skipped,
|
|
190
|
+
failed: output.failed.map(({ path, errorCode }) => ({
|
|
191
|
+
path,
|
|
192
|
+
errorCode,
|
|
193
|
+
})),
|
|
194
|
+
bytes: output.bytes,
|
|
195
|
+
workspacePersistence: "published",
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
owner: "sandbox",
|
|
203
|
+
requiredExecutionLevel: "high",
|
|
204
|
+
tool: exec,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
owner: "sandbox",
|
|
208
|
+
requiredExecutionLevel: "high",
|
|
209
|
+
tool: startProcess,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
owner: "sandbox",
|
|
213
|
+
requiredExecutionLevel: "safe",
|
|
214
|
+
retry: "idempotent",
|
|
215
|
+
tool: processLogs,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
owner: "sandbox",
|
|
219
|
+
requiredExecutionLevel: "high",
|
|
220
|
+
tool: stopProcess,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
owner: "sandbox",
|
|
224
|
+
requiredExecutionLevel: "high",
|
|
225
|
+
tool: publishFiles,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type, fauxToolCall, validateToolArguments, type TSchema } from "@earendil-works/pi-ai";
|
|
3
|
+
import {
|
|
4
|
+
toScheduleRuleSpec,
|
|
5
|
+
validateScheduleRuleSpec,
|
|
6
|
+
} from "../../adapter/cloudflare/scheduling/schedule-rule";
|
|
7
|
+
import { isValidTimeZone } from "../../adapter/cloudflare/scheduling/zoned-cron";
|
|
8
|
+
import type { RuntimeSchedulePort, RuntimeSchedulePreparation } from "../../config/ports";
|
|
9
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
10
|
+
import type { ScheduleSpec } from "../../submission/contracts";
|
|
11
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
12
|
+
|
|
13
|
+
const scheduleTriggerParameters = Type.Union([
|
|
14
|
+
Type.Object({
|
|
15
|
+
kind: Type.Literal("local", { description: "Interpret the rule in the explicit task timezone." }),
|
|
16
|
+
rule: Type.Union([
|
|
17
|
+
Type.Object({ kind: Type.Literal("once", { description: "Run on one local date." }), date: Type.String({ description: "Local date YYYY-MM-DD." }), time: Type.String({ description: "Local clock time HH:mm." }) }, { additionalProperties: false }),
|
|
18
|
+
Type.Object({ kind: Type.Literal("daily", { description: "Run each local day." }), time: Type.String({ description: "Local clock time HH:mm." }) }, { additionalProperties: false }),
|
|
19
|
+
Type.Object({ kind: Type.Literal("weekly", { description: "Run on the selected weekdays." }), weekdays: Type.Array(Type.Integer({ minimum: 0, maximum: 6 }), { minItems: 1, description: "Weekdays: Sunday 0 through Saturday 6." }), time: Type.String({ description: "Local clock time HH:mm." }) }, { additionalProperties: false }),
|
|
20
|
+
], { description: "Once, daily or weekly local-time rule." }),
|
|
21
|
+
startDate: Type.Optional(Type.String({ description: "Inclusive first local date YYYY-MM-DD." })),
|
|
22
|
+
endDate: Type.Optional(Type.String({ description: "Inclusive last local date YYYY-MM-DD." })),
|
|
23
|
+
}, { additionalProperties: false }),
|
|
24
|
+
Type.Object({
|
|
25
|
+
kind: Type.Literal("once-in", { description: "Run once after a delay." }),
|
|
26
|
+
seconds: Type.Integer({
|
|
27
|
+
minimum: 1,
|
|
28
|
+
description: "Fire once, this many seconds from now.",
|
|
29
|
+
}),
|
|
30
|
+
}, { additionalProperties: false }),
|
|
31
|
+
Type.Object({
|
|
32
|
+
kind: Type.Literal("once-at", { description: "Run once at an absolute UTC time." }),
|
|
33
|
+
at: Type.String({
|
|
34
|
+
description:
|
|
35
|
+
"Fire once at this absolute UTC ISO time, e.g. 2026-07-20T09:00:00Z.",
|
|
36
|
+
}),
|
|
37
|
+
}, { additionalProperties: false }),
|
|
38
|
+
Type.Object({
|
|
39
|
+
kind: Type.Literal("cron", { description: "Repeat on a UTC cron schedule." }),
|
|
40
|
+
cron: Type.String({
|
|
41
|
+
description:
|
|
42
|
+
"Repeat on a standard UTC cron, e.g. '0 9 * * *' = daily 09:00 UTC.",
|
|
43
|
+
}),
|
|
44
|
+
}, { additionalProperties: false }),
|
|
45
|
+
Type.Object({
|
|
46
|
+
kind: Type.Literal("interval", { description: "Repeat at fixed intervals." }),
|
|
47
|
+
seconds: Type.Integer({
|
|
48
|
+
minimum: 1,
|
|
49
|
+
description: "Repeat every this many seconds.",
|
|
50
|
+
}),
|
|
51
|
+
}, { additionalProperties: false }),
|
|
52
|
+
], {
|
|
53
|
+
description:
|
|
54
|
+
"When to fire. Discriminate on the `kind` field (never \"type\"). " +
|
|
55
|
+
'Shapes: {"kind":"local","rule":{"kind":"once","date":"2026-09-17","time":"14:06"}} runs once at 14:06 in `timezone`; ' +
|
|
56
|
+
'{"kind":"local","rule":{"kind":"daily","time":"09:00"}} runs daily; ' +
|
|
57
|
+
'{"kind":"local","rule":{"kind":"weekly","weekdays":[1,3],"time":"09:30"}} runs on Mondays and Wednesdays; ' +
|
|
58
|
+
'{"kind":"once-at","at":"2026-09-17T06:06:00Z"} fires once at an absolute UTC time.',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const scheduleParameters = Type.Object({
|
|
62
|
+
prompt: Type.String({
|
|
63
|
+
minLength: 1, maxLength: 1000, pattern: "\\S",
|
|
64
|
+
description:
|
|
65
|
+
"The standalone instruction to run when the schedule fires. The future turn has no context from this chat, so make it self-contained.",
|
|
66
|
+
}),
|
|
67
|
+
trigger: scheduleTriggerParameters,
|
|
68
|
+
label: Type.Optional(Type.String({ maxLength: 50, description: "Optional task name, at most 50 Unicode characters. The Host generates a name when omitted." })),
|
|
69
|
+
timezone: Type.String({ minLength: 1, description: "Required IANA timezone. Use the user's explicit timezone or reliable Host/browser timezone; ask when unknown." }),
|
|
70
|
+
}, { additionalProperties: false });
|
|
71
|
+
|
|
72
|
+
const noParameters = Type.Object({}, { additionalProperties: false });
|
|
73
|
+
const scheduleIdParameters = Type.Object({
|
|
74
|
+
id: Type.String({
|
|
75
|
+
description: "The schedule id, as returned by `list_schedules`.",
|
|
76
|
+
}),
|
|
77
|
+
}, { additionalProperties: false });
|
|
78
|
+
const updateScheduleParameters = Type.Object({
|
|
79
|
+
id: Type.String({ minLength: 1, description: "The exact id returned by `list_schedules`." }),
|
|
80
|
+
prompt: Type.Optional(scheduleParameters.properties.prompt),
|
|
81
|
+
trigger: Type.Optional(scheduleTriggerParameters),
|
|
82
|
+
label: scheduleParameters.properties.label,
|
|
83
|
+
timezone: Type.Optional(scheduleParameters.properties.timezone),
|
|
84
|
+
}, { additionalProperties: false });
|
|
85
|
+
const changeScheduleAgentParameters = Type.Object({
|
|
86
|
+
id: Type.String({
|
|
87
|
+
description: "The exact schedule id returned by `list_schedules`.",
|
|
88
|
+
}),
|
|
89
|
+
userAgentId: Type.String({
|
|
90
|
+
description: "The exact User Agent id selected by the user.",
|
|
91
|
+
}),
|
|
92
|
+
}, { additionalProperties: false });
|
|
93
|
+
|
|
94
|
+
function result<T>(details: T): AgentToolResult<T> {
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: serializeOutput(details).text }],
|
|
97
|
+
details,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function candidate<T extends TSchema>(
|
|
102
|
+
tool: AgentTool<T>,
|
|
103
|
+
options: Partial<
|
|
104
|
+
Pick<
|
|
105
|
+
PiToolCandidate,
|
|
106
|
+
"alwaysRequiresApproval" | "exposureMode" | "owner" | "requiredExecutionLevel" | "retry" | "summary"
|
|
107
|
+
>
|
|
108
|
+
> = {},
|
|
109
|
+
): PiToolCandidate {
|
|
110
|
+
return {
|
|
111
|
+
owner: options.owner ?? "runtime-base",
|
|
112
|
+
exposureMode: options.exposureMode ?? "direct",
|
|
113
|
+
requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
|
|
114
|
+
source: "action",
|
|
115
|
+
tool,
|
|
116
|
+
...(options.alwaysRequiresApproval
|
|
117
|
+
? { alwaysRequiresApproval: true }
|
|
118
|
+
: {}),
|
|
119
|
+
...(options.summary ? { summary: options.summary } : {}),
|
|
120
|
+
...(options.retry ? { retry: options.retry } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function schedulePiToolCandidates(
|
|
125
|
+
schedule: RuntimeSchedulePort,
|
|
126
|
+
): PiToolCandidate[] {
|
|
127
|
+
const candidates = [
|
|
128
|
+
candidate(
|
|
129
|
+
{
|
|
130
|
+
name: "schedule",
|
|
131
|
+
label: "Schedule prompt",
|
|
132
|
+
description:
|
|
133
|
+
"Schedule a prompt to run LATER, autonomously. Required inputs: prompt (Requirement, at most 1000 Unicode characters), trigger and timezone. Prefer local once/daily/weekly rules. trigger discriminates on `kind` (never \"type\"); see the trigger parameter schema for detailed shapes. label is optional (at most 50 characters); the Host generates an omitted name. Ask for missing information. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
|
|
134
|
+
parameters: scheduleParameters,
|
|
135
|
+
async execute(_toolCallId, input, signal) {
|
|
136
|
+
signal?.throwIfAborted();
|
|
137
|
+
throw new Error("Matching user confirmation is required");
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
alwaysRequiresApproval: true,
|
|
142
|
+
exposureMode: "direct",
|
|
143
|
+
summary: "Create a scheduled task",
|
|
144
|
+
},
|
|
145
|
+
),
|
|
146
|
+
candidate(
|
|
147
|
+
{
|
|
148
|
+
name: "list_schedules",
|
|
149
|
+
label: "List schedules",
|
|
150
|
+
description:
|
|
151
|
+
"List the user's existing scheduled tasks. Call this when the user asks what they have scheduled, or before cancelling one — you need the id, and the user will refer to a task by what it does, not by its id.",
|
|
152
|
+
parameters: noParameters,
|
|
153
|
+
async execute(_toolCallId, _input, signal) {
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
return result({ schedules: await schedule.list() });
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{ retry: "idempotent" },
|
|
159
|
+
),
|
|
160
|
+
candidate({
|
|
161
|
+
name: "update_schedule",
|
|
162
|
+
label: "Update schedule",
|
|
163
|
+
description:
|
|
164
|
+
"Update one scheduled task without changing its id or history. Required input: id. Optional prompt, trigger, label and timezone preserve the saved values when omitted; trigger uses the same `kind`-based shapes as `schedule`. Every edit requires user confirmation; changing Requirement requires a fresh Host authorization check. Call `list_schedules` first and use an exact id. If multiple tasks plausibly match the user's wording, ask which one instead of guessing.",
|
|
165
|
+
parameters: updateScheduleParameters,
|
|
166
|
+
async execute(_toolCallId, input, signal) {
|
|
167
|
+
signal?.throwIfAborted();
|
|
168
|
+
throw new Error("Matching user confirmation is required");
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
candidate({
|
|
172
|
+
name: "pause_schedule",
|
|
173
|
+
label: "Pause schedule",
|
|
174
|
+
description:
|
|
175
|
+
"Pause one scheduled task. Call `list_schedules` first and use an exact id; ask the user if the target is ambiguous.",
|
|
176
|
+
parameters: scheduleIdParameters,
|
|
177
|
+
async execute(_toolCallId, input, signal) {
|
|
178
|
+
signal?.throwIfAborted();
|
|
179
|
+
const { ok } = await schedule.pause(input.id);
|
|
180
|
+
return result({ paused: ok, id: input.id });
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
candidate({
|
|
184
|
+
name: "resume_schedule",
|
|
185
|
+
label: "Resume schedule",
|
|
186
|
+
description:
|
|
187
|
+
"Resume one paused scheduled task. Call `list_schedules` first and use an exact id; ask the user if the target is ambiguous.",
|
|
188
|
+
parameters: scheduleIdParameters,
|
|
189
|
+
async execute(_toolCallId, input, signal) {
|
|
190
|
+
signal?.throwIfAborted();
|
|
191
|
+
const { ok } = await schedule.resume(input.id);
|
|
192
|
+
return result({ resumed: ok, id: input.id });
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
candidate(
|
|
196
|
+
{
|
|
197
|
+
name: "cancel_schedule",
|
|
198
|
+
label: "Cancel schedule",
|
|
199
|
+
description:
|
|
200
|
+
"Cancel a scheduled task by id. Call `list_schedules` first to find the id that matches what the user described. If more than one plausibly matches, ask the user which one instead of guessing — cancelling is not reversible.",
|
|
201
|
+
parameters: scheduleIdParameters,
|
|
202
|
+
async execute(_toolCallId, input, signal) {
|
|
203
|
+
signal?.throwIfAborted();
|
|
204
|
+
const { ok } = await schedule.cancel(input.id);
|
|
205
|
+
return result(ok
|
|
206
|
+
? { cancelled: true, id: input.id }
|
|
207
|
+
: {
|
|
208
|
+
cancelled: false,
|
|
209
|
+
id: input.id,
|
|
210
|
+
reason: "no schedule with that id",
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
requiredExecutionLevel: "high",
|
|
216
|
+
summary: "Cancel a scheduled task",
|
|
217
|
+
},
|
|
218
|
+
),
|
|
219
|
+
candidate(
|
|
220
|
+
{
|
|
221
|
+
name: "list_user_agents",
|
|
222
|
+
label: "List User Agents",
|
|
223
|
+
description:
|
|
224
|
+
"List the enabled User Agents available for scheduled tasks. " +
|
|
225
|
+
"Use this before changing a schedule Agent.",
|
|
226
|
+
parameters: noParameters,
|
|
227
|
+
async execute(_toolCallId, _input, signal) {
|
|
228
|
+
signal?.throwIfAborted();
|
|
229
|
+
return result({ agents: await schedule.listAgents() });
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{ owner: "schedule-host" },
|
|
233
|
+
),
|
|
234
|
+
candidate(
|
|
235
|
+
{
|
|
236
|
+
name: "change_schedule_agent",
|
|
237
|
+
label: "Change schedule Agent",
|
|
238
|
+
description:
|
|
239
|
+
"Change which User Agent executes one scheduled task without " +
|
|
240
|
+
"changing its id or history. Call `list_schedules` and " +
|
|
241
|
+
"`list_user_agents` first and use exact ids; ask the user when " +
|
|
242
|
+
"the target is ambiguous.",
|
|
243
|
+
parameters: changeScheduleAgentParameters,
|
|
244
|
+
async execute(_toolCallId, input, signal) {
|
|
245
|
+
signal?.throwIfAborted();
|
|
246
|
+
const { ok } = await schedule.changeAgent(
|
|
247
|
+
input.id,
|
|
248
|
+
input.userAgentId,
|
|
249
|
+
);
|
|
250
|
+
return result({ updated: ok, id: input.id });
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{ owner: "schedule-host" },
|
|
254
|
+
),
|
|
255
|
+
];
|
|
256
|
+
return candidates.flatMap((toolCandidate): PiToolCandidate[] => {
|
|
257
|
+
const name = toolCandidate.tool.name;
|
|
258
|
+
if (name !== "schedule" && name !== "update_schedule") return [toolCandidate];
|
|
259
|
+
const confirmation = schedule.confirmation;
|
|
260
|
+
if (!confirmation) return [];
|
|
261
|
+
return [{
|
|
262
|
+
...toolCandidate, alwaysRequiresApproval: true, confirmation: {
|
|
263
|
+
async prepare(args, { intentId }) {
|
|
264
|
+
const validated = validateToolArguments(toolCandidate.tool, fauxToolCall(name, args as Record<string, unknown>));
|
|
265
|
+
const input = validated as { id?: string; prompt?: string; trigger?: ScheduleSpec["trigger"]; label?: string; timezone?: string };
|
|
266
|
+
if (input.timezone !== undefined && !isValidTimeZone(input.timezone)) throw new Error("A valid IANA timezone is required");
|
|
267
|
+
if (input.trigger?.kind === "local" && input.timezone) {
|
|
268
|
+
const error = validateScheduleRuleSpec(toScheduleRuleSpec(input.trigger, input.timezone));
|
|
269
|
+
if (error) throw new Error(error.message);
|
|
270
|
+
}
|
|
271
|
+
const prepared = await confirmation.prepare({
|
|
272
|
+
intentId, ...(name === "update_schedule" ? { id: input.id } : {}),
|
|
273
|
+
...(input.prompt !== undefined ? { prompt: input.prompt } : {}),
|
|
274
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {}),
|
|
275
|
+
...(input.label !== undefined ? { label: input.label } : {}),
|
|
276
|
+
...(input.timezone !== undefined ? { tz: input.timezone } : {}),
|
|
277
|
+
});
|
|
278
|
+
if (prepared.id !== input.id) throw new Error("Host preparation changed the target Task ID");
|
|
279
|
+
const fullInput = { prompt: prepared.prompt, label: prepared.label, trigger: prepared.trigger, timezone: prepared.timezone };
|
|
280
|
+
void validateToolArguments({ ...toolCandidate.tool, parameters: scheduleParameters }, fauxToolCall(name, fullInput));
|
|
281
|
+
if (!prepared.label?.trim() || !isValidTimeZone(prepared.timezone) || !prepared.candidateId?.trim() || !prepared.candidateVersion?.trim() || !prepared.authorization?.version?.trim() || !prepared.authorization.scope?.trim() || !Array.isArray(prepared.authorization.missing)) {
|
|
282
|
+
throw new Error("Host preparation is incomplete");
|
|
283
|
+
}
|
|
284
|
+
if (prepared.trigger.kind === "local") {
|
|
285
|
+
const error = validateScheduleRuleSpec(toScheduleRuleSpec(prepared.trigger, prepared.timezone));
|
|
286
|
+
if (error) throw new Error(error.message);
|
|
287
|
+
}
|
|
288
|
+
return prepared;
|
|
289
|
+
},
|
|
290
|
+
async execute(prepared, context) {
|
|
291
|
+
const preview = prepared as RuntimeSchedulePreparation;
|
|
292
|
+
if (preview.authorization.missing.length) throw new Error(`Missing authorization: ${preview.authorization.missing.join("; ")}`);
|
|
293
|
+
const { id } = await confirmation.commit(preview, context);
|
|
294
|
+
return result({ [name === "schedule" ? "scheduled" : "updated"]: true, id, task: { label: preview.label, prompt: preview.prompt, trigger: preview.trigger, timezone: preview.timezone } });
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
}];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 从 {@link RuntimeSchedulePort} 生成定时任务 Tool 集。 */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SkillSource } from "agents/skills";
|
|
2
|
+
import type { RuntimeSkillScriptPolicy, WorkspacePort } from "../../config/ports";
|
|
3
|
+
import type { RuntimeSkillBodyTransform } from "../../config/definition";
|
|
4
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
5
|
+
|
|
6
|
+
/** A configured Skill source and its script capabilities. */
|
|
7
|
+
export interface PiSkillBinding {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly description: string;
|
|
10
|
+
readonly source: SkillSource;
|
|
11
|
+
readonly script?: RuntimeSkillScriptPolicy;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Host capabilities available while creating Skill tools. */
|
|
15
|
+
export interface SkillPiToolOptions {
|
|
16
|
+
readonly loader?: WorkerLoader;
|
|
17
|
+
readonly workspace?: WorkspacePort;
|
|
18
|
+
readonly tools?: readonly PiToolCandidate[];
|
|
19
|
+
readonly transformSkillBody?: RuntimeSkillBodyTransform;
|
|
20
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { SkillRegistry } from "agents/skills";
|
|
2
|
+
import { aiToolToPi } from "../adapters/ai-to-pi";
|
|
3
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
4
|
+
import type { PiSkillBinding, SkillPiToolOptions } from "./contracts";
|
|
5
|
+
import { catalogSkillSource, materializeSkillResourceTool, wrapSkillResourceRead } from "./resources";
|
|
6
|
+
import { scriptRunner, wrapSkillScript } from "./scripts";
|
|
7
|
+
|
|
8
|
+
export type { PiSkillBinding, SkillPiToolOptions } from "./contracts";
|
|
9
|
+
const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
|
|
10
|
+
activate_skill: "Activate Skill",
|
|
11
|
+
read_skill_resource: "Read Skill resource",
|
|
12
|
+
run_skill_script: "Run Skill script",
|
|
13
|
+
materialize_skill_resource: "Materialize Skill resource",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SKILL_TOOL_PARAMETER_DESCRIPTIONS: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
17
|
+
activate_skill: {
|
|
18
|
+
name: "Exact name of the available Skill to activate.",
|
|
19
|
+
},
|
|
20
|
+
read_skill_resource: {
|
|
21
|
+
name: "Name of the activated Skill. Omit only when path starts with the Skill name.",
|
|
22
|
+
path: "Bundled resource path listed by activate_skill.",
|
|
23
|
+
},
|
|
24
|
+
run_skill_script: {
|
|
25
|
+
name: "Name of the activated Skill that supplies the script.",
|
|
26
|
+
path: "Bundled script path listed by activate_skill.",
|
|
27
|
+
input: "JSON input expected by the Skill script. Defaults to an empty object.",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
|
|
32
|
+
export async function skillPiToolCandidates(
|
|
33
|
+
bindings: readonly PiSkillBinding[],
|
|
34
|
+
options: SkillPiToolOptions = {},
|
|
35
|
+
): Promise<PiToolCandidate[]> {
|
|
36
|
+
if (bindings.length === 0) return [];
|
|
37
|
+
|
|
38
|
+
const availableToolNames = (options.tools ?? []).map(({ tool }) => tool.name);
|
|
39
|
+
|
|
40
|
+
const registry = new SkillRegistry(
|
|
41
|
+
bindings.map((binding) => catalogSkillSource(
|
|
42
|
+
binding,
|
|
43
|
+
options.transformSkillBody,
|
|
44
|
+
availableToolNames,
|
|
45
|
+
)),
|
|
46
|
+
scriptRunner(bindings, options),
|
|
47
|
+
);
|
|
48
|
+
await registry.load();
|
|
49
|
+
|
|
50
|
+
const tools = registry.tools();
|
|
51
|
+
if (options.workspace) {
|
|
52
|
+
tools.materialize_skill_resource = materializeSkillResourceTool(
|
|
53
|
+
bindings,
|
|
54
|
+
options.workspace,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Object.entries(tools).map(([name, tool]) => {
|
|
59
|
+
const adapted = aiToolToPi(name, tool, {
|
|
60
|
+
label: SKILL_TOOL_LABELS[name] ?? name,
|
|
61
|
+
...(name === "read_skill_resource"
|
|
62
|
+
? {
|
|
63
|
+
description:
|
|
64
|
+
"Read a bundled resource listed by activate_skill. Returns structured resource metadata and content. Do not pass SKILL.md; activate_skill returns the Skill instructions.",
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
67
|
+
});
|
|
68
|
+
const properties = (adapted.parameters as { properties?: unknown }).properties;
|
|
69
|
+
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
70
|
+
for (const [parameter, description] of Object.entries(
|
|
71
|
+
SKILL_TOOL_PARAMETER_DESCRIPTIONS[name] ?? {},
|
|
72
|
+
)) {
|
|
73
|
+
const schema = (properties as Record<string, unknown>)[parameter];
|
|
74
|
+
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
|
75
|
+
(schema as Record<string, unknown>).description ??= description;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (name === "read_skill_resource") wrapSkillResourceRead(adapted, bindings);
|
|
80
|
+
if (name === "run_skill_script") wrapSkillScript(adapted, options.workspace);
|
|
81
|
+
return {
|
|
82
|
+
owner: "runtime-skill",
|
|
83
|
+
requiredExecutionLevel: "safe",
|
|
84
|
+
// activate_skill 返回的是 Skill 指令本身,是模型接下来所有动作的依据。把它外置
|
|
85
|
+
// 成文件,模型手里就只剩一个路径,必须再取一次才能知道该做什么 —— 而典型
|
|
86
|
+
// SKILL.md 正好落在会触发外置的区间。指令必须当场到手。
|
|
87
|
+
...(name === "activate_skill"
|
|
88
|
+
? { outputBudget: { kind: "structure" as const } }
|
|
89
|
+
: {}),
|
|
90
|
+
...(name === "activate_skill"
|
|
91
|
+
? { exposureMode: "direct" as const }
|
|
92
|
+
: { exposureMode: "codemode" as const }),
|
|
93
|
+
...(name === "activate_skill" || name === "read_skill_resource"
|
|
94
|
+
? { retry: "idempotent" as const }
|
|
95
|
+
: {}),
|
|
96
|
+
tool: adapted,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|