@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.21

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.
Files changed (131) hide show
  1. package/package.json +11 -9
  2. package/src/adapter/cloudflare/agent.ts +233 -179
  3. package/src/adapter/cloudflare/define-agent.ts +252 -104
  4. package/src/adapter/cloudflare/index.ts +18 -0
  5. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  6. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  7. package/src/adapter/cloudflare/schema.ts +21 -2
  8. package/src/adapter/cloudflare/subagent/definition.ts +25 -13
  9. package/src/adapter/cloudflare/subagent/runner.ts +43 -50
  10. package/src/adapter/cloudflare/subagent/tools.ts +4 -4
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
  13. package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
  14. package/src/config/activation.ts +13 -15
  15. package/src/config/agent-context.ts +1 -1
  16. package/src/config/assembler.ts +48 -31
  17. package/src/config/assembly-snapshot.ts +1 -1
  18. package/src/config/definition.ts +1 -1
  19. package/src/config/harness-runtime.ts +77 -49
  20. package/src/config/index.ts +1 -1
  21. package/src/config/model-guidance.ts +20 -0
  22. package/src/config/ports.ts +57 -7
  23. package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
  24. package/src/config/public-contracts.ts +2 -2
  25. package/src/config/runtime-config.ts +2 -1
  26. package/src/context/assembly.ts +1 -1
  27. package/src/context/extension/assembly.ts +2 -2
  28. package/src/context/memory/budget/gate.ts +19 -3
  29. package/src/conversation/harness-resume.ts +89 -238
  30. package/src/conversation/harness-stream.ts +87 -71
  31. package/src/conversation/history.ts +154 -152
  32. package/src/conversation/message/contract.ts +28 -4
  33. package/src/conversation/message/conversion.ts +46 -5
  34. package/src/conversation/message/projection.ts +61 -21
  35. package/src/conversation/session-entry.ts +29 -0
  36. package/src/conversation/state.ts +23 -2
  37. package/src/harness/governance.ts +206 -0
  38. package/src/harness/index.ts +3 -0
  39. package/src/harness/session-harness.ts +269 -445
  40. package/src/harness/submission-operation.ts +15 -40
  41. package/src/index.ts +18 -8
  42. package/src/lib/artifacts.ts +18 -0
  43. package/src/lib/execution-level.ts +13 -0
  44. package/src/lib/prompt.ts +9 -10
  45. package/src/lib/token-estimate.ts +8 -0
  46. package/src/model-protocol/openrouter.ts +41 -0
  47. package/src/models/index.ts +200 -35
  48. package/src/models/openrouter/messages.ts +13 -8
  49. package/src/models/openrouter/openai.ts +200 -0
  50. package/src/models/openrouter/protocol-response.ts +85 -0
  51. package/src/models/provider-streams.ts +45 -0
  52. package/src/session/cloudflare/migration.ts +382 -864
  53. package/src/session/cloudflare/storage.ts +13 -536
  54. package/src/session/index.ts +22 -35
  55. package/src/skills/springbrand-worker-website/index.ts +1 -1
  56. package/src/submission/admission.ts +20 -30
  57. package/src/submission/contracts.ts +42 -2
  58. package/src/submission/delivery.ts +1 -1
  59. package/src/submission/finalization.ts +36 -15
  60. package/src/submission/lifecycle.ts +28 -15
  61. package/src/submission/repository.ts +97 -49
  62. package/src/submission/steer-delivery.ts +32 -76
  63. package/src/submission/unattended.ts +40 -0
  64. package/src/telemetry/contract.ts +0 -1
  65. package/src/telemetry/coordinator.ts +1 -1
  66. package/src/telemetry/runtime-telemetry.ts +0 -4
  67. package/src/telemetry/subagent-lifecycle.ts +1 -1
  68. package/src/telemetry/turn-events.ts +35 -25
  69. package/src/tools/README.md +44 -0
  70. package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
  71. package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
  72. package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
  73. package/src/tools/assistant/ask-user.ts +319 -0
  74. package/src/tools/assistant/candidate.ts +15 -0
  75. package/src/tools/assistant/followups.ts +34 -0
  76. package/src/tools/assistant/index.ts +19 -0
  77. package/src/tools/assistant/memory.ts +73 -0
  78. package/src/tools/assistant/plan.ts +98 -0
  79. package/src/tools/{time.ts → assistant/time.ts} +1 -1
  80. package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
  81. package/src/tools/{core.ts → codemode/index.ts} +36 -92
  82. package/src/tools/codemode/workspace-connector.ts +198 -0
  83. package/src/tools/extension/index.ts +42 -0
  84. package/src/tools/index.ts +29 -26
  85. package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
  86. package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
  87. package/src/tools/runtime/activation.ts +40 -0
  88. package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
  89. package/src/tools/runtime/compiler.ts +246 -0
  90. package/src/tools/runtime/contracts.ts +140 -0
  91. package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
  92. package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
  93. package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
  94. package/src/tools/runtime/result.ts +22 -0
  95. package/src/tools/runtime/unattended-error.ts +4 -0
  96. package/src/tools/sandbox/index.ts +228 -0
  97. package/src/tools/schedule/index.ts +301 -0
  98. package/src/tools/skill/contracts.ts +20 -0
  99. package/src/tools/skill/index.ts +99 -0
  100. package/src/tools/skill/resources.ts +322 -0
  101. package/src/tools/skill/scripts.ts +168 -0
  102. package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
  103. package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
  104. package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
  105. package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
  106. package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
  107. package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
  108. package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
  109. package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
  110. package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
  111. package/src/tools/web/index.ts +2 -0
  112. package/src/tools/web/search/api.ts +1322 -0
  113. package/src/tools/web/search/index.ts +2 -0
  114. package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
  115. package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
  116. package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
  117. package/src/tools/base.ts +0 -534
  118. package/src/tools/compiler.ts +0 -327
  119. package/src/tools/schedule.ts +0 -261
  120. package/src/tools/skill.ts +0 -565
  121. package/src/tools/web-search/api.ts +0 -1318
  122. package/src/tools/web-search/index.ts +0 -2
  123. package/src/tools/workspace-state-connector.ts +0 -146
  124. package/src/tools/{approval → runtime/approval}/index.ts +0 -0
  125. package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
  126. package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
  127. package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
  128. package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
  129. package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
  130. package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
  131. /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
