@springbrand/agent-runtime 0.3.0-alpha.1 → 0.3.0-alpha.11

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 (111) hide show
  1. package/package.json +8 -8
  2. package/src/adapter/cloudflare/agent.ts +160 -14
  3. package/src/adapter/cloudflare/define-agent.ts +12 -7
  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 +15 -0
  8. package/src/adapter/cloudflare/subagent/definition.ts +4 -4
  9. package/src/adapter/cloudflare/subagent/runner.ts +1 -1
  10. package/src/adapter/cloudflare/subagent/tools.ts +2 -2
  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/agent-context.ts +1 -1
  15. package/src/config/assembler.ts +29 -31
  16. package/src/config/assembly-snapshot.ts +1 -1
  17. package/src/config/definition.ts +1 -1
  18. package/src/config/harness-runtime.ts +9 -1
  19. package/src/config/model-guidance.ts +21 -0
  20. package/src/config/pinning.ts +22 -3
  21. package/src/config/ports.ts +27 -7
  22. package/src/config/public-contracts.ts +2 -2
  23. package/src/context/assembly.ts +1 -1
  24. package/src/context/extension/assembly.ts +2 -2
  25. package/src/context/memory/budget/gate.ts +19 -3
  26. package/src/conversation/harness-stream.ts +2 -1
  27. package/src/conversation/history.ts +2 -0
  28. package/src/conversation/message/projection.ts +38 -21
  29. package/src/conversation/state.ts +23 -2
  30. package/src/harness/session-harness.ts +160 -6
  31. package/src/index.ts +8 -7
  32. package/src/lib/artifacts.ts +18 -0
  33. package/src/lib/execution-level.ts +13 -0
  34. package/src/lib/prompt.ts +9 -10
  35. package/src/lib/token-estimate.ts +8 -0
  36. package/src/models/index.ts +32 -10
  37. package/src/models/openrouter/messages.ts +13 -8
  38. package/src/models/provider-streams.ts +45 -0
  39. package/src/session/cloudflare/migration.ts +71 -11
  40. package/src/skills/springbrand-worker-website/index.ts +1 -1
  41. package/src/submission/admission.ts +12 -1
  42. package/src/submission/contracts.ts +42 -2
  43. package/src/submission/delivery.ts +1 -1
  44. package/src/submission/finalization.ts +1 -1
  45. package/src/submission/repository.ts +64 -4
  46. package/src/submission/unattended.ts +40 -0
  47. package/src/telemetry/coordinator.ts +1 -1
  48. package/src/telemetry/subagent-lifecycle.ts +1 -1
  49. package/src/telemetry/turn-events.ts +1 -1
  50. package/src/tools/README.md +44 -0
  51. package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
  52. package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
  53. package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
  54. package/src/tools/assistant/ask-user.ts +317 -0
  55. package/src/tools/assistant/candidate.ts +15 -0
  56. package/src/tools/assistant/followups.ts +34 -0
  57. package/src/tools/assistant/index.ts +19 -0
  58. package/src/tools/assistant/memory.ts +73 -0
  59. package/src/tools/assistant/plan.ts +98 -0
  60. package/src/tools/{time.ts → assistant/time.ts} +1 -1
  61. package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
  62. package/src/tools/{core.ts → codemode/index.ts} +36 -92
  63. package/src/tools/codemode/workspace-connector.ts +198 -0
  64. package/src/tools/extension/index.ts +42 -0
  65. package/src/tools/index.ts +29 -26
  66. package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
  67. package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
  68. package/src/tools/runtime/activation.ts +40 -0
  69. package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
  70. package/src/tools/{compiler.ts → runtime/compiler.ts} +84 -147
  71. package/src/tools/runtime/contracts.ts +140 -0
  72. package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +147 -37
  73. package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
  74. package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
  75. package/src/tools/runtime/result.ts +22 -0
  76. package/src/tools/runtime/unattended-error.ts +4 -0
  77. package/src/tools/sandbox/index.ts +228 -0
  78. package/src/tools/schedule/index.ts +301 -0
  79. package/src/tools/skill/contracts.ts +20 -0
  80. package/src/tools/skill/index.ts +99 -0
  81. package/src/tools/skill/resources.ts +322 -0
  82. package/src/tools/skill/scripts.ts +168 -0
  83. package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
  84. package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
  85. package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
  86. package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
  87. package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
  88. package/src/tools/{temporary-agent → subagent/temporary}/core.ts +1 -1
  89. package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
  90. package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
  91. package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
  92. package/src/tools/web/index.ts +2 -0
  93. package/src/tools/web/search/api.ts +1320 -0
  94. package/src/tools/web/search/index.ts +2 -0
  95. package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
  96. package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
  97. package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
  98. package/src/tools/base.ts +0 -534
  99. package/src/tools/schedule.ts +0 -261
  100. package/src/tools/skill.ts +0 -565
  101. package/src/tools/web-search/api.ts +0 -1318
  102. package/src/tools/web-search/index.ts +0 -2
  103. package/src/tools/workspace-state-connector.ts +0 -146
  104. package/src/tools/{approval → runtime/approval}/index.ts +0 -0
  105. package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
  106. package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
  107. package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
  108. package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
  109. package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
  110. package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
  111. /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
