@springbrand/agent-runtime 0.1.0

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 (75) hide show
  1. package/package.json +28 -0
  2. package/src/db/approval.repo.ts +291 -0
  3. package/src/db/ext-context.repo.ts +34 -0
  4. package/src/db/index.ts +83 -0
  5. package/src/db/message-ui.repo.ts +39 -0
  6. package/src/db/milestone.repo.ts +96 -0
  7. package/src/db/runtime-event-outbox.repo.ts +89 -0
  8. package/src/db/schema.ts +164 -0
  9. package/src/db/settlement.repo.ts +104 -0
  10. package/src/db/steer.repo.ts +73 -0
  11. package/src/db/submission.repo.ts +323 -0
  12. package/src/index.ts +133 -0
  13. package/src/kernel/approval-lifecycle.ts +552 -0
  14. package/src/kernel/bindings.ts +898 -0
  15. package/src/kernel/degradation.ts +15 -0
  16. package/src/kernel/extensions.ts +108 -0
  17. package/src/kernel/profile.ts +116 -0
  18. package/src/kernel/public-contracts.ts +17 -0
  19. package/src/kernel/receipts.ts +124 -0
  20. package/src/kernel/recoverable-chat-agent.ts +899 -0
  21. package/src/kernel/state.ts +76 -0
  22. package/src/kernel/submission-lifecycle.ts +600 -0
  23. package/src/layers/context/budget/gate.ts +88 -0
  24. package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
  25. package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
  26. package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
  27. package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
  28. package/src/layers/orchestration/temporary-agent/core.ts +152 -0
  29. package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
  30. package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
  31. package/src/lib/artifacts.ts +54 -0
  32. package/src/lib/egress.ts +44 -0
  33. package/src/lib/execution-level.ts +27 -0
  34. package/src/lib/extension-name.ts +18 -0
  35. package/src/lib/host-actions.ts +57 -0
  36. package/src/lib/mcp.ts +86 -0
  37. package/src/lib/model-catalog.ts +7 -0
  38. package/src/lib/prompt.ts +139 -0
  39. package/src/lib/telemetry-dev.ts +44 -0
  40. package/src/pi/assembly/context.ts +510 -0
  41. package/src/pi/assembly/extensions.ts +661 -0
  42. package/src/pi/assembly/index.ts +19 -0
  43. package/src/pi/assembly/snapshot.ts +200 -0
  44. package/src/pi/message/contract.ts +8 -0
  45. package/src/pi/message/conversion.ts +73 -0
  46. package/src/pi/message/index.ts +3 -0
  47. package/src/pi/message/projection.ts +604 -0
  48. package/src/pi/runtime-adapter/assembly.ts +552 -0
  49. package/src/pi/runtime-adapter/execution.ts +683 -0
  50. package/src/pi/runtime-adapter/index.ts +232 -0
  51. package/src/pi/runtime-adapter/models.ts +243 -0
  52. package/src/pi/runtime-adapter/recovery.ts +805 -0
  53. package/src/pi/runtime-adapter/transcript.ts +825 -0
  54. package/src/pi/session/index.ts +24 -0
  55. package/src/pi/session/storage.ts +353 -0
  56. package/src/pi/tool/ai-adapter.ts +100 -0
  57. package/src/pi/tool/base.ts +110 -0
  58. package/src/pi/tool/compiler.ts +444 -0
  59. package/src/pi/tool/core-host.ts +48 -0
  60. package/src/pi/tool/core.ts +251 -0
  61. package/src/pi/tool/index.ts +32 -0
  62. package/src/pi/tool/mcp.ts +319 -0
  63. package/src/pi/tool/schedule.ts +198 -0
  64. package/src/pi/tool/skill.ts +455 -0
  65. package/src/pi/tool/subagent.ts +148 -0
  66. package/src/pi/tool/web-search/api.ts +1292 -0
  67. package/src/pi/tool/web-search/index.ts +2 -0
  68. package/src/pi/tool/web-search/web-search.ts +127 -0
  69. package/src/pi/tool/workspace-sandbox.ts +664 -0
  70. package/src/pi/turn/approval.ts +181 -0
  71. package/src/pi/turn/index.ts +62 -0
  72. package/src/pi/turn/tool-recovery.ts +792 -0
  73. package/src/plugins.ts +1024 -0
  74. package/src/runtime-agent.ts +654 -0
  75. package/src/runtime.ts +2880 -0