@@ -0,0 +1,322 @@
1
+ import { type SkillResource, type SkillSource } from "agents/skills";
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+ import type { WorkspacePort } from "../../config/ports";
5
+ import type { RuntimeSkillBodyTransform } from "../../config/definition";
6
+ import { STORAGE_LEAF_MAX_CHARS } from "../../context/memory/budget/gate";
7
+ import { result } from "../runtime/result";
8
+ import { serializeWorkspaceMutation } from "../workspace";
9
+ import type { PiSkillBinding } from "./contracts";
10
+
11
+ /**
12
+ * `run_skill_script` 一次挂载的 Skill 资源总量上限。
13
+ *
14
+ * 上游 SkillRegistry 在启动脚本前会把 Skill 的**全部**资源读进内存
15
+ * (`readSkillResources`),运行器随后还要再复制几份:files map、base64 解码后的
16
+ * 字节、以及内联进生成源码的那份 JSON。同一个资源包因此会在 Worker isolate 里
17
+ * 同时存在三到四份,按四倍放大倒推 128 MB 的 DO 内存上限,安全线落在 8 MB。
18
+ *
19
+ * 这道闸只认体积不认 Skill 名字:以后再上一个大包不需要改代码。
20
+ */
21
+ const SKILL_RESOURCE_BUDGET_BYTES = 8 * 1024 * 1024;
22
+
23
+ type LoadedSkill = NonNullable<Awaited<ReturnType<SkillSource["load"]>>>;
24
+
25
+ const SKILL_RESOURCE_READ_GUIDANCE =
26
+ "Bundled Skill resources are not Workspace files. " +
27
+ "Inside execute, use tools.read_skill_resource for text instead of state.* on /skills.";
28
+
29
+ const SKILL_RESOURCE_MATERIALIZE_GUIDANCE =
30
+ "Inside execute, use tools.materialize_skill_resource for Workspace assets when available.";
31
+
32
+ /**
33
+ * 按体积裁掉超预算的 Skill 资源,并把裁掉的事实写回 Skill 正文。
34
+ *
35
+ * @remarks
36
+ * `catalogSkillSource` 在每次 `load` 后调用,必须在这一层拦截:上游拿到 descriptor
37
+ * 后会立刻把它们全读进内存,等运行器收到请求时资源已经在堆上,再判断就晚了。
38
+ *
39
+ * 裁掉的资源必须**说出来**。它们同时会从 `activate_skill` 的资源清单里消失,若不
40
+ * 在正文里点名,模型只会看到一个凭空少了文件的 Skill,然后去猜。
41
+ *
42
+ * 已知缺口:没有 `size` 的 descriptor 不计入预算。R2 来源的 size 来自对象列表因而
43
+ * 总是存在;manifest 来源可能没有,此时这道闸对那些资源不生效。宁可放过也不误杀,
44
+ * 因为误杀会让本来能跑的 Skill 直接坏掉。
45
+ */
46
+ function budgetSkillResources(skill: LoadedSkill): LoadedSkill {
47
+ const resources = skill.resources ?? [];
48
+ const kept: typeof resources = [];
49
+ const dropped: { path: string; size: number }[] = [];
50
+ let used = 0;
51
+
52
+ for (const resource of resources) {
53
+ if (typeof resource.size !== "number") {
54
+ kept.push(resource);
55
+ continue;
56
+ }
57
+ if (used + resource.size > SKILL_RESOURCE_BUDGET_BYTES) {
58
+ dropped.push({ path: resource.path, size: resource.size });
59
+ continue;
60
+ }
61
+ used += resource.size;
62
+ kept.push(resource);
63
+ }
64
+
65
+ if (dropped.length === 0) return skill;
66
+ const budgetMb = Math.round(SKILL_RESOURCE_BUDGET_BYTES / (1024 * 1024));
67
+ // materialize_skill_resource 也要把整份资源读进内存(base64 字符串 → atob 副本 →
68
+ // Uint8Array),放大倍数和挂载路径同量级。把一个单独就超预算的文件推荐给它,只是把
69
+ // OOM 从脚本挪到复制那一步 —— 那种资源这一轮就是拿不到,必须直说。
70
+ const portable = dropped.filter((entry) => entry.size <= SKILL_RESOURCE_BUDGET_BYTES);
71
+ const oversized = dropped.filter((entry) => entry.size > SKILL_RESOURCE_BUDGET_BYTES);
72
+ const lines = [
73
+ `> ${dropped.length} bundled resource(s) exceed the ${budgetMb} MB ` +
74
+ "script-mount budget and are NOT available to run_skill_script.",
75
+ ];
76
+ if (portable.length > 0) {
77
+ lines.push(
78
+ `> If any of these resources are needed, copy all required ones into the Workspace ` +
79
+ `in one execute with tools.materialize_skill_resource: ` +
80
+ `${portable.map((entry) => entry.path).join(", ")}.`,
81
+ );
82
+ }
83
+ if (oversized.length > 0) {
84
+ lines.push(
85
+ `> These are individually larger than ${budgetMb} MB and cannot be loaded at ` +
86
+ `all in this session — do not try: ${oversized.map((entry) => entry.path).join(", ")}.`,
87
+ );
88
+ }
89
+ return { ...skill, resources: kept, body: `${skill.body}\n\n${lines.join("\n")}` };
90
+ }
91
+
92
+ export function catalogSkillSource(
93
+ binding: PiSkillBinding,
94
+ transformSkillBody: RuntimeSkillBodyTransform | undefined,
95
+ availableToolNames: readonly string[],
96
+ ): SkillSource {
97
+ const source = binding.source;
98
+ return {
99
+ id: source.id,
100
+ get fingerprint() {
101
+ return source.fingerprint;
102
+ },
103
+ // Runtime configuration already resolved the catalogue; Skill content stays lazy.
104
+ list: async () => [{
105
+ name: binding.name,
106
+ description: binding.description,
107
+ sourceId: source.id,
108
+ }],
109
+ load: async (name) => {
110
+ const skill = await source.load(name);
111
+ if (!skill) return null;
112
+ const prepared = transformSkillBody
113
+ ? {
114
+ ...skill,
115
+ body: transformSkillBody({
116
+ name: skill.name,
117
+ body: skill.body,
118
+ availableToolNames,
119
+ }),
120
+ }
121
+ : skill;
122
+ const loaded = budgetSkillResources(prepared);
123
+ const hasOversizedResource = skill.resources?.some((resource) =>
124
+ typeof resource.size === "number" &&
125
+ resource.size > SKILL_RESOURCE_BUDGET_BYTES
126
+ );
127
+ const guidance = hasOversizedResource
128
+ ? SKILL_RESOURCE_READ_GUIDANCE
129
+ : `${SKILL_RESOURCE_READ_GUIDANCE} ${SKILL_RESOURCE_MATERIALIZE_GUIDANCE}`;
130
+ return loaded.resources?.length
131
+ ? {
132
+ ...loaded,
133
+ body: `${loaded.body}\n\n${guidance}`,
134
+ }
135
+ : loaded;
136
+ },
137
+ ...(source.readResource
138
+ ? {
139
+ readResource: (name: string, path: string) =>
140
+ source.readResource!(name, path)
141
+ }
142
+ : {}),
143
+ ...(source.refresh ? { refresh: () => source.refresh!() } : {}),
144
+ };
145
+ }
146
+
147
+ const SKILL_ENTRY_READ_GUIDANCE =
148
+ "SKILL.md contains the Skill instructions; use activate_skill instead.";
149
+
150
+ function resourceTarget(
151
+ bindings: readonly PiSkillBinding[],
152
+ input: unknown,
153
+ ): { binding: PiSkillBinding; path: string } | null {
154
+ if (input === null || typeof input !== "object") return null;
155
+ const target = input as { name?: unknown; path?: unknown };
156
+ if (typeof target.path !== "string") return null;
157
+ if (typeof target.name === "string") {
158
+ const binding = bindings.find(({ name }) => name === target.name);
159
+ return binding ? { binding, path: target.path } : null;
160
+ }
161
+ const [name, ...rest] = target.path.split("/");
162
+ const binding = bindings.find((candidate) => candidate.name === name);
163
+ return binding && rest.length > 0
164
+ ? { binding, path: rest.join("/") }
165
+ : null;
166
+ }
167
+
168
+ function materializationRequired(name: string, path: string, bytes?: number) {
169
+ return result({
170
+ status: "materialization_required",
171
+ name,
172
+ path,
173
+ ...(bytes === undefined ? {} : { bytes }),
174
+ next: "tools.materialize_skill_resource",
175
+ });
176
+ }
177
+
178
+ function structuredSkillResource(
179
+ details: string,
180
+ target: { binding: PiSkillBinding; path: string },
181
+ ): ReturnType<typeof result> | null {
182
+ const firstNewline = details.indexOf("\n");
183
+ const closing = "\n</skill_resource>";
184
+ if (
185
+ firstNewline < 0 ||
186
+ !details.startsWith("<skill_resource ") ||
187
+ !details.endsWith(closing)
188
+ ) return null;
189
+ const header = details.slice(0, firstNewline);
190
+ const attribute = (name: string) =>
191
+ new RegExp(`${name}="([^"]*)"`, "u").exec(header)?.[1];
192
+ const kind = attribute("kind");
193
+ const encoding = attribute("encoding");
194
+ if (!kind || !encoding) return null;
195
+ const mimeType = attribute("mimeType");
196
+ return result({
197
+ name: target.binding.name,
198
+ path: target.path,
199
+ kind,
200
+ encoding,
201
+ ...(mimeType ? { mimeType } : {}),
202
+ content: details.slice(firstNewline + 1, -closing.length),
203
+ });
204
+ }
205
+
206
+ export function resourceBytes(resource: SkillResource): Uint8Array {
207
+ if ((resource.encoding ?? "text") === "text") {
208
+ return new TextEncoder().encode(resource.content);
209
+ }
210
+ const binary = atob(resource.content);
211
+ const bytes = new Uint8Array(binary.length);
212
+ for (let index = 0;index < binary.length;index++) {
213
+ bytes[index] = binary.charCodeAt(index);
214
+ }
215
+ return bytes;
216
+ }
217
+
218
+ export function materializeSkillResourceTool(
219
+ bindings: readonly PiSkillBinding[],
220
+ workspace: WorkspacePort,
221
+ ) {
222
+ const names = bindings.map(({ name }) => name) as [string, ...string[]];
223
+ const byName = new Map(bindings.map((binding) => [binding.name, binding]));
224
+ return tool({
225
+ description:
226
+ "Copy a complete bundled Skill resource directly into the Workspace without returning its contents to the model. " +
227
+ "Use this instead of read_skill_resource when a template, script, image, font, or other asset must become a Workspace file. " +
228
+ "The destination and its parent directories are created automatically; do not call state.mkdir first. " +
229
+ "When multiple resources are needed, copy them sequentially in one execute; never start one execute per resource. " +
230
+ "The destination is created or overwritten, including parent directories.",
231
+ inputSchema: z.object({
232
+ name: z.enum(names).describe("Activated Skill name"),
233
+ path: z.string().min(1).describe("Bundled resource path listed by activate_skill"),
234
+ destination: z.string().min(1).describe("Absolute destination path in the Workspace"),
235
+ }),
236
+ execute: async ({ name, path, destination }, { abortSignal }) => {
237
+ abortSignal?.throwIfAborted();
238
+ const source = byName.get(name)?.source;
239
+ if (!source?.readResource) {
240
+ throw new Error(`Skill \"${name}\" has no readable resources.`);
241
+ }
242
+ const resource = await source.readResource(name, path);
243
+ if (!resource) throw new Error(`Resource not found: ${name}/${path}`);
244
+ abortSignal?.throwIfAborted();
245
+ const bytes = resourceBytes(resource);
246
+
247
+ await serializeWorkspaceMutation(workspace, destination, async () => {
248
+ abortSignal?.throwIfAborted();
249
+ const parent = destination.replace(/\/[^/]+$/, "");
250
+ if (parent && parent !== "/") {
251
+ await workspace.mkdir(parent, { recursive: true });
252
+ }
253
+ await workspace.writeFileBytes(destination, bytes, resource.mimeType);
254
+ });
255
+
256
+ return {
257
+ name,
258
+ path: resource.path,
259
+ destination,
260
+ bytesWritten: bytes.byteLength,
261
+ encoding: resource.encoding ?? "text",
262
+ ...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
263
+ };
264
+ },
265
+ });
266
+ }
267
+
268
+ export function wrapSkillResourceRead(adapted: import("@earendil-works/pi-agent-core").AgentTool<any, unknown>, bindings: readonly PiSkillBinding[]): void {
269
+ const execute = adapted.execute;
270
+ adapted.execute = async (toolCallId, input, signal) => {
271
+ const target = input as { name?: unknown; path?: unknown };
272
+ const parts = typeof target.path === "string"
273
+ ? target.path.split("/")
274
+ : [];
275
+ if (
276
+ target.path === "SKILL.md" ||
277
+ (target.name === undefined &&
278
+ parts.length === 2 &&
279
+ parts[1] === "SKILL.md")
280
+ ) {
281
+ return {
282
+ content: [{ type: "text", text: SKILL_ENTRY_READ_GUIDANCE }],
283
+ details: SKILL_ENTRY_READ_GUIDANCE,
284
+ };
285
+ }
286
+ const resolved = resourceTarget(bindings, input);
287
+ if (resolved) {
288
+ const skill = await resolved.binding.source
289
+ .load(resolved.binding.name)
290
+ .catch(() => null);
291
+ const descriptor = skill?.resources?.find(({ path }) =>
292
+ path === resolved.path
293
+ );
294
+ if (
295
+ typeof descriptor?.size === "number" &&
296
+ descriptor.size > STORAGE_LEAF_MAX_CHARS
297
+ ) {
298
+ return materializationRequired(
299
+ resolved.binding.name,
300
+ resolved.path,
301
+ descriptor.size,
302
+ );
303
+ }
304
+ }
305
+ const output = await execute(toolCallId, input, signal);
306
+ if (
307
+ resolved &&
308
+ typeof output.details === "string" &&
309
+ output.details.length > STORAGE_LEAF_MAX_CHARS
310
+ ) {
311
+ return materializationRequired(
312
+ resolved.binding.name,
313
+ resolved.path,
314
+ );
315
+ }
316
+ if (resolved && typeof output.details === "string") {
317
+ const structured = structuredSkillResource(output.details, resolved);
318
+ if (structured) return structured;
319
+ }
320
+ return output;
321
+ };
322
+ }
@@ -0,0 +1,168 @@
1
+ import {
2
+ runner as createSkillScriptRunner,
3
+ type SkillScriptRequest,
4
+ type SkillScriptRunner,
5
+ } from "agents/skills";
6
+ import { truncateResponse, truncateResult } from "@cloudflare/codemode";
7
+ import type { WorkspacePort } from "../../config/ports";
8
+ import { piCandidatesToAiTools } from "../adapters/pi-to-ai";
9
+ import { result } from "../runtime/result";
10
+ import { serializeWorkspaceMutation } from "../workspace";
11
+ import type { PiSkillBinding, SkillPiToolOptions } from "./contracts";
12
+ import { resourceBytes } from "./resources";
13
+
14
+ const SKILL_SCRIPT_OUTPUT_ROOT = "/scratch/skill-output";
15
+
16
+ export function scriptRunner(
17
+ bindings: readonly PiSkillBinding[],
18
+ options: SkillPiToolOptions,
19
+ ): SkillScriptRunner | null {
20
+ if (!options.loader || !bindings.some((binding) => binding.script)) {
21
+ return null;
22
+ }
23
+ const byName = new Map(bindings.map((binding) => [binding.name, binding]));
24
+ return {
25
+ async run(request: SkillScriptRequest) {
26
+ const policy = byName.get(request.skill.name)?.script;
27
+ if (!policy) {
28
+ throw new Error(`Skill cannot run scripts: ${request.skill.name}`);
29
+ }
30
+ const workspace = policy.workspace !== "none" && options.workspace
31
+ ? policy.workspace
32
+ : "none";
33
+ let toolCallOrdinal = 0;
34
+ const selected = (options.tools ?? []).filter(({ tool }) =>
35
+ policy.tools.includes(tool.name)
36
+ );
37
+ return createSkillScriptRunner({
38
+ loader: options.loader!,
39
+ network: policy.network === "full",
40
+ workspace,
41
+ ...(workspace !== "none" && options.workspace
42
+ ? { workspaceInstance: options.workspace }
43
+ : {}),
44
+ tools: piCandidatesToAiTools(selected, {
45
+ fallbackToolCallId: (name) =>
46
+ `skill-script:${++toolCallOrdinal}:${name}`,
47
+ }),
48
+ }).run(request);
49
+ },
50
+ };
51
+ }
52
+
53
+ function normalizeRunSkillScriptArguments(input: unknown): unknown {
54
+ if (input === null || typeof input !== "object") return input;
55
+ const value = input as Record<string, unknown>;
56
+ if (typeof value.input !== "string") return input;
57
+ try {
58
+ const decoded = JSON.parse(value.input);
59
+ return decoded !== null && typeof decoded === "object"
60
+ ? { ...value, input: decoded }
61
+ : input;
62
+ } catch {
63
+ return input;
64
+ }
65
+ }
66
+
67
+ type ScriptOutputFile = {
68
+ path?: unknown;
69
+ content?: unknown;
70
+ encoding?: unknown;
71
+ mimeType?: unknown;
72
+ };
73
+
74
+ function scratchSegment(value: string): string {
75
+ const segment = value.replace(/[^a-zA-Z0-9._-]+/g, "_");
76
+ return !segment || segment === "." || segment === ".." ? "_" : segment;
77
+ }
78
+
79
+ function outputRelativePath(value: unknown): string {
80
+ const path = String(value ?? "output.txt").replace(/^\/?output\//, "");
81
+ const parts = path.split("/");
82
+ if (parts.some((part) => !part || part === "." || part === "..")) {
83
+ throw new Error(`Invalid Skill output path: ${path}`);
84
+ }
85
+ return parts.join("/");
86
+ }
87
+
88
+ async function projectSkillScriptResult(
89
+ details: unknown,
90
+ input: unknown,
91
+ toolCallId: string,
92
+ workspace: WorkspacePort | undefined,
93
+ signal: AbortSignal | undefined,
94
+ ): Promise<unknown> {
95
+ const record = details !== null && typeof details === "object"
96
+ ? details as Record<string, unknown>
97
+ : null;
98
+ const outputFiles = Array.isArray(record?.outputFiles)
99
+ ? record.outputFiles as ScriptOutputFile[]
100
+ : [];
101
+ const target = input as { name?: unknown };
102
+ const outputs: Array<{ path: string; bytes: number; mediaType?: string }> = [];
103
+ if (outputFiles.length > 0 && !workspace) {
104
+ throw new Error("Skill output files require Workspace access");
105
+ }
106
+ for (const file of outputFiles) {
107
+ signal?.throwIfAborted();
108
+ const relative = outputRelativePath(file.path);
109
+ const destination = `${SKILL_SCRIPT_OUTPUT_ROOT}/${scratchSegment(String(target.name ?? "skill"))}/${scratchSegment(toolCallId)}/${relative}`;
110
+ const content = String(file.content ?? "");
111
+ const binary = file.encoding === "base64";
112
+ const mediaType = typeof file.mimeType === "string"
113
+ ? file.mimeType
114
+ : binary ? "application/octet-stream" : "text/plain";
115
+ let bytes = new Blob([content]).size;
116
+ await serializeWorkspaceMutation(workspace!, destination, async () => {
117
+ const parent = destination.replace(/\/[^/]+$/, "");
118
+ await workspace!.mkdir(parent, { recursive: true });
119
+ if (!binary) {
120
+ await workspace!.writeFile(destination, content, mediaType);
121
+ return;
122
+ }
123
+ const decoded = resourceBytes({
124
+ path: relative,
125
+ kind: "file",
126
+ encoding: "base64",
127
+ content,
128
+ });
129
+ bytes = decoded.byteLength;
130
+ await workspace!.writeFileBytes(destination, decoded, mediaType);
131
+ });
132
+ outputs.push({ path: destination, bytes, mediaType });
133
+ }
134
+ const scriptResult = record && "result" in record
135
+ ? record.result
136
+ : record && ("outputFiles" in record || "logs" in record)
137
+ ? Object.fromEntries(Object.entries(record).filter(([key]) =>
138
+ key !== "outputFiles" && key !== "logs"
139
+ ))
140
+ : details;
141
+ const logs = Array.isArray(record?.logs)
142
+ ? record.logs.map(String)
143
+ : [];
144
+ return {
145
+ result: truncateResult(scriptResult),
146
+ ...(logs.length > 0
147
+ ? { logs: [truncateResponse(logs.join("\n"))] }
148
+ : {}),
149
+ ...(outputs.length > 0 ? { outputs } : {}),
150
+ };
151
+ }
152
+
153
+ export function wrapSkillScript(adapted: import("@earendil-works/pi-agent-core").AgentTool<any, unknown>, workspace: WorkspacePort | undefined): void {
154
+ adapted.description +=
155
+ " Large artifacts must be written directly to Workspace, not returned or logged; scratch outputs are materialized to Workspace and returned only as file references.";
156
+ adapted.prepareArguments = normalizeRunSkillScriptArguments;
157
+ const execute = adapted.execute;
158
+ adapted.execute = async (toolCallId, input, signal) => {
159
+ const output = await execute(toolCallId, input, signal);
160
+ return result(await projectSkillScriptResult(
161
+ output.details,
162
+ input,
163
+ toolCallId,
164
+ workspace,
165
+ signal,
166
+ ));
167
+ };
168
+ }
@@ -47,10 +47,10 @@ export interface RuntimeSubmissionAdmissionInput {
47
47
  export type RuntimeSubmissionAdmissionResult =
48
48
  | { readonly accepted: true }
49
49
  | {
50
- readonly accepted: false;
51
- readonly code: string;
52
- readonly retryable?: boolean;
53
- };
50
+ readonly accepted: false;
51
+ readonly code: string;
52
+ readonly retryable?: boolean;
53
+ };
54
54
 
55
55
  export type RuntimeSubmissionAdmissionHook = (
56
56
  input: RuntimeSubmissionAdmissionInput,
@@ -58,36 +58,36 @@ export type RuntimeSubmissionAdmissionHook = (
58
58
 
59
59
  export type RuntimeLifecycleFact =
60
60
  | {
61
- readonly kind: "submission-admitted";
62
- readonly eventId: string;
63
- readonly submissionId: string;
64
- readonly runId: string;
65
- }
61
+ readonly kind: "submission-admitted";
62
+ readonly eventId: string;
63
+ readonly submissionId: string;
64
+ readonly runId: string;
65
+ }
66
66
  | {
67
- readonly kind: "submission-terminal";
68
- readonly eventId: string;
69
- readonly submissionId: string;
70
- readonly runId: string;
71
- readonly status: "completed" | "aborted" | "skipped" | "error";
72
- readonly childRegistrationClosed: true;
73
- readonly zeroUsage: boolean;
74
- }
67
+ readonly kind: "submission-terminal";
68
+ readonly eventId: string;
69
+ readonly submissionId: string;
70
+ readonly runId: string;
71
+ readonly status: "completed" | "aborted" | "skipped" | "error";
72
+ readonly childRegistrationClosed: true;
73
+ readonly zeroUsage: boolean;
74
+ }
75
75
  | {
76
- readonly kind: "subagent-run-registered";
77
- readonly eventId: string;
78
- readonly submissionId: string;
79
- readonly subagentRunId: string;
80
- readonly parentRunId: string;
81
- }
76
+ readonly kind: "subagent-run-registered";
77
+ readonly eventId: string;
78
+ readonly submissionId: string;
79
+ readonly subagentRunId: string;
80
+ readonly parentRunId: string;
81
+ }
82
82
  | {
83
- readonly kind: "subagent-terminal";
84
- readonly eventId: string;
85
- readonly submissionId: string;
86
- readonly subagentRunId: string;
87
- readonly parentRunId: string;
88
- readonly status: "completed" | "failed" | "aborted" | "skipped";
89
- readonly zeroUsage: boolean;
90
- };
83
+ readonly kind: "subagent-terminal";
84
+ readonly eventId: string;
85
+ readonly submissionId: string;
86
+ readonly subagentRunId: string;
87
+ readonly parentRunId: string;
88
+ readonly status: "completed" | "failed" | "aborted" | "skipped";
89
+ readonly zeroUsage: boolean;
90
+ };
91
91
 
