@springbrand/agent-runtime 0.1.3-alpha.8 → 0.2.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +4 -3
  2. package/src/adapter/cloudflare/index.ts +1 -0
  3. package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
  4. package/src/adapter/cloudflare/subagent/definition.ts +60 -5
  5. package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
  6. package/src/adapter/cloudflare/universal-agent/preparation.ts +36 -2
  7. package/src/adapter/cloudflare/universal-agent/tools.ts +9 -12
  8. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  9. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  10. package/src/db/index.ts +4 -0
  11. package/src/db/runtime-event-outbox.repo.ts +34 -1
  12. package/src/db/schema.ts +42 -0
  13. package/src/db/submission-admission.repo.ts +127 -0
  14. package/src/db/submission.repo.ts +54 -3
  15. package/src/index.ts +5 -2
  16. package/src/kernel/bindings.ts +52 -0
  17. package/src/kernel/durable-lifecycle.ts +100 -0
  18. package/src/kernel/public-contracts.ts +11 -0
  19. package/src/kernel/receipts.ts +1 -0
  20. package/src/kernel/recoverable-chat-agent.ts +0 -19
  21. package/src/kernel/subagent-runtime.ts +137 -0
  22. package/src/kernel/submission-authority.ts +114 -0
  23. package/src/kernel/submission-lifecycle.ts +35 -10
  24. package/src/layers/context/budget/gate.ts +99 -0
  25. package/src/lib/prompt.ts +7 -3
  26. package/src/pi/message/projection.ts +2 -2
  27. package/src/pi/runtime-adapter/assembly.ts +4 -3
  28. package/src/pi/runtime-adapter/execution.ts +55 -16
  29. package/src/pi/runtime-adapter/index.ts +34 -0
  30. package/src/pi/runtime-adapter/models.ts +10 -1
  31. package/src/pi/runtime-adapter/recovery.ts +17 -17
  32. package/src/pi/runtime-adapter/transcript.ts +2 -2
  33. package/src/pi/tool/base.ts +113 -27
  34. package/src/pi/tool/compiler.ts +66 -6
  35. package/src/pi/tool/core-host.ts +50 -23
  36. package/src/pi/tool/core.ts +2 -23
  37. package/src/pi/tool/index.ts +1 -0
  38. package/src/pi/tool/mcp.ts +2 -2
  39. package/src/pi/tool/skill.ts +78 -3
  40. package/src/pi/tool/subagent.ts +142 -16
  41. package/src/pi/tool/workspace-revision.ts +64 -0
  42. package/src/pi/tool/workspace-sandbox.ts +4 -4
  43. package/src/pi/turn/tool-recovery.ts +7 -7
  44. package/src/runtime-agent-context.ts +1 -0
  45. package/src/runtime-agent.ts +8 -3
  46. package/src/runtime-assembler.ts +61 -9
  47. package/src/runtime-definition.ts +12 -0
  48. package/src/runtime.ts +485 -66
  49. package/src/tool-registry.ts +10 -1
  50. package/src/workspace-versioning.ts +46 -0
