@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
@@ -0,0 +1,140 @@
1
+ import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { TSchema } from "@earendil-works/pi-ai";
3
+ import type { ApprovalReceipt } from "../../submission/contracts";
4
+ import { type BudgetPolicy } from "../../context/memory/budget/gate";
5
+ import { type SpillWorkspace } from "../../lib/artifacts";
6
+ import { type ExecutionLevel } from "../../lib/execution-level";
7
+
8
+ // 本文件沿用 `src/index.ts` 入口定义的 Tool Candidate 和 Tool Settlement 术语。
9
+
10
+
11
+ /**
12
+ * 声明一个 Tool 的结果由客户端投递,而不是由 `execute()` 产出。
13
+ *
14
+ * 带这个字段的 Tool 会在执行链里 park 住,等宿主经 `respondToolInteraction` 把响应送进来,
15
+ * 再用 `settle` 把响应映射成这次 `toolCallId` 的权威 ToolResult。它的 `execute` 不会被调用。
16
+ *
17
+ * 这是 AI SDK 里「没有 execute 的 user-interaction tool」的等价物;差别是我们 park 住整个 Turn,
18
+ * 而不是让 tool part 悬空到下一次请求 —— 我们的 transcript 是服务端权威的,悬空的 tool_use
19
+ * 会让下一次模型调用非法。
20
+ */
21
+ export interface PiToolInteractionSpec {
22
+ /** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
23
+ readonly validateResponse: (input: unknown, response: unknown) => boolean;
24
+ /** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
25
+ readonly settle?: (
26
+ input: unknown,
27
+ response: unknown,
28
+ ) => Omit<AgentToolResult<unknown>, "terminate">;
29
+ }
30
+
31
+ /** Trusted Tool metadata used to evaluate a scheduled task grant. */
32
+ export interface PiToolUnattendedExecutionSpec {
33
+ readonly capability: string;
34
+ readonly operation: string;
35
+ readonly objectForInput?: (
36
+ input: unknown,
37
+ ) => unknown | Promise<unknown>;
38
+ }
39
+
40
+ /** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
41
+ export type ToolExposureMode = "direct" | "codemode" | "both";
42
+
43
+ /** Host-prepared confirmation travels through the existing durable approval. */
44
+ export interface PiToolConfirmationSpec {
45
+ prepare(input: unknown, context: { intentId: string }): Promise<unknown>;
46
+ execute(prepared: unknown, context: { intentId: string; signal: AbortSignal }): Promise<AgentToolResult<unknown>>;
47
+ }
48
+
49
+ /**
50
+ * 一份尚未进入最终 Tool Surface 的 Pi 工具及其治理字段。
51
+ *
52
+ * 单个工厂可以写成 `PiToolCandidate<typeof parameters, Details>`,把 Schema 和
53
+ * details 留在类型里。混进同一个 `PiToolCandidate[]` 时走默认擦除形式:
54
+ * `AgentTool` 在两个类型参数上都是不变的,只有默认 `any` 能收进异构列表。
55
+ */
56
+ export interface PiToolCandidate<
57
+ TParameters extends TSchema = any,
58
+ TDetails = any,
59
+ > {
60
+ /** Presentation only, resolved at the model boundary; never changes approved execution inputs. */
61
+ readonly modelGuidance?: () => Promise<{ description: string; systemPrompt?: string }>;
62
+ readonly confirmation?: PiToolConfirmationSpec;
63
+ readonly owner: string;
64
+ readonly tool: AgentTool<TParameters, TDetails>;
65
+ /** @internal Send the complete schema but hide it until provider Tool Search finds it. */
66
+ readonly deferLoading?: true;
67
+ /** Omitted means visible both directly and through Code Mode. */
68
+ readonly exposureMode?: ToolExposureMode;
69
+ /** Conservative maximum used in the stable Runtime descriptor. */
70
+ readonly requiredExecutionLevel: ExecutionLevel;
71
+ /** Trusted parameter-level policy, evaluated before approval or dispatch. */
72
+ readonly requiredExecutionLevelForInput?: (
73
+ input: unknown,
74
+ ) => ExecutionLevel | Promise<ExecutionLevel>;
75
+ /**
76
+ * 无条件需要人来决定:不看执行档位矩阵,每次调用都 park 出审批。
77
+ *
78
+ * 档位差是「够不够格自己跑」,不是「该不该问人」。花用户钱、改变账户
79
+ * 状态这类工具,一旦 Agent 档位被 `allow_level` 永久拉高就会静默执行,
80
+ * 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
81
+ */
82
+ readonly alwaysRequiresApproval?: boolean;
83
+ /** Declares the concrete grant identity derived from actual Tool input. */
84
+ readonly unattendedExecution?: PiToolUnattendedExecutionSpec;
85
+ /** Explicitly forbids this Tool from unattended Scheduled Runs. */
86
+ readonly unattendedMode?: "forbidden";
87
+ /** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
88
+ readonly retry?: "idempotent" | "non-idempotent";
89
+ readonly summary?: string;
90
+ readonly source?: ApprovalReceipt["source"];
91
+ readonly interaction?: PiToolInteractionSpec;
92
+ /** Large payloads spill by default; protocol-shaped results opt into structure. */
93
+ readonly outputBudget?: BudgetPolicy;
94
+ /**
95
+ * 编进 Harness 工具表,但不进入初始 `setActiveTools`。
96
+ * 之后由其他 Tool 的 `toolsToActivate` 经 `setActiveTools` 写入 transcript `toolsAdded`。
97
+ */
98
+ readonly inactiveUntilActivated?: true;
99
+ /**
100
+ * 本次成功结果要把哪些已编译工具加入活动集。未知名字会被忽略。
101
+ * 替代已删除的 `AgentToolResult.addedToolNames`。
102
+ */
103
+ readonly toolsToActivate?: (
104
+ result: AgentToolResult<unknown>,
105
+ ) => readonly string[];
106
+ }
107
+
108
+ export interface CompileHarnessToolsOptions {
109
+ readonly workspace?: SpillWorkspace;
110
+ readonly resolveConfirmation?: (candidate: PiToolCandidate, toolCallId: string, args: unknown) => { prepared: unknown; intentId: string };
111
+ /** Recheck live authorization at the effect boundary, including safe replay. */
112
+ readonly authorize?: (
113
+ candidate: PiToolCandidate,
114
+ args: unknown,
115
+ invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
116
+ ) => void | Promise<void>;
117
+ /** Return an already decided interaction result without invoking the handler. */
118
+ readonly resolveReplay?: (
119
+ candidate: PiToolCandidate,
120
+ toolCallId: string,
121
+ args: unknown,
122
+ ) => AgentToolResult<unknown> | undefined | Promise<AgentToolResult<unknown> | undefined>;
123
+ /** Optional business projection; Pi remains the authoritative Tool result. */
124
+ readonly onSettlement?: (
125
+ call: SettledPiToolCall,
126
+ ) => void | Promise<void>;
127
+ }
128
+
129
+ /** 描述一次已进入持久化结算边界的 Pi 工具调用。 */
130
+ export interface SettledPiToolCall {
131
+ readonly toolCallId: string;
132
+ readonly toolName: string;
133
+ readonly args: unknown;
134
+ readonly result: AgentToolResult<unknown>;
135
+ readonly isError: boolean;
136
+ /** Diagnostic-only original error; callers must not persist or expose it. */
137
+ readonly cause?: unknown;
138
+ readonly summary?: string;
139
+ readonly source?: ApprovalReceipt["source"];
140
+ }
@@ -1,21 +1,25 @@
1
- import type {
2
- AgentToolResult,
3
- HookHandler,
4
- } from "@earendil-works/pi-agent-core";
5
- import type { ExecutionLevel } from "../lib/execution-level";
1
+ import type { AgentToolResult, HookHandler } from "@earendil-works/pi-agent-core";
2
+ import type { ExecutionLevel } from "../../lib/execution-level";
6
3
  import {
7
4
  EXECUTION_LEVELS,
5
+ minExecutionLevel,
8
6
  requiresExecutionApproval,
9
- } from "../lib/execution-level";
10
- import type { PiToolCandidate } from "./compiler";
7
+ } from "../../lib/execution-level";
8
+ import type { RuntimeTelemetryCoordinator } from "../../telemetry/coordinator";
11
9
  import type { ApprovalRepository } from "./approval";