92
92
  export interface RuntimeEventConfirmation {
93
93
  readonly confirmed: true;
@@ -5,14 +5,11 @@ import type {
5
5
  RuntimeSubagentPort,
6
6
  RuntimeSubagentRunResult,
7
7
  RuntimeSubagentUsageEvent,
8
- } from "../config/ports";
9
- import { AGENT_TYPES } from "./subagent-types/registry";
10
- import { serializeOutput } from "../lib/artifacts";
11
- import {
12
- isZeroSubagentUsage,
13
- subagentTerminalStatus,
14
- } from "./subagent-runtime";
15
- import type { PiToolCandidate } from "./compiler";
8
+ } from "../../config/ports";
9
+ import { serializeOutput } from "../../lib/artifacts";
10
+ import type { PiToolCandidate } from "../runtime/contracts";
11
+ import { isZeroSubagentUsage, subagentTerminalStatus } from "./runtime";
12
+ import { AGENT_TYPES } from "./types/registry";
16
13
 
17
14
  const BACKGROUND_MAX_BUDGET_MS = 10 * 60 * 1_000;
18
15
 
@@ -105,7 +102,7 @@ async function recordSubagentResult(
105
102
  * 会等待子任务结果,“后台派发”只返回 runId,并由 Host 通过通知把最终结果送回
106
103
  * 当前会话。注册表同时生成工具 Schema 和再次校验后台 input,不能改成两份定义。
107
104
  *
108
- * `Host`、`Pi`、`Port`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
105
+ * `Host`、`Pi`、`Port`、`Runtime Snapshot` 与“候选工具”见 `src/index.ts`。
109
106
  */
110
107
  export function subagentPiToolCandidates(
111
108
  subagents: RuntimeSubagentPort | undefined,
@@ -143,25 +140,25 @@ export function subagentPiToolCandidates(
143
140
  }
144
141
  const reserved = lifecycle
145
142
  ? await subagents.reserve!(type.name, childInput, {
146
- parentRunId: lifecycle.parentRunId,
147
- submissionId: lifecycle.submissionId,
148
- toolCallId: _toolCallId,
149
- })
143
+ parentRunId: lifecycle.parentRunId,
144
+ submissionId: lifecycle.submissionId,
145
+ toolCallId: _toolCallId,
146
+ })
150
147
  : undefined;