@@ -0,0 +1,444 @@
1
+ import type {
2
+ AgentOptions,
3
+ AgentTool,
4
+ AgentToolResult,
5
+ } from "@earendil-works/pi-agent-core";
6
+ import type { ApprovalReceipt } from "../../kernel/receipts";
7
+ import { boundDurableToolOutput } from "../../layers/context/budget/gate";
8
+ import {
9
+ EXECUTION_LEVELS,
10
+ type ExecutionLevel,
11
+ } from "../../lib/execution-level";
12
+
13
+ // 本文件沿用 `../../index.ts` 入口定义的 Tool Candidate、Tool Settlement 和 Governance 术语。
14
+
15
+ // #region Public contracts
16
+
17
+ /** 描述一个尚未经过去重、禁用规则和结算包装的 Pi 工具。 */
18
+ export interface PiToolCandidate {
19
+ readonly owner: string;
20
+ readonly authorized: boolean;
21
+ readonly tool: AgentTool<any, any>;
22
+ readonly requiredExecutionLevel: ExecutionLevel;
23
+ readonly summary?: string;
24
+ readonly source?: ApprovalReceipt["source"];
25
+ }
26
+
27
+ /** 控制候选 Pi 工具如何被编译为可执行工具集。 */
28
+ export interface CompilePiToolsOptions {
29
+ readonly deny?: readonly string[];
30
+ readonly governance?: PiToolGovernance;
31
+ readonly settle: (
32
+ call: SettledPiToolCall,
33
+ ) => void | Promise<void>;
34
+ }
35
+
36
+ /** 记录一次 Pi 工具调用的耗时、结果大小和溢出状态。 */
37
+ export interface PiToolTelemetry {
38
+ readonly tool: string;
39
+ readonly ms: number;
40
+ readonly ok: boolean;
41
+ readonly bytes: number;
42
+ readonly spill: boolean;
43
+ }
44
+
45
+ const governanceState = Symbol("PiToolGovernanceState");
46
+
47
+ /** 向 Pi Agent 提供工具后置钩子,并为编译器保留同一 Turn 的治理状态。 */
48
+ export interface PiToolGovernance {
49
+ readonly afterToolCall: NonNullable<AgentOptions["afterToolCall"]>;
50
+ readonly [governanceState]: PiToolGovernanceState;
51
+ }
52
+
53
+ /** 描述一次已进入持久化结算边界的 Pi 工具调用。 */
54
+ export interface SettledPiToolCall {
55
+ readonly toolCallId: string;
56
+ readonly toolName: string;
57
+ readonly args: unknown;
58
+ readonly result: AgentToolResult<unknown>;
59
+ readonly isError: boolean;
60
+ /** Diagnostic-only original error; callers must not persist or expose it. */
61
+ readonly cause?: unknown;
62
+ readonly summary?: string;
63
+ readonly source?: ApprovalReceipt["source"];
64
+ }
65
+
66
+ const MAX_TOOL_RETRIES = 3;
67
+ const TURN_FAILURE_CEILING = 8;
68
+
69
+ interface PiToolGovernanceState {
70
+ readonly failures: Map<string, number>;
71
+ readonly terminalCalls: Set<string>;
72
+ totalFailures: number;
73
+ aborted: boolean;
74
+ readonly telemetry?: (event: PiToolTelemetry) => void;
75
+ }
76
+
77
+ // #endregion
78
+
79
+ // #region Governance helpers
80
+
81
+ // 把任意失败值收敛为可安全显示的简短消息。
82
+ // 受治理工具在工具执行或结算抛错时调用它。
83
+ // 读取错误自身也可能抛错,因此最外层必须保留固定回退文本。
84
+ function errorMessage(error: unknown): string {
85
+ try {
86
+ if (error instanceof Error) {
87
+ return error.message || error.name;
88
+ }
89
+ return String(error);
90
+ } catch {
91
+ return "Tool execution failed";
92
+ }
93
+ }
94
+
95
+ // 把工具参数转成可用于重试键的稳定文本。
96
+ // `callKey()` 为每次工具调用计算相同输入的失败次数时调用。
97
+ // JSON 序列化失败时保留 String 回退,以避免循环引用直接使治理失效。
98
+ // TODO(待确认): `String(value)` 本身仍可能抛错,需确认是否要把该极端输入收敛为固定文本。
99
+ function safeStringify(value: unknown): string {
100
+ try {
101
+ return JSON.stringify(value) ?? "∅";
102
+ } catch {
103
+ return String(value);
104
+ }
105
+ }
106
+
107
+ // 把工具名和参数压缩成同一 Turn 内的重试计数键。
108
+ // `recordFailure()`、`blockReason()` 和成功后的计数清理都调用它。
109
+ // 不直接把完整参数存入 Map,可避免 Turn 内重复保留大输入。
110
+ // TODO(待确认): 32 位哈希冲突会把不同参数当成同一次重试,需确认当前误拦截风险是否可接受。
111
+ function callKey(toolName: string, args: unknown): string {
112
+ const input = safeStringify(args);
113
+ let hash = 0x811c9dc5;
114
+ for (let index = 0; index < input.length; index += 1) {
115
+ hash ^= input.charCodeAt(index);
116
+ hash = Math.imul(hash, 0x01000193);
117
+ }
118
+ return `${toolName}\0${(hash >>> 0).toString(36)}`;
119
+ }
120
+
121
+ // 估算工具结果给 Durable Object 带来的字节压力。
122
+ // `emitToolTelemetry()` 在每次成功、失败或阻断后调用它。
123
+ // artifact_ref 已把大结果外溢,因此必须使用 details 中的原始字节数而不是小引用自身的大小。
124
+ function resultPressure(result: AgentToolResult<unknown>): {
125
+ bytes: number;
126
+ spill: boolean;
127
+ } {
128
+ const details = result.details as {
129
+ kind?: string;
130
+ bytes?: number;
131
+ } | undefined;
132
+ if (
133
+ details?.kind === "artifact_ref" &&
134
+ typeof details.bytes === "number"
135
+ ) {
136
+ return { bytes: details.bytes, spill: true };
137
+ }
138
+ try {
139
+ return {
140
+ bytes: JSON.stringify(result).length,
141
+ spill: false,
142
+ };
143
+ } catch {
144
+ return { bytes: 0, spill: false };
145
+ }
146
+ }
147
+
148
+ // 尽力把一次工具调用的计量事件交给可选遥测端口。
149
+ // 受治理 execute 方法在每条终止路径上调用。
150
+ // 遥测是观测而非业务逻辑,所以端口抛错必须被吞掉,不能改变工具结果。
151
+ function emitToolTelemetry(
152
+ state: PiToolGovernanceState | undefined,
153
+ tool: string,
154
+ startedAt: number,
155
+ ok: boolean,
156
+ result: AgentToolResult<unknown>,
157
+ ): void {
158
+ if (!state?.telemetry) return;
159
+ const pressure = resultPressure(result);
160
+ try {
161
+ state.telemetry({
162
+ tool,
163
+ ms: Math.max(0, Math.round(performance.now() - startedAt)),
164
+ ok,
165
+ ...pressure,
166
+ });
167
+ } catch {
168
+ // Observability must not change Tool execution.
169
+ }
170
+ }
171
+
172
+ // 记录一次工具失败并在 Turn 达到上限时打开断路器。
173
+ // 受治理 execute 方法在工具或结算失败后调用。
174
+ // 同时记录单输入次数和 Turn 总数,是为了既阻止原样重试又防止换参数无限失败。
175
+ function recordFailure(
176
+ state: PiToolGovernanceState | undefined,
177
+ toolCallId: string,
178
+ toolName: string,
179
+ args: unknown,
180
+ ): void {
181
+ if (!state) return;
182
+ const key = callKey(toolName, args);
183
+ state.failures.set(key, (state.failures.get(key) ?? 0) + 1);
184
+ state.totalFailures += 1;
185
+ if (state.totalFailures >= TURN_FAILURE_CEILING) {
186
+ state.aborted = true;
187
+ state.terminalCalls.add(toolCallId);
188
+ }
189
+ }
190
+
191
+ // 在执行前判断这次工具调用是否已超过重试或 Turn 上限。
192
+ // 受治理 execute 方法对每个调用首先调用它。
193
+ // 被阻断的 call id 必须记入 terminalCalls,才能让 Pi 的 afterToolCall 在返回错误后终止 Turn。
194
+ function blockReason(
195
+ state: PiToolGovernanceState | undefined,
196
+ toolCallId: string,
197
+ toolName: string,
198
+ args: unknown,
199
+ ): string | undefined {
200
+ if (!state) return;
201
+ if (state.aborted) {
202
+ state.terminalCalls.add(toolCallId);
203
+ return "Turn circuit breaker tripped: too many tool failures this turn. Stop calling tools now.";
204
+ }
205
+ if (
206
+ (state.failures.get(callKey(toolName, args)) ?? 0) >=
207
+ MAX_TOOL_RETRIES
208
+ ) {
209
+ state.terminalCalls.add(toolCallId);
210
+ return `This exact ${toolName} call has already failed ${MAX_TOOL_RETRIES} times — change the input or approach instead of retrying it verbatim.`;
211
+ }
212
+ }
213
+
214
+ // #endregion
215
+
216
+ // #region Governance and compilation
217
+
218
+ /**
219
+ * 为一个 Pi Turn 创建共享的工具重试治理状态。
220
+ *
221
+ * `PiTurnAdapter` 在准备 Turn 时调用,并同时把返回的 `afterToolCall` 交给 Pi Agent。
222
+ *
223
+ * 状态用 symbol 绑定到钩子对象,让编译后的工具和 Pi 后置钩子共享一份 Turn 计数而不暴露公开可变 API。
224
+ */
225
+ export function createPiToolGovernance(
226
+ telemetry?: (event: PiToolTelemetry) => void,
227
+ ): PiToolGovernance {
228
+ const state: PiToolGovernanceState = {
229
+ failures: new Map(),
230
+ terminalCalls: new Set(),
231
+ totalFailures: 0,
232
+ aborted: false,
233
+ telemetry,
234
+ };
235
+ const governance: PiToolGovernance = {
236
+ [governanceState]: state,
237
+ // 在必须终止的工具调用后告诉 Pi 停止当前 Turn。
238
+ // Pi Agent 在每次工具执行结束后调用,并传入实际 toolCall id。
239
+ // 只消费 terminalCalls 中的 id,可避免一次失败误终止后续无关工具调用。
240
+ async afterToolCall(context) {
241
+ if (!state.terminalCalls.delete(context.toolCall.id)) {
242
+ return;
243
+ }
244
+ return { terminate: true };
245
+ },
246
+ };
247
+ return governance;
248
+ }
249
+
250
+ // 为一个候选工具包上输出限额、持久化结算、重试治理和遥测。
251
+ // `compilePiTools()` 对通过授权和 deny 筛选的每个候选项调用。
252
+ // 一个共享包装边界可确保所有工具无论成功还是失败都经过 settle,不能由各工具自行选择是否持久化。
253
+ function governedTool(
254
+ candidate: PiToolCandidate,
255
+ options: CompilePiToolsOptions,
256
+ ): AgentTool<any, any> {
257
+ const execute = candidate.tool.execute;
258
+ const state = options.governance?.[governanceState];
259
+ const settle = options.settle;
260
+
261
+ return {
262
+ ...candidate.tool,
263
+ // 执行一次完整的受治理 Pi 工具调用。
264
+ // Pi 工具循环选中编译后的工具时调用,调用方应传入稳定 toolCall id 供结算去重。
265
+ // 输出限额必须发生在 settle 之前,否则 Durable Object 会持久化一份与模型最终所见不同的过大结果。
266
+ async execute(toolCallId, args, signal, onUpdate) {
267
+ const startedAt = performance.now();
268
+ const blocked = blockReason(
269
+ state,
270
+ toolCallId,
271
+ candidate.tool.name,
272
+ args,
273
+ );
274
+ if (blocked) {
275
+ const cause = new Error(blocked);
276
+ const result = {
277
+ content: [{ type: "text" as const, text: blocked }],
278
+ details: {},
279
+ };
280
+ try {
281
+ await settle({
282
+ toolCallId,
283
+ toolName: candidate.tool.name,
284
+ args,
285
+ result,
286
+ isError: true,
287
+ cause,
288
+ summary: candidate.summary,
289
+ source: candidate.source,
290
+ });
291
+ } catch (settlementFailure) {
292
+ emitToolTelemetry(
293
+ state,
294
+ candidate.tool.name,
295
+ startedAt,
296
+ false,
297
+ result,
298
+ );
299
+ throw settlementFailure;
300
+ }
301
+ emitToolTelemetry(
302
+ state,
303
+ candidate.tool.name,
304
+ startedAt,
305
+ false,
306
+ result,
307
+ );
308
+ throw cause;
309
+ }
310
+
311
+ let result: AgentToolResult<unknown>;
312
+ try {
313
+ result = boundDurableToolOutput(
314
+ await execute(toolCallId, args, signal, onUpdate),
315
+ ) as AgentToolResult<unknown>;
316
+ } catch (cause) {
317
+ const message = errorMessage(cause);
318
+ const result = boundDurableToolOutput({
319
+ content: [{ type: "text", text: message }],
320
+ details: {},
321
+ }) as AgentToolResult<unknown>;
322
+ let failure = cause;
323
+ try {
324
+ await settle({
325
+ toolCallId,
326
+ toolName: candidate.tool.name,
327
+ args,
328
+ result,
329
+ isError: true,
330
+ cause,
331
+ summary: candidate.summary,
332
+ source: candidate.source,
333
+ });
334
+ } catch (settlementFailure) {
335
+ failure = settlementFailure;
336
+ state?.terminalCalls.add(toolCallId);
337
+ }
338
+ recordFailure(
339
+ state,
340
+ toolCallId,
341
+ candidate.tool.name,
342
+ args,
343
+ );
344
+ emitToolTelemetry(
345
+ state,
346
+ candidate.tool.name,
347
+ startedAt,
348
+ false,
349
+ result,
350
+ );
351
+ const boundedMessage = (result.content[0] as { text: string }).text;
352
+ throw cause instanceof Error &&
353
+ failure === cause &&
354
+ boundedMessage === cause.message
355
+ ? failure
356
+ : new Error(
357
+ failure === cause ? boundedMessage : errorMessage(failure),
358
+ { cause: failure },
359
+ );
360
+ }
361
+
362
+ try {
363
+ await settle({
364
+ toolCallId,
365
+ toolName: candidate.tool.name,
366
+ args,
367
+ result,
368
+ isError: false,
369
+ summary: candidate.summary,
370
+ source: candidate.source,
371
+ });
372
+ } catch (cause) {
373
+ state?.terminalCalls.add(toolCallId);
374
+ recordFailure(
375
+ state,
376
+ toolCallId,
377
+ candidate.tool.name,
378
+ args,
379
+ );
380
+ emitToolTelemetry(
381
+ state,
382
+ candidate.tool.name,
383
+ startedAt,
384
+ false,
385
+ result,
386
+ );
387
+ throw cause;
388
+ }
389
+ state?.failures.delete(
390
+ callKey(candidate.tool.name, args),
391
+ );
392
+ emitToolTelemetry(
393
+ state,
394
+ candidate.tool.name,
395
+ startedAt,
396
+ true,
397
+ result,
398
+ );
399
+ return result;
400
+ },
401
+ };
402
+ }
403
+
404
+ /**
405
+ * 把已授权且未被禁用的 Pi 工具候选项编译成可执行工具集。
406
+ *
407
+ * Runtime 组装层在启动前用它验证工具集,`PiTurnAdapter` 在每个 Turn 及手动重试时用它创建执行包装。
408
+ *
409
+ * 工具名是模型调用和结算的唯一键,所以冲突必须在组装时失败,不能用后来者静默覆盖。
410
+ */
411
+ export function compilePiTools(
412
+ candidates: readonly PiToolCandidate[],
413
+ options: CompilePiToolsOptions,
414
+ ): AgentTool<any, any>[] {
415
+ if (!options?.settle) {
416
+ throw new Error("Pi Tool settlement port is required");
417
+ }
418
+ const deny = new Set(options.deny ?? []);
419
+ const tools: AgentTool<any, any>[] = [];
420
+ const owners = new Map<string, string>();
421
+
422
+ for (const candidate of candidates) {
423
+ if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
424
+ throw new Error(
425
+ `Pi Tool "${candidate.tool.name}" has invalid requiredExecutionLevel`,
426
+ );
427
+ }
428
+ if (!candidate.authorized || deny.has(candidate.tool.name)) continue;
429
+
430
+ const existingOwner = owners.get(candidate.tool.name);
431
+ if (existingOwner !== undefined) {
432
+ throw new Error(
433
+ `duplicate tool key "${candidate.tool.name}" (${candidate.owner} collides with ${existingOwner})`,
434
+ );
435
+ }
436
+
437
+ owners.set(candidate.tool.name, candidate.owner);
438
+ tools.push(governedTool(candidate, options));
439
+ }
440
+
441
+ return tools;
442
+ }
443
+
444
+ // #endregion
@@ -0,0 +1,48 @@
1
+ import {
2
+ createCodemodeRuntime,
3
+ DynamicWorkerExecutor,
4
+ truncateResult,
5
+ } from "@cloudflare/codemode";
6
+ import {
7
+ createWorkspaceStateBackend,
8
+ type WorkspaceFsLike,
9
+ } from "@cloudflare/shell";
10
+ import { StateConnector } from "@cloudflare/shell/workers";
11
+ import type { WorkspacePort } from "../../kernel/bindings";
12
+ import { codeExecutionPiToolCandidate } from "./core";
13
+ import type { PiToolCandidate } from "./compiler";
14
+
15
+ // 本文件沿用 `../../index.ts` 入口定义的 Workspace、Port 和 Tool Candidate 术语。
16
+
17
+ /**
18
+ * 把宿主的 Worker Loader、出站网络和 Workspace 组装成 Pi 代码执行工具候选项。
19
+ *
20
+ * Worker 宿主在具备 Durable Object state 和完整平台绑定时调用,然后把返回值交给 Runtime 工具组装。
21
+ *
22
+ * Cloudflare Codemode 把持久化 Runtime 和无状态 Dynamic Worker executor 分开;这里必须用 StateConnector 显式注入已限定范围的 Workspace,并在返回模型前用官方 `truncateResult` 限制结果。
23
+ */
24
+ export function workspaceCodeExecutionPiToolCandidate(options: {
25
+ readonly ctx: DurableObjectState;
26
+ readonly loader: WorkerLoader;
27
+ readonly outbound: Fetcher;
28
+ readonly workspace: WorkspacePort;
29
+ }): PiToolCandidate {
30
+ const runtime = createCodemodeRuntime({
31
+ ctx: options.ctx,
32
+ executor: new DynamicWorkerExecutor({
33
+ loader: options.loader,
34
+ globalOutbound: options.outbound,
35
+ }),
36
+ connectors: [
37
+ new StateConnector(
38
+ options.ctx,
39
+ createWorkspaceStateBackend(
40
+ options.workspace as unknown as WorkspaceFsLike,
41
+ ),
42
+ ),
43
+ ],
44
+ name: "execute",
45
+ transformResult: truncateResult,
46
+ });
47
+ return codeExecutionPiToolCandidate(runtime);
48
+ }