@@ -1,38 +1,31 @@
1
+ import { truncateResponse, truncateResult, type ProxyToolOutput } from "@cloudflare/codemode";
2
+ import { generateTypes } from "@cloudflare/codemode/ai";
1
3
  import {
2
- createWorkspaceStateBackend,
3
4
  STATE_TYPES,
5
+ createWorkspaceStateBackend,
4
6
  type StateBackend,
5
7
  type WorkspaceFsLike,
6
8
  } from "@cloudflare/shell";
7
- import {
8
- truncateResponse,
9
- truncateResult,
10
- type ProxyToolOutput,
11
- } from "@cloudflare/codemode";
12
9
  import { createBrowserTools } from "@cloudflare/think/tools/browser";
13
- import {
14
- createExecuteRuntime,
15
- type CreateExecuteToolOptions,
16
- } from "@cloudflare/think/tools/execute";
10
+ import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
17
11
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
18
12
  import type { Usage } from "@earendil-works/pi-ai";
19
13
  import type { ToolSet } from "ai";
20
14
  import type {
21
15
  RuntimeBrowserPort,
16
+ RuntimeCodeExecutionFactory,
22
17
  RuntimeCodeExecutionPort,
23
18
  WorkspacePort,
24
- } from "../config/ports";
19
+ } from "../../config/ports";
20
+ import { serializeOutput } from "../../lib/artifacts";
21
+ import { piCandidatesToAiTools } from "../adapters/pi-to-ai";
25
22
  import {
26
- WorkspaceStateConnector,
27
23
  WORKSPACE_PAGING_INSTRUCTIONS,
28
24
  WORKSPACE_STATE_TYPES,
29
- } from "./workspace-state-connector";
30
- import type { RuntimeCodeExecutionFactory } from "../config/ports";
31
- import { serializeOutput } from "../lib/artifacts";
32
- import type { PiToolCandidate } from "./compiler";
33
- import { piCandidatesToAiTools } from "./nested-tools";
25
+ WorkspaceStateConnector,
26
+ } from "./workspace-connector";
34
27
 
35
- // 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
28
+ // 将 Cloudflare 的 Code Mode 与浏览器执行能力适配为 Runtime Port;Pi Tool 定义位于同目录 index.ts。
36
29
 
37
30
  const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
38
31
  export type WorkspaceAccessMode = "none" | "read" | "write";
@@ -103,7 +96,7 @@ When an asset already exists in Workspace, use state.cp to copy it into the curr
103
96
 
104
97
  Never create or write data URLs, base64 image strings, or embedded binary payloads in Code Mode.
105
98
 
106
- Copy a remote asset into Workspace with tools.materialize_url({ url, destination }). Raw fetch remains available. Do not carry large responses, binary payloads, data URLs, or base64 assets through Code Mode.
99
+ Copy a remote asset into Workspace with tools.materialize_url({ url, destination }). Do not carry large responses, binary payloads, data URLs, or base64 assets through Code Mode.
107
100
 
108
101
  After materialization, reference the Workspace file by a relative path. Website HTML and CSS must reference relative Workspace assets, never temporary external URLs or data URLs.
109
102
 