10
+ import type { PiToolCandidate } from "./contracts";
12
11
  import type { ToolInteractionRepository } from "./interaction";
13
- import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
12
+ import { UnattendedExecutionError } from "./unattended-error";
14
13
 
15
14
  export interface HarnessDecisionSubmission {
16
15
  readonly submissionId: string;
17
16
  readonly requestId: string;
18
17
  readonly executionLevel: ExecutionLevel;
18
+ readonly origin?: "user" | "scheduled";
19
+ /** Maximum level allowed by the owning Schedule Definition. */
20
+ readonly scheduledMaxExecutionLevel?: ExecutionLevel;
21
+ /** @deprecated Retained for rolling upgrades; ignored by the decision gate. */
22
+ readonly unattendedExecutionGrant?: unknown;
19
23
  }
20
24
 
21
25
  export interface HarnessDecisionGateOptions {
@@ -37,6 +41,10 @@ export interface HarnessDecisionGateOptions {
37
41
  readonly requiredExecutionLevel: ExecutionLevel;
38
42
  readonly signal: AbortSignal;
39
43
  }) => void | Promise<void>;
44
+ /** @deprecated Scheduled authorization is local and never calls this hook. */
45
+ readonly authorizeUnattendedExecution?: (input: unknown) => void | Promise<void>;
46
+ readonly onUnattendedFailure?: (reason: string) => void | Promise<void>;
47
+ readonly unattendedFailure?: () => string | null | undefined;
40
48
  }