@@ -201,7 +201,7 @@ function milestoneMutation(
201
201
  // 缺少身份时立即失败,避免把无归属的记录混入另一次 Turn 的恢复日志。
202
202
  function recoveryIdentity(input: PiRecoveryInput) {
203
203
  if (!input.identity) {
204
- throw new Error("Pi recovery command is missing its Turn revision");
204
+ throw new Error("SpringBrand recovery command is missing its Turn revision");
205
205
  }
206
206
  return { version: 1 as const, ...input.identity };
207
207
  }
@@ -255,7 +255,7 @@ function interactionToolResult(
255
255
  timestamp: number,
256
256
  ): ToolResultMessage {
257
257
  if (!interaction) {
258
- throw new Error("Pi Tool interaction is missing for its response");
258
+ throw new Error("SpringBrand Tool interaction is missing for its response");
259
259
  }
260
260
  return {
261
261
  role: "toolResult",
@@ -417,7 +417,7 @@ export function decidePiRecovery(
417
417
  (milestone) => milestone.type === "approval",
418
418
  );
419
419
  if (!approval || approval.type !== "approval") {
420
- throw new Error("Pi approval decision milestone is missing");
420
+ throw new Error("SpringBrand approval decision milestone is missing");
421
421
  }
422
422
  mutations.push({
423
423
  kind: "decide-approval",
@@ -453,7 +453,7 @@ export function decidePiRecovery(
453
453
  JSON.stringify(existing.input) !== JSON.stringify(record.input)
454
454
  ) {
455
455
  throw new Error(
456
- `Conflicting Pi Tool input for ${record.toolCallId}`,
456
+ `Conflicting SpringBrand Tool input for ${record.toolCallId}`,
457
457
  );
458
458
  }
459
459
  } else {
@@ -482,7 +482,7 @@ export function decidePiRecovery(
482
482
  existing.inputJson !== approval.inputJson
483
483
  ) {
484
484
  throw new Error(
485
- `Conflicting Pi Tool approval: ${approval.executionId}`,
485
+ `Conflicting SpringBrand Tool approval: ${approval.executionId}`,
486
486
  );
487
487
  }
488
488
  } else {
@@ -530,7 +530,7 @@ export function decidePiRecovery(
530
530
  existing.inputJson !== interaction.inputJson
531
531
  ) {
532
532
  throw new Error(
533
- `Conflicting Pi Tool interaction: ${interaction.interactionId}`,
533
+ `Conflicting SpringBrand Tool interaction: ${interaction.interactionId}`,
534
534
  );
535
535
  }
536
536
  } else {
@@ -580,7 +580,7 @@ export function decidePiRecovery(
580
580
  (milestone) => milestone.type === "interaction",
581
581
  );
582
582
  if (!record || record.type !== "interaction") {
583
- throw new Error("Pi interaction settlement milestone is missing");
583
+ throw new Error("SpringBrand interaction settlement milestone is missing");
584
584
  }
585
585
  mutations.push({
586
586
  kind: "settle-interaction",
@@ -621,7 +621,7 @@ export function decidePiRecovery(
621
621
  if (existing) {
622
622
  if (JSON.stringify(existing) !== JSON.stringify(toolResult)) {
623
623
  throw new Error(
624
- `Conflicting Pi Tool result for ${command.toolCallId}`,
624
+ `Conflicting SpringBrand Tool result for ${command.toolCallId}`,
625
625
  );
626
626
  }
627
627
  } else {
@@ -676,7 +676,7 @@ export function decidePiRecovery(
676
676
  plan.continuationKey !== input.command.continuationKey
677
677
  ) {
678
678
  throw new Error(
679
- `Pi continuation is not ready: ${input.command.continuationKey}`,
679
+ `SpringBrand continuation is not ready: ${input.command.continuationKey}`,
680
680
  );
681
681
  }
682
682
  const committed = commitPiRecoveryContinuation(
@@ -686,7 +686,7 @@ export function decidePiRecovery(
686
686
  );
687
687
  if (!committed) {
688
688
  throw new Error(
689
- `Pi continuation could not be committed: ${plan.continuationKey}`,
689
+ `SpringBrand continuation could not be committed: ${plan.continuationKey}`,
690
690
  );
691
691
  }
692
692
  const mutation = milestoneMutation(
@@ -809,13 +809,13 @@ export class PiRuntimeRecoveryAdapter
809
809
  if (!durable) {
810
810
  return {
811
811
  kind: "fail",
812
- reason: "Recovered Pi submission is missing",
812
+ reason: "Recovered SpringBrand submission is missing",
813
813
  } as const;
814
814
  }
815
815
  if (durable.terminal) {
816
816
  return {
817
817
  kind: "ignore",
818
- reason: "Pi submission is already terminal",
818
+ reason: "SpringBrand submission is already terminal",
819
819
  } as const;
820
820
  }
821
821
  const decision = decidePiRecovery({
@@ -827,20 +827,20 @@ export class PiRuntimeRecoveryAdapter
827
827
  if (decision.effect.reason === "approval") {
828
828
  return {
829
829
  kind: "park",
830
- reason: "Pi Turn is waiting for Tool approval",
830
+ reason: "SpringBrand Turn is waiting for Tool approval",
831
831
  } as const;
832
832
  }
833
833
  if (decision.effect.reason === "interaction") {
834
834
  return {
835
835
  kind: "park",
836
- reason: "Pi Turn is waiting for a client Tool interaction response",
836
+ reason: "SpringBrand Turn is waiting for a client Tool interaction response",
837
837
  } as const;
838
838
  }
839
839
  if (decision.effect.reason === "uncertain-tool") {
840
840
  return {
841
841
  kind: "park",
842
842
  reason:
843
- "Pi Turn is parked on an uncertain non-idempotent Tool",
843
+ "SpringBrand Turn is parked on an uncertain non-idempotent Tool",
844
844
  incidentStatus: "failed",
845
845
  } as const;
846
846
  }
@@ -848,12 +848,12 @@ export class PiRuntimeRecoveryAdapter
848
848
  return {
849
849
  kind: "fail",
850
850
  reason:
851
- "Pi recovery is complete but the Submission is not terminal",
851
+ "SpringBrand recovery is complete but the Submission is not terminal",
852
852
  } as const;
853
853
  }
854
854
  return {
855
855
  kind: "park",
856
- reason: "Pi recovery is waiting for durable input",
856
+ reason: "SpringBrand recovery is waiting for durable input",
857
857
  } as const;
858
858
  }
859
859
  return {
@@ -526,7 +526,7 @@ export class PiRuntimeTranscript {
526
526
  */
527
527
  importSnapshot(snapshot: PiCanonicalTranscriptSnapshot): void {
528
528
  if (this.hasActiveTurn()) {
529
- throw new Error("Cannot import messages while a Pi Turn is active");
529
+ throw new Error("Cannot import messages while a SpringBrand Turn is active");
530
530
  }
531
531
  this.durability.transaction(() => {
532
532
  let currentSubmissionId: string | undefined;
@@ -551,7 +551,7 @@ export class PiRuntimeTranscript {
551
551
  : {}),
552
552
  });
553
553
  if (!appended) {
554
- throw new Error(`Pi Session message "${entry.id}" already exists`);
554
+ throw new Error(`SpringBrand Session message "${entry.id}" already exists`);
555
555
  }
556
556
  }
557
557
  });
@@ -15,47 +15,122 @@ import {
15
15
  import { webSearchPiToolCandidate } from "./web-search";
16
16
  import type { WebSearch } from "./web-search/api";
17
17
 
18
- const askUserParameters = Type.Object({
18
+ const askUserQuestion = Type.Object({
19
19
  question: Type.String({
20
+ minLength: 1,
20
21
  description: "The question to put to the user.",
21
22
  }),
22
- options: Type.Array(Type.String(), {
23
+ options: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
23
24
  minItems: 2,
24
25
  maxItems: 6,
26
+ uniqueItems: true,
25
27
  description: "2-6 mutually distinct options for the user to choose from.",
26
- }),
28
+ })),
27
29
  multiSelect: Type.Optional(Type.Boolean({
28
30
  description:
29
31
  "Whether the user may select more than one option. Defaults to false.",
30
32
  })),
33
+ allowCustom: Type.Optional(Type.Boolean({
34
+ description:
35
+ "Whether the user may add free text. Defaults to false with options and true without options.",
36
+ })),
37
+ });
38
+
39
+ const askUserParameters = Type.Object({
40
+ questions: Type.Array(askUserQuestion, {
41
+ minItems: 1,
42
+ maxItems: 4,
43
+ description:
44
+ "All user decisions needed to continue. Ask them together in one call.",
45
+ }),
31
46
  });
32
47
 
33
48
  /**
34
49
  * `ask_user` 的客户端响应体。
35
50
  *
36
- * `selections` 是选中的选项原文(多选时多于一项),`text` 是可选的补充说明。
51
+ * `answers` 与 Tool 输入的 `questions` 按下标一一对应。
37
52
  * 刻意用结构化数组而不是拼好的字符串 —— 选项本身可能含分隔符,拼了再拆是有损的。
38
53
  */
39
54
  const askUserResponse = Type.Object({
40
- selections: Type.Array(Type.String()),
41
- text: Type.Optional(Type.String()),
55
+ answers: Type.Array(Type.Object({
56
+ selections: Type.Array(Type.String()),
57
+ text: Type.Optional(Type.String()),
58
+ })),
42
59
  });
43
60
 
44
- // 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
45
- // 且校验必须在 Worker 冷启动路径上零成本。
46
- function isAskUserResponse(value: unknown): boolean {
47
- if (typeof value !== "object" || value === null) return false;
48
- const record = value as Record<string, unknown>;
49
- if (!Array.isArray(record.selections)) return false;
50
- if (!record.selections.every((item) => typeof item === "string")) {
51
- return false;
61
+ type AskUserInput = Static<typeof askUserParameters>;
62
+ type AskUserResponse = Static<typeof askUserResponse>;
63
+
64
+ function recordOf(value: unknown): Record<string, unknown> | null {
65
+ return typeof value === "object" && value !== null && !Array.isArray(value)
66
+ ? value as Record<string, unknown>
67
+ : null;
68
+ }
69
+
70
+ function questionsOf(value: unknown): AskUserInput["questions"] | null {
71
+ const questions = recordOf(value)?.questions;
72
+ if (!Array.isArray(questions) || questions.length < 1 || questions.length > 4) {
73
+ return null;
52
74
  }
53
- if (record.text !== undefined && typeof record.text !== "string") {
75
+ for (const value of questions) {
76
+ const question = recordOf(value);
77
+ if (!question || typeof question.question !== "string" || !question.question) {
78
+ return null;
79
+ }
80
+ if (
81
+ question.multiSelect !== undefined &&
82
+ typeof question.multiSelect !== "boolean"
83
+ ) return null;
84
+ if (
85
+ question.allowCustom !== undefined &&
86
+ typeof question.allowCustom !== "boolean"
87
+ ) return null;
88
+ if (question.options !== undefined) {
89
+ if (
90
+ !Array.isArray(question.options) ||
91
+ question.options.length < 2 ||
92
+ question.options.length > 6 ||
93
+ !question.options.every((option) =>
94
+ typeof option === "string" && option.length > 0
95
+ ) ||
96
+ new Set(question.options).size !== question.options.length
97
+ ) return null;
98
+ }
99
+ }
100
+ return questions as AskUserInput["questions"];
101
+ }
102
+
103
+ // 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
104
+ // 且校验位于 Worker 响应信任边界,不应为每次回答编译 schema。
105
+ function isAskUserResponse(input: unknown, value: unknown): boolean {
106
+ const questions = questionsOf(input);
107
+ const answers = recordOf(value)?.answers;
108
+ if (!questions || !Array.isArray(answers) || answers.length !== questions.length) {
54
109
  return false;
55
110
  }
56
- // 什么都没选、也没写字,等于没回答 —— 不该被当成一次有效结算。
57
- return record.selections.length > 0 ||
58
- (typeof record.text === "string" && record.text.trim().length > 0);
111
+
112
+ return questions.every((question, index) => {
113
+ const answer = recordOf(answers[index]);
114
+ if (!answer || !Array.isArray(answer.selections)) return false;
115
+ if (!answer.selections.every((item) => typeof item === "string")) {
116
+ return false;
117
+ }
118
+ if (answer.text !== undefined && typeof answer.text !== "string") {
119
+ return false;
120
+ }
121
+
122
+ const options = question.options ?? [];
123
+ const text = typeof answer.text === "string" ? answer.text : "";
124
+ if (text.length > 2_000) return false;
125
+ if (answer.selections.some((selection) => !options.includes(selection))) {
126
+ return false;
127
+ }
128
+ if (new Set(answer.selections).size !== answer.selections.length) return false;
129
+ if (!question.multiSelect && answer.selections.length > 1) return false;
130
+ const allowCustom = options.length === 0 || question.allowCustom === true;
131
+ if (!allowCustom && text.trim()) return false;
132
+ return answer.selections.length > 0 || text.trim().length > 0;
133
+ });
59
134
  }
60
135
 
61
136
  const suggestFollowupsParameters = Type.Object({
@@ -162,7 +237,7 @@ export function basePiToolCandidates(
162
237
  name: "ask_user",
163
238
  label: "Ask user",
164
239
  description:
165
- "Ask the user to choose among a small set of options. Call this tool whenever you need the user to make a choice from limited options do NOT write plain text like 'please pick A / B / C'. The user's choice comes back to you as this tool's result, so just continue once you have it. Do NOT end your turn after calling this tool.",
240
+ "Ask all 1-4 user questions needed to continue in one call. Each question may offer 2-6 options, allow multiple selections, and/or allow a custom answer. Do not make one call per question or write plain text like 'please pick A / B / C'. The answers return as this tool's result, so continue the same turn after calling it.",
166
241
  parameters: askUserParameters,
167
242
  // 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
168
243
  // 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
@@ -174,18 +249,29 @@ export function basePiToolCandidates(
174
249
  }),
175
250
  interaction: {
176
251
  validateResponse: isAskUserResponse,
177
- settle: (_input, response) => {
178
- const answer = response as Static<typeof askUserResponse>;
179
- const parts = [
180
- ...answer.selections,
181
- ...(answer.text?.trim() ? [answer.text.trim()] : []),
182
- ];
252
+ settle: (input, response) => {
253
+ const questions = (input as AskUserInput).questions;
254
+ const responseAnswers = (response as AskUserResponse).answers;
255
+ const answers = questions.map((question, index) => {
256
+ const answer = responseAnswers[index]!;
257
+ return {
258
+ question: question.question,
259
+ selections: answer.selections,
260
+ ...(answer.text !== undefined ? { text: answer.text.trim() } : {}),
261
+ };
262
+ });
183
263
  return {
184
264
  content: [{
185
265
  type: "text",
186
- text: `The user answered: ${parts.join(" / ")}`,
266
+ text: `The user answered:\n${answers.map((answer, index) => {
267
+ const parts = [
268
+ ...answer.selections,
269
+ ...(answer.text ? [answer.text] : []),
270
+ ];
271
+ return `${index + 1}. ${answer.question}\n ${parts.join(" / ")}`;
272
+ }).join("\n")}`,
187
273
  }],
188
- details: answer,
274
+ details: { answers },
189
275
  };
190
276
  },
191
277
  },
@@ -4,7 +4,16 @@ import type {
4
4
  AgentToolResult,
5
5
  } from "@earendil-works/pi-agent-core";
6
6
  import type { ApprovalReceipt } from "../../kernel/receipts";
7
- import { boundDurableToolOutput } from "../../layers/context/budget/gate";
7
+ import {
8
+ boundDurableToolOutput,
9
+ type BudgetPolicy,
10
+ spillDurableToolOutput,
11
+ } from "../../layers/context/budget/gate";
12
+ import {
13
+ type ArtifactRef,
14
+ serializeOutput,
15
+ type SpillWorkspace,
16
+ } from "../../lib/artifacts";
8
17
  import {
9
18
  type ExecutionLevel,
10
19
  } from "../../lib/execution-level";
@@ -24,8 +33,8 @@ import {
24
33
  * 会让下一次模型调用非法。
25
34
  */
26
35
  export interface PiToolInteractionSpec {
27
- /** 校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
28
- readonly validateResponse: (response: unknown) => boolean;
36
+ /** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
37
+ readonly validateResponse: (input: unknown, response: unknown) => boolean;
29
38
  /** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
30
39
  readonly settle?: (
31
40
  input: unknown,
@@ -54,11 +63,14 @@ export interface PiToolCandidate {
54
63
  readonly summary?: string;
55
64
  readonly source?: ApprovalReceipt["source"];
56
65
  readonly interaction?: PiToolInteractionSpec;
66
+ /** Large payloads spill by default; protocol-shaped results opt into structure. */
67
+ readonly outputBudget?: BudgetPolicy;
57
68
  }
58
69
 
59
70
  /** 控制候选 Pi 工具如何被编译为可执行工具集。 */
60
71
  export interface CompilePiToolsOptions {
61
72
  readonly governance?: PiToolGovernance;
73
+ readonly workspace?: SpillWorkspace;
62
74
  readonly settle: (
63
75
  call: SettledPiToolCall,
64
76
  ) => void | Promise<void>;
@@ -200,6 +212,52 @@ function emitToolTelemetry(
200
212
  }
201
213
  }
202
214
 
215
+ // Most Pi adapters keep the original Tool value in details and its serialized
216
+ // model form in one text block. Spill that original once instead of duplicating
217
+ // it inside an AgentToolResult JSON envelope.
218
+ function spillPayload(result: AgentToolResult<unknown>): unknown {
219
+ const text = result.content.length === 1 && result.content[0]?.type === "text"
220
+ ? result.content[0].text
221
+ : undefined;
222
+ if (text !== undefined && serializeOutput(result.details).text === text) {
223
+ return result.details;
224
+ }
225
+ return result;
226
+ }
227
+
228
+ function artifactResult(
229
+ result: AgentToolResult<unknown>,
230
+ ref: ArtifactRef,
231
+ ): AgentToolResult<unknown> {
232
+ return {
233
+ ...result,
234
+ content: [{
235
+ type: "text",
236
+ text: [
237
+ ref.note,
238
+ `Path: ${ref.path}`,
239
+ `Bytes: ${ref.bytes}`,
240
+ `Preview:\n${ref.preview}`,
241
+ ].join("\n"),
242
+ }],
243
+ details: ref,
244
+ };
245
+ }
246
+
247
+ async function durableResult(
248
+ result: AgentToolResult<unknown>,
249
+ candidate: PiToolCandidate,
250
+ workspace: SpillWorkspace | undefined,
251
+ ): Promise<AgentToolResult<unknown>> {
252
+ if ((candidate.outputBudget ?? { kind: "spill" }).kind === "spill") {
253
+ const ref = await spillDurableToolOutput(spillPayload(result), {
254
+ workspace,
255
+ });
256
+ if (ref) return artifactResult(result, ref);
257
+ }
258
+ return boundDurableToolOutput(result) as AgentToolResult<unknown>;
259
+ }
260
+
203
261
  // 记录一次工具失败并在 Turn 达到上限时打开断路器。
204
262
  // 受治理 execute 方法在工具或结算失败后调用。
205
263
  // 同时记录单输入次数和 Turn 总数,是为了既阻止原样重试又防止换参数无限失败。
@@ -341,9 +399,11 @@ function governedTool(
341
399
 
342
400
  let result: AgentToolResult<unknown>;
343
401
  try {
344
- result = boundDurableToolOutput(
402
+ result = await durableResult(
345
403
  await execute(toolCallId, args, signal, onUpdate),
346
- ) as AgentToolResult<unknown>;
404
+ candidate,
405
+ options.workspace,
406
+ );
347
407
  } catch (cause) {
348
408
  const message = errorMessage(cause);
349
409
  const result = boundDurableToolOutput({
@@ -442,7 +502,7 @@ export function compilePiTools(
442
502
  options: CompilePiToolsOptions,
443
503
  ): AgentTool<any, any>[] {
444
504
  if (!options?.settle) {
445
- throw new Error("Pi Tool settlement port is required");
505
+ throw new Error("SpringBrand Tool settlement port is required");
446
506
  }
447
507
  return candidates.map((candidate) => governedTool(candidate, options));
448
508
  }
@@ -3,10 +3,14 @@ import {
3
3
  type WorkspaceFsLike,
4
4
  } from "@cloudflare/shell";
5
5
  import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
6
+ import { jsonSchema, tool as aiTool, type ToolSet } from "ai";
6
7
  import type {
7
- RuntimeCodeExecutionPort,
8
8
  WorkspacePort,
9
9
  } from "../../kernel/bindings";
10
+ import type {
11
+ RuntimeCodeExecutionFactory,
12
+ ToolRegistry,
13
+ } from "../../tool-registry";
10
14
 
11
15
  // 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
12
16
 
@@ -19,32 +23,55 @@ const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
19
23
  *
20
24
  * Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
21
25
  */
22
- export function createWorkspaceCodeExecutionPort(options: {
26
+ function codeExecutionTools(tools: ToolRegistry): ToolSet {
27
+ return Object.fromEntries(Object.entries(tools).map(([name, spec]) => [
28
+ name,
29
+ aiTool({
30
+ description: spec.description,
31
+ inputSchema: jsonSchema(spec.parameters as never),
32
+ execute: (input, options) => spec.execute(input, {
33
+ toolCallId: options?.toolCallId ?? name,
34
+ signal: options?.abortSignal ?? new AbortController().signal,
35
+ }),
36
+ }),
37
+ ]));
38
+ }
39
+
40
+ export function createWorkspaceCodeExecutionFactory(options: {
23
41
  readonly ctx: DurableObjectState;
24
42
  readonly loader: WorkerLoader;
25
43
  readonly outbound: Fetcher;
26
44
  readonly workspace: WorkspacePort;
27
- }): RuntimeCodeExecutionPort {
28
- const { tool } = createExecuteRuntime({
29
- ctx: options.ctx,
30
- loader: options.loader,
31
- globalOutbound: options.outbound,
32
- // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
33
- timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
34
- state: createWorkspaceStateBackend(
35
- options.workspace as unknown as WorkspaceFsLike,
36
- ),
37
- name: "execute",
38
- });
39
- const execute = tool.execute;
40
- if (typeof execute !== "function") {
41
- throw new Error("Think createExecuteRuntime returned a non-executable tool");
42
- }
45
+ }): RuntimeCodeExecutionFactory {
43
46
  return {
44
- execute: (input) => Promise.resolve(execute(input, {
45
- toolCallId: "execute",
46
- messages: [],
47
- context: undefined,
48
- })),
47
+ create(tools) {
48
+ const { tool } = createExecuteRuntime({
49
+ ctx: options.ctx,
50
+ loader: options.loader,
51
+ globalOutbound: options.outbound,
52
+ tools: codeExecutionTools(tools),
53
+ // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
54
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
55
+ state: createWorkspaceStateBackend(
56
+ options.workspace as unknown as WorkspaceFsLike,
57
+ ),
58
+ name: "execute",
59
+ });
60
+ const { description, execute } = tool;
61
+ if (typeof description !== "string" || description.length === 0) {
62
+ throw new Error("Think createExecuteRuntime returned a tool without a description");
63
+ }
64
+ if (typeof execute !== "function") {
65
+ throw new Error("Think createExecuteRuntime returned a non-executable tool");
66
+ }
67
+ return {
68
+ description,
69
+ execute: (input) => Promise.resolve(execute(input, {
70
+ toolCallId: "execute",
71
+ messages: [],
72
+ context: undefined,
73
+ })),
74
+ };
75
+ },
49
76
  };
50
77
  }
@@ -12,10 +12,6 @@ import { serializeOutput } from "../../lib/artifacts";
12
12
  import type { PiLoadedExtension } from "../assembly/extensions";
13
13
  import { aiToolToPi } from "./ai-adapter";
14
14
  import type { PiToolCandidate } from "./compiler";
15
- import {
16
- toolRegistryFromPiCandidates,
17
- type ToolRegistry,
18
- } from "../../tool-registry";
19
15
 
20
16
  // 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
21
17
 
@@ -125,16 +121,7 @@ export function codeExecutionPiToolCandidate(
125
121
  const tool: AgentTool<typeof executeParameters> = {
126
122
  name: "execute",
127
123
  label: "Execute JavaScript",
128
- description:
129
- "Run JavaScript in a short-lived Code Mode Dynamic Worker and return a value. This is your " +
130
- "raw-network and tool-composition instrument, not a general web-search tool. Call fetch directly " +
131
- "when you need a raw response, custom headers/method/body, a structured API, or content that " +
132
- "web_search could not retrieve. Check res.status; when several known endpoints are necessary, " +
133
- "fetch them in one invocation. Also available: " +
134
- "state.* for your workspace filesystem (readFile/writeFile/glob/searchFiles/replaceInFiles, " +
135
- "each taking one object argument), and codemode.step(name, fn) to run a side-effecting block " +
136
- "exactly once so it survives replay. Write plain JavaScript — TypeScript type annotations are " +
137
- "a syntax error. Use return for the result and console.log for notes.",
124
+ description: runtime.description,
138
125
  parameters: executeParameters,
139
126
  // 把模型提供的 JavaScript 交给 Codemode Runtime 执行。
140
127
  // Pi 工具循环在模型选择 `execute` 时调用,调用前允许 Turn 取消。
@@ -173,19 +160,11 @@ export function codeExecutionPiToolCandidate(
173
160
  return {
174
161
  owner: "core:codemode",
175
162
  requiredExecutionLevel: "high",
163
+ outputBudget: { kind: "structure" },
176
164
  source: "codemode",
177
165
  summary: "Run JavaScript with network and configured connector access",
178
166
  tool,
179
167
  };
180
168
  }
181
169
 
182
- /** 从 Codemode Runtime Port 生成 `execute` Tool。 */
183
- export function createCodeExecutionTool(
184
- runtime: RuntimeCodeExecutionPort,
185
- ): ToolRegistry {
186
- return toolRegistryFromPiCandidates([
187
- codeExecutionPiToolCandidate(runtime),
188
- ]);
189
- }
190
-
191
170
  // #endregion
@@ -30,4 +30,5 @@ export * from "./schedule";
30
30
  export * from "./skill";
31
31
  export * from "./subagent";
32
32
  export * from "./workspace-sandbox";
33
+ export * from "./workspace-revision";
33
34
  export * from "./web-search";
@@ -59,7 +59,8 @@ export interface PiMcpHost {
59
59
  * 参数和中止信号必须原样交给 Agents SDK,避免另造一套传输生命周期。
60
60
  */
61
61
  callTool(
62
- ...args: Parameters<MCPClientManager["callTool"]>
62
+ params: Parameters<MCPClientManager["callTool"]>[0],
63
+ options?: { signal?: AbortSignal },
63
64
  ): Promise<unknown>;
64
65
  };
65
66
  }
@@ -333,7 +334,6 @@ export function createPiMcpToolCandidates(
333
334
  name: mcpTool.name,
334
335
  arguments: args,
335
336
  },
336
- undefined,
337
337
  { signal },
338
338
  ),
339
339
  {