151
148
  if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
152
149
  const runOptions = lifecycle
153
150
  ? {
154
- parentRunId: lifecycle.parentRunId,
155
- submissionId: lifecycle.submissionId,
156
- ...(reserved ? { subagentRunId: reserved.runId } : {}),
157
- }
151
+ parentRunId: lifecycle.parentRunId,
152
+ submissionId: lifecycle.submissionId,
153
+ ...(reserved ? { subagentRunId: reserved.runId } : {}),
154
+ }
158
155
  : undefined;
159
156
  const run = lifecycle && reserved
160
157
  ? await runRegisteredSubagent(
161
- lifecycle,
162
- reserved.runId,
163
- () => subagents.run(type.name, childInput, runOptions),
164
- )
158
+ lifecycle,
159
+ reserved.runId,
160
+ () => subagents.run(type.name, childInput, runOptions),
161
+ )
165
162
  : await subagents.run(type.name, childInput);
166
163
  if (lifecycle && !run.childStillRunning) {
167
164
  await recordSubagentResult(lifecycle, run);
@@ -213,14 +210,14 @@ export function subagentPiToolCandidates(
213
210
  }
214
211
  const reserved = lifecycle
215
212
  ? await subagents.reserve!(
216
- input.agentType,
217
- parsed.data as Record<string, unknown>,
218
- {
219
- parentRunId: lifecycle.parentRunId,
220
- submissionId: lifecycle.submissionId,
221
- toolCallId,
222
- },
223
- )
213
+ input.agentType,
214
+ parsed.data as Record<string, unknown>,
215
+ {
216
+ parentRunId: lifecycle.parentRunId,
217
+ submissionId: lifecycle.submissionId,
218
+ toolCallId,
219
+ },
220
+ )
224
221
  : undefined;