41
49
 
42
50
  function waitForRelease(signal: AbortSignal): Promise<never> {
@@ -81,33 +89,41 @@ export class HarnessDecisionGate {
81
89
  readonly beforeTool: HookHandler<"before_tool"> = async (event, context) => {
82
90
  const candidate = this.candidates.get(event.toolName);
83
91
  if (!candidate) return undefined;
84
- const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
85
- ? await candidate.requiredExecutionLevelForInput(event.args)
86
- : candidate.requiredExecutionLevel;
87
- if (!EXECUTION_LEVELS.includes(requiredExecutionLevel)) {
88
- throw new Error(
89
- `SpringBrand Tool "${event.toolName}" resolved an invalid execution level`,
90
- );
92
+ let requiredExecutionLevel: ExecutionLevel;
93
+ try {
94
+ requiredExecutionLevel = await this.resolveExecutionLevel(candidate, event.args);
95
+ } catch (error) {
96
+ if (!(error instanceof UnattendedExecutionError)) throw error;
97
+ return { block: { reason: error.message, terminate: true } };
91
98
  }
99
+ const effectiveLevel = this.effectiveExecutionLevel();
100
+ const needsApproval = candidate.alwaysRequiresApproval ||
101
+ requiresExecutionApproval(effectiveLevel, requiredExecutionLevel);
92
102
  const executionId = `${this.options.submission.submissionId}:${event.toolCallId}`;
103
+ let unattendedAuthorized = false;
104
+ try {
105
+ unattendedAuthorized = await this.authorizeUnattended(candidate, event.args, {
106
+ toolCallId: event.toolCallId,
107
+ signal: operationSignal(context.abortSignal),
108
+ }, requiredExecutionLevel);
109
+ } catch (error) {
110
+ if (!(error instanceof UnattendedExecutionError)) throw error;
111
+ return { block: { reason: error.message, terminate: true } };
112
+ }
93
113
  const existing = this.options.approvals.find(executionId);
114
+ if (existing && (existing.requestId !== this.options.submission.requestId ||
115
+ existing.toolName !== event.toolName || existing.inputJson !== stableInput(event.args))) {
116
+ throw new Error(`Conflicting durable Tool approval: ${event.toolCallId}`);
117
+ }
94
118
  if (existing?.status === "rejected") {
95
119
  return { block: { reason: existing.reason ?? "Tool execution denied by user" } };
96
120
  }
97
- const needsApproval = candidate.alwaysRequiresApproval ||
98
- requiresExecutionApproval(
99
- this.options.submission.executionLevel,
100
- requiredExecutionLevel,
101
- );
102
- if (needsApproval && existing?.status !== "approved") {
103
- if (existing && (
104
- existing.requestId !== this.options.submission.requestId ||
105
- existing.toolName !== event.toolName ||
106
- existing.inputJson !== stableInput(event.args)
107
- )) {
108
- throw new Error(`Conflicting durable Tool approval: ${event.toolCallId}`);
109
- }
121
+ if (needsApproval && !unattendedAuthorized && existing?.status !== "approved") {
110
122
  if (!existing) {
123
+ const prepared = candidate.confirmation
124
+ ? await candidate.confirmation.prepare(event.args, { intentId: executionId })
125
+ : undefined;
126
+ operationSignal(context.abortSignal).throwIfAborted();
111
127
  const createdAt = Date.now();
112
128
  this.options.transaction(() => this.options.approvals.insert({
113
129
  executionId,
@@ -120,6 +136,7 @@ export class HarnessDecisionGate {
120
136
  executionLevel: this.options.submission.executionLevel,
121
137
  requiredExecutionLevel,
122
138
  inputJson: stableInput(event.args),
139
+ confirmationJson: prepared === undefined ? null : JSON.stringify(prepared),
123
140
  status: "pending",
124
141
  createdAt,
125
142
  }));
@@ -181,16 +198,109 @@ export class HarnessDecisionGate {
181
198
  args: unknown,
182
199
  invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
183
200
  ): Promise<void> {
184
- const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
185
- ? await candidate.requiredExecutionLevelForInput(args)
186
- : candidate.requiredExecutionLevel;
187
- await this.options.gateTool?.({
188
- toolCallId: invocation.toolCallId,
189
- toolName: candidate.tool.name,
190
- input: args,
201
+ const requiredExecutionLevel = await this.resolveExecutionLevel(candidate, args);
202
+ await this.authorizeUnattended(
203
+ candidate,
204
+ args,
205
+ invocation,
191
206
  requiredExecutionLevel,
192
- signal: invocation.signal,
193
- });
207
+ );
208
+ try {
209
+ await this.options.gateTool?.({
210
+ toolCallId: invocation.toolCallId,
211
+ toolName: candidate.tool.name,
212
+ input: args,
213
+ requiredExecutionLevel,
214
+ signal: invocation.signal,
215
+ });
216
+ } catch (error) {
217
+ if (this.options.submission.origin !== "scheduled") throw error;
218
+ const reason = error instanceof Error ? error.message : String(error);
219
+ await this.denyUnattended(
220
+ `Unattended execution denied for ${candidate.tool.name}: ${reason}`,
221
+ );
222
+ }
223
+ this.assertUnattendedActive();
224
+ }
225
+
226
+ private assertUnattendedActive(): void {
227
+ if (this.options.submission.origin !== "scheduled") return;
228
+ const failure = this.options.unattendedFailure?.();
229
+ if (failure) throw new UnattendedExecutionError(failure);
230
+ }
231
+
232
+ private effectiveExecutionLevel(): ExecutionLevel {
233
+ const submission = this.options.submission;
234
+ const ceiling = submission.origin === "scheduled"
235
+ ? submission.scheduledMaxExecutionLevel ?? "high"
236
+ : submission.executionLevel;
237
+ return minExecutionLevel(submission.executionLevel, ceiling);
238
+ }
239
+
240
+ private async resolveExecutionLevel(candidate: PiToolCandidate, input: unknown): Promise<ExecutionLevel> {
241
+ this.assertUnattendedActive();
242
+ try {
243
+ const level = candidate.requiredExecutionLevelForInput
244
+ ? await candidate.requiredExecutionLevelForInput(input)
245
+ : candidate.requiredExecutionLevel;
246
+ if (!EXECUTION_LEVELS.includes(level)) {
247
+ throw new Error(`SpringBrand Tool "${candidate.tool.name}" resolved an invalid execution level`);
248
+ }
249
+ return level;
250
+ } catch (error) {
251
+ if (this.options.submission.origin !== "scheduled") throw error;
252
+ const reason = error instanceof Error ? error.message : String(error);
253
+ return this.denyUnattended(
254
+ `SCHEDULED_EXECUTION_RISK_CHECK_FAILED: ${candidate.tool.name}: ${reason}`,
255
+ );
256
+ }
257
+ }
258
+
259
+ private async authorizeUnattended(
260
+ candidate: PiToolCandidate,
261
+ input: unknown,
262
+ invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
263
+ requiredExecutionLevel: ExecutionLevel,
264
+ ): Promise<boolean> {
265
+ if (this.options.submission.origin !== "scheduled") return false;
266
+ this.assertUnattendedActive();
267
+ if (candidate.interaction) {
268
+ return this.denyUnattended(
269
+ `SCHEDULED_EXECUTION_REQUIRES_INTERACTION: ${candidate.tool.name}`,
270
+ );
271
+ }
272
+ if (candidate.alwaysRequiresApproval) {
273
+ return this.denyUnattended(
274
+ `SCHEDULED_EXECUTION_ALWAYS_REQUIRES_APPROVAL: ${candidate.tool.name}`,
275
+ );
276
+ }
277
+ if (candidate.unattendedMode === "forbidden") {
278
+ return this.denyUnattended(
279
+ `SCHEDULED_EXECUTION_FORBIDDEN: ${candidate.tool.name}`,
280
+ );
281
+ }
282
+ const effectiveLevel = this.effectiveExecutionLevel();
283
+ if (requiresExecutionApproval(effectiveLevel, requiredExecutionLevel)) {
284
+ return this.denyUnattended(
285
+ `SCHEDULED_EXECUTION_LEVEL_EXCEEDED: ${candidate.tool.name} requires ${requiredExecutionLevel}; effective level is ${effectiveLevel}`,
286
+ );
287
+ }
288
+ return true;
289
+ }
290
+
291
+ private async denyUnattended(reason: string): Promise<never> {
292
+ await this.options.onUnattendedFailure?.(reason);
293
+ throw new UnattendedExecutionError(reason);
294
+ }
295
+
296
+ resolveConfirmation(candidate: PiToolCandidate, toolCallId: string, args: unknown): { prepared: unknown; intentId: string } {
297
+ const intentId = `${this.options.submission.submissionId}:${toolCallId}`;
298
+ const approval = this.options.approvals.find(intentId);
299
+ if (!approval || approval.status !== "approved" || !approval.confirmationJson ||
300
+ approval.toolName !== candidate.tool.name || approval.inputJson !== stableInput(args)) {
301
+ throw new Error("Matching user confirmation is required");
302
+ }
303
+ return { prepared: JSON.parse(approval.confirmationJson), intentId };
194
304
  }
195
305
 
196
306
  resolveReplay(
@@ -1,7 +1,7 @@
1
- import type { ApprovalDecision } from "../submission/contracts";
2
- import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
1
+ import type { ApprovalDecision } from "../../submission/contracts";
2
+ import type { RuntimeTelemetryCoordinator } from "../../telemetry/coordinator";
3
3
  import type { ApprovalRepository } from "./approval/repository";
4
- import type { PiToolInteractionSpec } from "./compiler";
4
+ import type { PiToolInteractionSpec } from "./contracts";
5
5
  import type { StoredToolInteraction, ToolInteractionRepository } from "./interaction/repository";
6
6
 
7
7
  /** Human decisions own validation and durable facts; the caller resumes Pi. */
@@ -12,7 +12,7 @@ export class HarnessHumanDecisions {
12
12
  readonly transaction: <T>(run: () => T) => T;
13
13
  readonly isSubmissionActive: (submissionId: string) => boolean;
14
14
  readonly telemetry: Pick<RuntimeTelemetryCoordinator, "capture">;
15
- }) {}
15
+ }) { }
16
16
 
17
17
  isWaitingForApproval(submissionId: string): boolean {
18
18
  return this.options.approvals.listPendingForSubmission(submissionId).length > 0;
@@ -1,7 +1,6 @@
1
1
  import type { SqlTaggedTemplate } from "agents/chat";
2
2
 
3
3
  // Tool Interaction 的统一定义见 ./index.ts。
4
- // #region 状态与行类型
5
4
  export type ToolInteractionStatus = "pending" | "responded" | "cancelled";
6
5
 
7
6
  export interface StoredToolInteraction {
@@ -41,8 +40,6 @@ type ToolInteractionRow = {
41
40
  responded_at: number | null;
42
41
  };
43
42
 
44
- // #endregion
45
- // #region 行映射
46
43
  // 把 SQLite 的 Interaction 行转成 Runtime 对象。
47
44
  // find 与 findPending* 查到记录后调用,上层因而不需要理解 snake_case 列名。
48
45
  // 状态由表内字符串转成联合类型;新增列或状态时必须同步核对类型、SELECT 和映射。
@@ -61,13 +58,11 @@ function mapRow(row: ToolInteractionRow): StoredToolInteraction {
61
58
  };
62
59
  }
63
60
 
64
- // #endregion
65
- // #region Repository
66
61
  export class ToolInteractionRepository {
67
62
  // 保存当前 Agent SQLite 的查询入口。
68
63
  // RuntimeDatabase 构造时调用,InteractionLifecycle 随后共享这个 Repository。
69
64
  // Interaction 必须与所属 Submission 在同一 Agent 存储中查询,不能跨 Durable Object 拼接状态。
70
- constructor(private readonly sql: SqlTaggedTemplate) {}
65
+ constructor(private readonly sql: SqlTaggedTemplate) { }
71
66
 
72
67
  // 按 interactionId 读取一条完整 Interaction,不存在时返回 null。
73
68
  // InteractionLifecycle 在请求去重、响应、取消和收尾时调用,调用方必须处理 null。
@@ -182,4 +177,3 @@ export class ToolInteractionRepository {
182
177
  `.map(mapRow);
183
178
  }
184
179
  }
185
- // #endregion
@@ -0,0 +1,22 @@
1
+ import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
2
+ import { serializeOutput } from "../../lib/artifacts";
3
+
4
+ export function result<T>(details: T): AgentToolResult<T> {
5
+ return {
6
+ content: [{ type: "text", text: serializeOutput(details).text }],
7
+ details,
8
+ };
9
+ }
10
+ // 作用:向 Pi 报告一次 Workspace 或 Sandbox 操作已经开始。
11
+ // 调用:可能访问存储或容器的 execute 方法在进入 Port 前调用。
12
+ // 原因:复用统一的 running 结果形状,
13
+ // 让长操作有进度反馈而不复制回调协议。
14
+ export function running(
15
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
16
+ text: string,
17
+ ): void {
18
+ onUpdate?.({
19
+ content: [{ type: "text", text }],
20
+ details: { status: "running" },
21
+ });
22
+ }
@@ -0,0 +1,4 @@
1
+ /** A durable scheduled-run rejection, not a recoverable Tool failure. */
2
+ export class UnattendedExecutionError extends Error {
3
+ readonly name = "UnattendedExecutionError";
4
+ }