@@ -171,13 +164,9 @@ function codeExecutionResult(
171
164
  details: unknown,
172
165
  innerResults: readonly AgentToolResult<unknown>[],
173
166
  ): AgentToolResult<unknown> {
174
- const addedToolNames = [...new Set(
175
- innerResults.flatMap(({ addedToolNames }) => addedToolNames ?? []),
176
- )].sort();
177
167
  const usage = sumUsage(innerResults);
178
168
  return {
179
169
  ...result(details),
180
- ...(addedToolNames.length > 0 ? { addedToolNames } : {}),
181
170
  ...(usage ? { usage } : {}),
182
171
  ...(innerResults.length > 0 && innerResults.every(({ terminate }) => terminate === true)
183
172
  ? { terminate: true }
@@ -193,12 +182,12 @@ function projectCodeModeResult(details: unknown): unknown {
193
182
  const calls = Array.isArray(output.calls) ? output.calls : [];
194
183
  const callSummary = calls.length > 0
195
184
  ? {
196
- total: calls.length,
197
- failed: calls.filter(({ state }) => state === "error").length,
198
- methods: [...new Set(calls.map(({ method }) => method))]
199
- .map((method) => truncateResponse(method, { maxChars: 256 }))
200
- .slice(0, 100),
201
- }
185
+ total: calls.length,
186
+ failed: calls.filter(({ state }) => state === "error").length,
187
+ methods: [...new Set(calls.map(({ method }) => method))]
188
+ .map((method) => truncateResponse(method, { maxChars: 256 }))
189
+ .slice(0, 100),
190
+ }
202
191
  : undefined;
203
192
  const common = {
204
193
  status: output.status,
@@ -224,7 +213,7 @@ function projectCodeModeResult(details: unknown): unknown {
224
213
  // 上游把 Code Mode 类工具交付为 AI SDK tool;本仓只取 description 和 execute 两件,
225
214
  // 并在这里就地校验,使装配期缺件立刻失败,而不是等模型调用时才炸。
226
215
  function toCodeExecutionPort(
227
- tool: ToolSet[string],
216
+ tool: Pick<ToolSet[string], "description" | "execute">,
228
217
  name: string,
229
218
  project: (details: unknown) => AgentToolResult<unknown> = result,
230
219
  ): RuntimeCodeExecutionPort {
@@ -345,35 +334,40 @@ export function createWorkspaceCodeExecutionFactory(options: {
345
334
  options.workspace,
346
335
  options.workspaceAccessMode,
347
336
  );
348
- const workspaceState = state && options.workspace.readFilePage
337
+ const workspaceState = state
349
338
  ? new WorkspaceStateConnector(
350
- options.ctx,
351
- state,
352
- options.workspace as WorkspacePort & {
353
- readFilePage: NonNullable<WorkspacePort["readFilePage"]>;
354
- },
355
- )
339
+ options.ctx,
340
+ state,
341
+ options.workspace,
342
+ )
356
343
  : undefined;
357
344
  const createPort = (
358
345
  innerResults: AgentToolResult<unknown>[],
359
- stateOptions: Pick<CreateExecuteToolOptions, "state" | "connectors">,
360
346
  ) => {
361
347
  let nestedCallOrdinal = 0;
362
- const { tool } = createExecuteRuntime({
348
+ const tools = piCandidatesToAiTools(candidates, {
349
+ fallbackToolCallId: (name) =>
350
+ `codemode:${++nestedCallOrdinal}:${name}`,
351
+ onResult: (value) => innerResults.push(value),
352
+ });
353
+ const { tool, runtime } = createExecuteRuntime({
363
354
  ctx: options.ctx,
364
355
  loader: options.loader,
365
356
  globalOutbound: options.outbound,
366
- tools: piCandidatesToAiTools(candidates, {
367
- fallbackToolCallId: (name) =>
368
- `codemode:${++nestedCallOrdinal}:${name}`,
369
- onResult: (value) => innerResults.push(value),
370
- }),
357
+ tools,
371
358
  timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
372
- ...stateOptions,
359
+ connectors: workspaceState ? [workspaceState] : [],
373
360
  name: "execute",
374
361
  });
362
+ // Generate the official description from the same connectors used to execute.
363
+ const description = runtime.tool({
364
+ connectorHints: {
365
+ tools: "host tools as async functions; call tools.<method>({ ... }) inside execute. Discover additional methods with codemode.search and codemode.describe.",
366
+ state: "the workspace filesystem. Every method takes ONE object argument: state.readFile({ path }), state.writeFile({ path, content }), state.readdir({ path }).",
367
+ }
368
+ }).description;
375
369
  return toCodeExecutionPort(
376
- tool,
370
+ { ...tool, description },
377
371
  "execute",
378
372
  (details) => codeExecutionResult(
379
373
  projectCodeModeResult(details),
@@ -381,21 +375,24 @@ export function createWorkspaceCodeExecutionFactory(options: {
381
375
  ),
382
376
  );
383
377
  };
384
- // Inherit the complete official state description; custom connectors otherwise lose its hints.
385
- const description = createPort([], state ? { state } : {}).description;
378
+ // External declarations opt out of constrained sampling and remain discoverable on demand.
379
+ const inlineCandidates = candidates.filter(({ tool }) => tool.constrainedSampling !== false);
380
+ const toolTypes = inlineCandidates.length > 0
381
+ ? generateTypes(piCandidatesToAiTools(inlineCandidates, {}), "tools")
382
+ : undefined;
383
+ const description = createPort([]).description;
386
384
  return {
387
385
  description: [
388
386
  description,
387
+ ...(toolTypes ? ["## Code Mode Tool types\nUse these signatures inside execute. The Tool routing list defines which methods also have a top-level entry.\n\n```ts\n" + toolTypes + "\n```"] : []),
389
388
  ...(state ? [
390
389
  "## Workspace type reference\nOnly methods exposed by connector discovery are callable.\n\n```ts\n" +
391
- (workspaceState ? WORKSPACE_STATE_TYPES : STATE_TYPES) + "\n```",
390
+ (options.workspace.readFilePage ? WORKSPACE_STATE_TYPES : STATE_TYPES) + "\n```",
392
391
  ] : []),
393
- ...(workspaceState ? [WORKSPACE_PAGING_INSTRUCTIONS] : []),
392
+ ...(state && options.workspace.readFilePage ? [WORKSPACE_PAGING_INSTRUCTIONS] : []),
394
393
  SPRINGBRAND_CODE_MODE_POLICY,
395
394
  ].join("\n\n"),
396
- execute: (input) => createPort(
397
- [], workspaceState ? { connectors: [workspaceState] } : state ? { state } : {},
398
- ).execute(input),
395
+ execute: (input) => createPort([]).execute(input),
399
396
  };
400
397
  },
401
398
  };
@@ -1,29 +1,14 @@
1
- import type {
2
- AgentTool,
3
- AgentToolResult,
4
- } from "@earendil-works/pi-agent-core";
1
+ import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
5
2
  import { Type } from "@earendil-works/pi-ai";
6
- import type {
7
- RuntimeCodeExecutionPort,
8
- } from "../config/ports";
9
- import { serializeOutput } from "../lib/artifacts";
10
- import type { PiLoadedExtension } from "../context/extension/assembly";
11
- import type { PiToolCandidate } from "./compiler";
12
-
13
- // 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
14
-
15
- // 把核心工具的结构化值投影为 Pi 同时需要的文本和 details。
16
- // 本文件中所有核心工具 execute 方法在返回宿主结果时调用它。
17
- // 统一使用 `serializeOutput()` 可以保持模型可见文本和持久化 details 来自同一个值。
18
- function result(details: unknown): AgentToolResult<unknown> {
19
- return {
20
- content: [{ type: "text", text: serializeOutput(details).text }],
21
- details,
22
- };
23
- }
3
+ import type { RuntimeCodeExecutionPort } from "../../config/ports";
4
+ import { serializeOutput } from "../../lib/artifacts";
5
+ import type { PiToolCandidate } from "../runtime/contracts";
6
+ import { result } from "../runtime/result";
24
7
 
25
8
  const BROWSER_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
9
+
26
10
  const BROWSER_IMAGE_MAX_BASE64_CHARS = Math.ceil(BROWSER_IMAGE_MAX_BYTES / 3) * 4;
11
+
27
12
  const BROWSER_IMAGE_MIME_TYPES = new Set([
28
13
  "image/jpeg",
29
14
  "image/png",
@@ -87,7 +72,7 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
87
72
 
88
73
  const calls = Array.isArray(outer.calls) ? outer.calls : [];
89
74
  let screenshotCall: Record<string, unknown> | undefined;
90
- for (let index = calls.length - 1; index >= 0; index -= 1) {
75
+ for (let index = calls.length - 1;index >= 0;index -= 1) {
91
76
  const call = record(calls[index]);
92
77
  const args = record(call?.args);
93
78
  if (
@@ -117,39 +102,39 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
117
102
 
118
103
  const projectedCalls = Array.isArray(outer.calls)
119
104
  ? outer.calls.map((call) => {
120
- const entry = record(call);
121
- const args = record(entry?.args);
122
- const callResult = record(entry?.result);
123
- if (
124
- entry?.connector !== "cdp" ||
125
- entry.method !== "send" ||
126
- args?.method !== "Page.captureScreenshot" ||
127
- typeof callResult?.data !== "string"
128
- ) return call;
129
- const callFormat = record(args.params)?.format;
130
- return {
131
- ...entry,
132
- result: {
133
- ...callResult,
134
- data: browserImageMetadata(
135
- callResult.data,
136
- screenshotMimeType(callFormat),
137
- ),
138
- },
139
- };
140
- })
105
+ const entry = record(call);
106
+ const args = record(entry?.args);
107
+ const callResult = record(entry?.result);
108
+ if (
109
+ entry?.connector !== "cdp" ||
110
+ entry.method !== "send" ||
111
+ args?.method !== "Page.captureScreenshot" ||
112
+ typeof callResult?.data !== "string"
113
+ ) return call;
114
+ const callFormat = record(args.params)?.format;
115
+ return {
116
+ ...entry,
117
+ result: {
118
+ ...callResult,
119
+ data: browserImageMetadata(
120
+ callResult.data,
121
+ screenshotMimeType(callFormat),
122
+ ),
123
+ },
124
+ };
125
+ })
141
126
  : outer.calls;
142
127
  const projectedDetails = {
143
128
  ...outer,
144
129
  result: inner
145
130
  ? {
146
- ...inner,
147
- image: projectedImage,
148
- }
131
+ ...inner,
132
+ image: projectedImage,
133
+ }
149
134
  : {
150
- image: projectedImage,
151
- note: "Screenshot recovered from the Page.captureScreenshot result.",
152
- },
135
+ image: projectedImage,
136
+ note: "Screenshot recovered from the Page.captureScreenshot result.",
137
+ },
153
138
  ...("calls" in outer ? { calls: projectedCalls } : {}),
154
139
  };
155
140
  return {
@@ -163,47 +148,7 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
163
148
  };
164
149
  }
165
150
 
166
- // #region Extension discovery
167
-
168
- const noParameters = Type.Object({}, { additionalProperties: false });
169
-
170
- /**
171
- * 为当前已加载 Extension 列表创建一个 Pi 工具候选项。
172
- *
173
- * Runtime 组装层在 Extension 装载完成后调用,模型需要发现当前可用扩展时使用该工具。
174
- *
175
- * 候选项捕获准备阶段的只读列表,这与 pinned Runtime 组装保持一致,不能在执行时重新扫描出另一份 Extension 集合。
176
- */
177
- export function listExtensionsPiToolCandidate(
178
- extensions: readonly PiLoadedExtension[],
179
- ): PiToolCandidate {
180
- const tool: AgentTool<typeof noParameters> = {
181
- name: "list_extensions",
182
- label: "List extensions",
183
- description: "List all currently loaded extensions and their tools.",
184
- parameters: noParameters,
185
- // 返回这个 pinned Runtime 已加载的 Extension 快照。
186
- // Pi 工具循环在模型选择 `list_extensions` 时调用,调用前允许 Turn 取消。
187
- // 同时返回 count 和原列表,使模型既能快速判空又不丢失 Extension 细节。
188
- async execute(_toolCallId, _input, signal) {
189
- signal?.throwIfAborted();
190
- return result({
191
- count: extensions.length,
192
- extensions,
193
- });
194
- },
195
- };
196
- return {
197
- owner: "core:extensions",
198
- requiredExecutionLevel: "low",
199
- retry: "idempotent",
200
- tool,
201
- };
202
- }
203
-
204
- // #endregion
205
151
 
206
- // #region Code Mode
207
152
 
208
153
  const executeParameters = Type.Object({
209
154
  code: Type.String({
@@ -213,6 +158,7 @@ const executeParameters = Type.Object({
213
158
  "Plain JavaScript async function. TypeScript annotations are not supported.",
214
159
  }),
215
160
  }, { additionalProperties: false });
161
+
216
162
  const CODEMODE_EXECUTE_TIMEOUT_MS = 60_000;
217
163
 
218
164
  // 把模型提供的代码交给 Codemode Runtime 执行,并在外层再压一道截止。
@@ -331,5 +277,3 @@ export function browserExecutionPiToolCandidate(
331
277
  tool,
332
278
  };
333
279
  }
334
-
335
- // #endregion
@@ -0,0 +1,198 @@
1
+ import type { ConnectorTools } from "@cloudflare/codemode";
2
+ import { STATE_TYPES } from "@cloudflare/shell";
3
+ import type { StateBackend } from "@cloudflare/shell/workers";
4
+ import { StateConnector } from "@cloudflare/shell/workers";
5
+ import type { WorkspacePort } from "../../config/ports";
6
+
7
+ export const WORKSPACE_DIRECT_TEXT_READ_MAX_BYTES = 1024 * 1024;
8
+
9
+ const READ_FILE_PAGE_TYPES = `
10
+ type WorkspaceFilePage =
11
+ | { kind: "text"; size: number; nextOffset: number | null; content: string }
12
+ | { kind: "csv"; size: number; nextOffset: number | null; columns: string[] | null; rows: string[][] };
13
+ `;
14
+
15
+ export const WORKSPACE_STATE_TYPES = `${READ_FILE_PAGE_TYPES}\n${STATE_TYPES.replace(
16
+ /\n\};$/u,
17
+ '\n /** CSV pages have columns and rows; never data. Omit offset first, then pass nextOffset until null. */\n readFilePage(args: { path: string; offset?: number }): Promise<WorkspaceFilePage | null>;\n};',
18
+ )}`;
19
+
20
+ export const WORKSPACE_PAGING_INSTRUCTIONS = `For CSV files, do not make a separate probe read or return page contents. Inspect columns and aggregate rows in the same execution with this loop shape:
21
+
22
+ let page = await state.readFilePage({ path });
23
+ if (!page || page.kind !== "csv") throw new Error("CSV not found");
24
+ const columns = page.columns;
25
+ for (;;) {
26
+ // Aggregate page.rows here. Do not return raw rows.
27
+ if (page.nextOffset === null) break;
28
+ page = await state.readFilePage({ path, offset: page.nextOffset });
29
+ }
30
+ return summary;
31
+
32
+ CSV pages use page.columns and page.rows; never page.data or page.content. Only the first page has columns. Do not use console.log to inspect a page; explicitly return the final summary.`;
33
+
34
+ export class WorkspaceStateConnector extends StateConnector {
35
+ constructor(
36
+ ctx: DurableObjectState | ExecutionContext,
37
+ backend: StateBackend,
38
+ private readonly workspace: WorkspacePort,
39
+ ) {
40
+ super(ctx, backend);
41
+ }
42
+
43
+ protected override instructions(): string {
44
+ const base = super.instructions();
45
+ return this.workspace.readFilePage ? `${base}
46
+
47
+ ${WORKSPACE_PAGING_INSTRUCTIONS}` : base;
48
+ }
49
+
50
+ protected override tools(): ConnectorTools {
51
+ const tools = super.tools();
52
+ const readFile = tools.readFile!;
53
+ const mkdir = tools.mkdir!;
54
+ const readFilePage = this.workspace.readFilePage;
55
+ return {
56
+ ...tools,
57
+ mkdir: {
58
+ ...mkdir,
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ path: { type: "string", minLength: 1 },
63
+ options: {
64
+ type: "object",
65
+ properties: { recursive: { type: "boolean" } },
66
+ additionalProperties: false,
67
+ },
68
+ },
69
+ required: ["path"],
70
+ additionalProperties: false,
71
+ },
72
+ execute: (args, context) => mkdir.execute(mkdirInput(args), context),
73
+ },
74
+ ...(readFilePage
75
+ ? {
76
+ readFile: {
77
+ ...readFile,
78
+ description:
79
+ `${readFile.description} CSV and files over ${WORKSPACE_DIRECT_TEXT_READ_MAX_BYTES} bytes must use readFilePage.`,
80
+ execute: async (args, context) => {
81
+ const path = requiredPath(args, "readFile");
82
+ const file = await this.workspace.stat(path);
83
+ if (file?.type === "file" && (
84
+ file.mimeType === "text/csv" ||
85
+ file.size > WORKSPACE_DIRECT_TEXT_READ_MAX_BYTES
86
+ )) {
87
+ throw new Error(
88
+ `${path} requires state.readFilePage({ path: ${JSON.stringify(path)} }); continue with nextOffset until it is null`,
89
+ );
90
+ }
91
+ return readFile.execute(args, context);
92
+ },
93
+ },
94
+ readFilePage: {
95
+ description:
96
+ "Read up to 512 KiB or 2,000 CSV rows from a Workspace file. " +
97
+ "A CSV page returns columns and rows and has no data field; rows are string arrays aligned to columns. " +
98
+ "A text page returns content. Omit offset on the first call, then pass page.nextOffset until it is null. " +
99
+ "Prefer this over readFile for CSV and large files.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ path: { type: "string", minLength: 1, description: "Absolute Workspace file path." },
104
+ offset: {
105
+ type: "integer",
106
+ minimum: 0,
107
+ description: "Previous page.nextOffset. Omit on the first call.",
108
+ },
109
+ },
110
+ required: ["path"],
111
+ additionalProperties: false,
112
+ },
113
+ replay: "reexecute",
114
+ execute: (args: unknown) => {
115
+ const input = readFilePageInput(args);
116
+ return readFilePage.call(this.workspace, input.path, input.offset);
117
+ },
118
+ },
119
+ }
120
+ : {}),
121
+ };
122
+ }
123
+
124
+ override async getTypeScriptTypes(): Promise<string> {
125
+ return this.workspace.readFilePage ? WORKSPACE_STATE_TYPES : STATE_TYPES;
126
+ }
127
+ }
128
+
129
+ function mkdirInput(value: unknown): {
130
+ path: string;
131
+ options?: { recursive?: boolean };
132
+ } {
133
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
134
+ throw new Error("state.mkdir requires an object argument");
135
+ }
136
+ const input = value as Record<string, unknown>;
137
+ if (Object.keys(input).some((key) => !["path", "options"].includes(key))) {
138
+ throw new Error("state.mkdir accepts only path and options");
139
+ }
140
+ if (typeof input.path !== "string" || input.path.length === 0) {
141
+ throw new Error("state.mkdir requires path");
142
+ }
143
+ if (input.options === undefined) return { path: input.path };
144
+ if (!input.options || typeof input.options !== "object" || Array.isArray(input.options)) {
145
+ throw new Error("state.mkdir options must be an object");
146
+ }
147
+ const options = input.options as Record<string, unknown>;
148
+ if (Object.keys(options).some((key) => key !== "recursive")) {
149
+ throw new Error("state.mkdir options accepts only recursive");
150
+ }
151
+ if (options.recursive !== undefined && typeof options.recursive !== "boolean") {
152
+ throw new Error("state.mkdir options.recursive must be boolean");
153
+ }
154
+ return {
155
+ path: input.path,
156
+ options: options.recursive === undefined
157
+ ? {}
158
+ : { recursive: options.recursive },
159
+ };
160
+ }
161
+
162
+ function requiredPath(value: unknown, method: string): string {
163
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
164
+ typeof (value as { path?: unknown }).path !== "string" ||
165
+ !(value as { path: string }).path) {
166
+ throw new Error(
167
+ `state.${method} takes one object argument: state.${method}({ path: "/path/to/file" })`,
168
+ );
169
+ }
170
+ return (value as { path: string }).path;
171
+ }
172
+
173
+ function readFilePageInput(value: unknown): {
174
+ path: string;
175
+ offset?: number;
176
+ } {
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
178
+ throw new Error("state.readFilePage requires an object argument");
179
+ }
180
+ const input = value as Record<string, unknown>;
181
+ const keys = Object.keys(input);
182
+ if (keys.some((key) => !["path", "offset"].includes(key))) {
183
+ throw new Error("state.readFilePage accepts only path and offset");
184
+ }
185
+ if (typeof input.path !== "string" || input.path.length === 0) {
186
+ throw new Error("state.readFilePage requires path");
187
+ }
188
+ const offset = input.offset;
189
+ if (offset !== undefined && (!Number.isInteger(offset) || (offset as number) < 0)) {
190
+ throw new Error(
191
+ "state.readFilePage offset must be a previous page.nextOffset; omit it on the first call",
192
+ );
193
+ }
194
+ return {
195
+ path: input.path,
196
+ ...(offset === undefined ? {} : { offset: offset as number }),
197
+ };
198
+ }
@@ -0,0 +1,42 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import { Type } from "@earendil-works/pi-ai";
3
+ import type { PiLoadedExtension } from "../../context/extension/assembly";
4
+ import type { PiToolCandidate } from "../runtime/contracts";
5
+ import { result } from "../runtime/result";
6
+
7
+
8
+ const noParameters = Type.Object({}, { additionalProperties: false });
9
+
10
+ /**
11
+ * 为当前已加载 Extension 列表创建一个 Pi 工具候选项。
12
+ *
13
+ * Runtime 组装层在 Extension 装载完成后调用,模型需要发现当前可用扩展时使用该工具。
14
+ *
15
+ * 候选项捕获准备阶段的只读列表,这与 pinned Runtime 组装保持一致,不能在执行时重新扫描出另一份 Extension 集合。
16
+ */
17
+ export function listExtensionsPiToolCandidate(
18
+ extensions: readonly PiLoadedExtension[],
19
+ ): PiToolCandidate {
20
+ const tool: AgentTool<typeof noParameters> = {
21
+ name: "list_extensions",
22
+ label: "List extensions",
23
+ description: "List all currently loaded extensions and their tools.",
24
+ parameters: noParameters,
25
+ // 返回这个 pinned Runtime 已加载的 Extension 快照。
26
+ // Pi 工具循环在模型选择 `list_extensions` 时调用,调用前允许 Turn 取消。
27
+ // 同时返回 count 和原列表,使模型既能快速判空又不丢失 Extension 细节。
28
+ async execute(_toolCallId, _input, signal) {
29
+ signal?.throwIfAborted();
30
+ return result({
31
+ count: extensions.length,
32
+ extensions,
33
+ });
34
+ },
35
+ };
36
+ return {
37
+ owner: "core:extensions",
38
+ requiredExecutionLevel: "low",
39
+ retry: "idempotent",
40
+ tool,
41
+ };
42
+ }
@@ -1,30 +1,33 @@
1
- export * from "./ai-adapter";
2
- export * from "./approval";
3
- export * from "./base";
4
- export * from "./compiler";
5
- export * from "./core";
6
- export * from "./core-host";
7
- export * from "./declared";
8
- export * from "./decision-gate";
9
- export * from "./human-decisions";
10
- export * from "./gateway";
1
+ export * from "./adapters/ai-to-pi";
2
+ export * from "./runtime/approval";
3
+ export * from "./assistant";
4
+ export * from "./runtime/contracts";
5
+ export * from "./runtime/compiler";
6
+ export * from "./runtime/activation";
7
+ export * from "./codemode";
8
+ export * from "./codemode/adapter";
9
+ export * from "./extension";
10
+ export * from "./adapters/declared";
11
+ export * from "./runtime/decision-gate";
12
+ export * from "./runtime/human-decisions";
13
+ export * from "./mcp/gateway";
11
14
  export * from "./mcp";
12
- export * from "./interaction";
15
+ export * from "./runtime/interaction";
13
16
  export * from "./schedule";
14
17
  export * from "./skill";
15
18
  export * from "./subagent";
16
- export * from "./subagent-authority";
17
- export * from "./subagent-run-repository";
18
- export * from "./subagent-runtime";
19
- export * from "./subagent-types/contract";
20
- export * from "./subagent-types/registry";
21
- export * from "./temporary-agent/approvals";
22
- export * from "./temporary-agent/core";
23
- export * from "./temporary-agent/coordinator";
24
- export * from "./temporary-agent/runner";
25
- export * from "./temporary-agent/workspace";
26
- export * from "./time";
27
- export * from "./workspace-sandbox";
28
- export * from "./workspace-revision";
29
- export * from "./workspace-versioning";
30
- export * from "./web-search";
19
+ export * from "./subagent/authority";
20
+ export * from "./subagent/repository";
21
+ export * from "./subagent/runtime";
22
+ export * from "./subagent/types/contract";
23
+ export * from "./subagent/types/registry";
24
+ export * from "./subagent/temporary/approvals";
25
+ export * from "./subagent/temporary/core";
26
+ export * from "./subagent/temporary/coordinator";
27
+ export * from "./subagent/temporary/runner";
28
+ export * from "./subagent/temporary/workspace";
29
+ export * from "./workspace";
30
+ export * from "./sandbox";
31
+ export * from "./workspace/revision";
32
+ export * from "./workspace/versioning";
33
+ export * from "./web/search";