225
222
  if (reserved && lifecycle) await lifecycle.onRegistered(reserved.runId);
226
223
  const runInput = parsed.data as Record<string, unknown>;
@@ -230,18 +227,18 @@ export function subagentPiToolCandidates(
230
227
  notifySource: "dispatch-background",
231
228
  ...(lifecycle
232
229
  ? {
233
- parentRunId: lifecycle.parentRunId,
234
- submissionId: lifecycle.submissionId,
235
- ...(reserved ? { subagentRunId: reserved.runId } : {}),
236
- }
230
+ parentRunId: lifecycle.parentRunId,
231
+ submissionId: lifecycle.submissionId,
232
+ ...(reserved ? { subagentRunId: reserved.runId } : {}),
233
+ }
237
234
  : {}),
238
235
  };
239
236
  const run = lifecycle && reserved
240
237
  ? await runRegisteredSubagent(
241
- lifecycle,
242
- reserved.runId,
243
- () => subagents.run(input.agentType, runInput, runOptions),
244
- )
238
+ lifecycle,
239
+ reserved.runId,
240
+ () => subagents.run(input.agentType, runInput, runOptions),
241
+ )
245
242
  : await subagents.run(input.agentType, runInput, runOptions);
246
243
  if (lifecycle && !run.childStillRunning && run.status !== "running") {
247
244
  await recordSubagentResult(lifecycle, run);