@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.10

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 (172) hide show
  1. package/package.json +7 -7
  2. package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
  3. package/src/adapter/cloudflare/agent.ts +1682 -0
  4. package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
  5. package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +37 -42
  6. package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
  7. package/src/adapter/cloudflare/index.ts +18 -0
  8. package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
  9. package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
  10. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  11. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  12. package/src/{db → adapter/cloudflare}/schema.ts +28 -80
  13. package/src/adapter/cloudflare/subagent/definition.ts +63 -33
  14. package/src/adapter/cloudflare/subagent/runner.ts +174 -120
  15. package/src/adapter/cloudflare/subagent/tools.ts +13 -14
  16. package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
  17. package/src/adapter/cloudflare/universal-agent/tools.ts +10 -12
  18. package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
  19. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
  20. package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
  21. package/src/config/activation.ts +140 -0
  22. package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
  23. package/src/{runtime-assembler.ts → config/assembler.ts} +45 -46
  24. package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
  25. package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
  26. package/src/{runtime-definition.ts → config/definition.ts} +13 -13
  27. package/src/config/harness-runtime.ts +190 -0
  28. package/src/config/index.ts +14 -0
  29. package/src/config/model-guidance.ts +21 -0
  30. package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +60 -19
  31. package/src/{kernel/bindings.ts → config/ports.ts} +32 -16
  32. package/src/{kernel → config}/profile.ts +7 -0
  33. package/src/{kernel → config}/public-contracts.ts +6 -6
  34. package/src/{kernel → config}/runtime-load.ts +1 -1
  35. package/src/{pi/assembly/context.ts → context/assembly.ts} +5 -207
  36. package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
  37. package/src/context/extension/index.ts +3 -0
  38. package/src/context/index.ts +6 -0
  39. package/src/{layers/context → context/memory}/budget/gate.ts +21 -5
  40. package/src/conversation/harness-resume.ts +288 -0
  41. package/src/conversation/harness-stream.ts +99 -0
  42. package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
  43. package/src/conversation/index.ts +6 -0
  44. package/src/{pi → conversation}/message/contract.ts +3 -0
  45. package/src/{pi → conversation}/message/conversion.ts +75 -0
  46. package/src/{pi → conversation}/message/index.ts +1 -0
  47. package/src/{pi → conversation}/message/model-input.ts +1 -1
  48. package/src/{pi → conversation}/message/projection.ts +113 -30
  49. package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
  50. package/src/conversation/state.ts +196 -0
  51. package/src/harness/index.ts +7 -0
  52. package/src/harness/session-harness.ts +562 -0
  53. package/src/harness/submission-operation.ts +49 -0
  54. package/src/index.ts +38 -43
  55. package/src/lib/artifacts.ts +18 -0
  56. package/src/lib/execution-level.ts +13 -0
  57. package/src/lib/mcp.ts +1 -1
  58. package/src/lib/prompt.ts +7 -6
  59. package/src/lib/token-estimate.ts +8 -0
  60. package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -17
  61. package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
  62. package/src/{pi/session → session/cloudflare}/migration.ts +404 -14
  63. package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
  64. package/src/session/index.ts +39 -0
  65. package/src/skills/springbrand-worker-website/index.ts +1 -1
  66. package/src/submission/admission.ts +286 -0
  67. package/src/{kernel/receipts.ts → submission/contracts.ts} +56 -2
  68. package/src/submission/delivery.ts +212 -0
  69. package/src/submission/finalization.ts +111 -0
  70. package/src/submission/index.ts +9 -0
  71. package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
  72. package/src/{db/submission.repo.ts → submission/repository.ts} +117 -114
  73. package/src/submission/steer-delivery.ts +167 -0
  74. package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
  75. package/src/submission/unattended.ts +40 -0
  76. package/src/telemetry/contract.ts +12 -0
  77. package/src/telemetry/coordinator.ts +65 -0
  78. package/src/telemetry/delivery.ts +2 -2
  79. package/src/telemetry/index.ts +5 -0
  80. package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
  81. package/src/telemetry/recorder.ts +1 -1
  82. package/src/telemetry/runtime-telemetry.ts +12 -0
  83. package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
  84. package/src/telemetry/turn-events.ts +279 -0
  85. package/src/tools/README.md +44 -0
  86. package/src/{pi/tool/ai-adapter.ts → tools/adapters/ai-to-pi.ts} +6 -9
  87. package/src/{pi/tool → tools/adapters}/declared.ts +6 -2
  88. package/src/{pi/tool/nested-tools.ts → tools/adapters/pi-to-ai.ts} +2 -2
  89. package/src/tools/assistant/ask-user.ts +317 -0
  90. package/src/tools/assistant/candidate.ts +15 -0
  91. package/src/tools/assistant/followups.ts +34 -0
  92. package/src/tools/assistant/index.ts +19 -0
  93. package/src/tools/assistant/memory.ts +73 -0
  94. package/src/tools/assistant/plan.ts +98 -0
  95. package/src/{pi/tool → tools/assistant}/time.ts +1 -1
  96. package/src/{pi/tool/core-host.ts → tools/codemode/adapter.ts} +50 -49
  97. package/src/{pi/tool/core.ts → tools/codemode/index.ts} +35 -91
  98. package/src/tools/codemode/workspace-connector.ts +198 -0
  99. package/src/tools/extension/index.ts +42 -0
  100. package/src/tools/index.ts +32 -0
  101. package/src/{pi/tool → tools/mcp}/gateway.ts +19 -19
  102. package/src/{pi/tool/mcp.ts → tools/mcp/index.ts} +5 -5
  103. package/src/tools/runtime/approval/index.ts +1 -0
  104. package/src/{db/approval.repo.ts → tools/runtime/approval/repository.ts} +22 -41
  105. package/src/tools/runtime/compiler.ts +264 -0
  106. package/src/tools/runtime/contracts.ts +117 -0
  107. package/src/tools/runtime/decision-gate.ts +337 -0
  108. package/src/tools/runtime/human-decisions.ts +127 -0
  109. package/src/tools/runtime/interaction/index.ts +1 -0
  110. package/src/{db/interaction.repo.ts → tools/runtime/interaction/repository.ts} +1 -7
  111. package/src/tools/runtime/result.ts +22 -0
  112. package/src/tools/runtime/unattended-error.ts +4 -0
  113. package/src/tools/sandbox/index.ts +228 -0
  114. package/src/tools/schedule/index.ts +301 -0
  115. package/src/tools/skill/contracts.ts +20 -0
  116. package/src/tools/skill/index.ts +99 -0
  117. package/src/tools/skill/resources.ts +322 -0
  118. package/src/tools/skill/scripts.ts +168 -0
  119. package/src/{kernel/submission-authority.ts → tools/subagent/authority.ts} +31 -31
  120. package/src/{pi/tool/subagent.ts → tools/subagent/index.ts} +33 -36
  121. package/src/{db/agent-tool.repo.ts → tools/subagent/repository.ts} +1 -1
  122. package/src/{kernel/subagent-runtime.ts → tools/subagent/runtime.ts} +1 -1
  123. package/src/tools/subagent/temporary/approvals.ts +123 -0
  124. package/src/{layers/orchestration/temporary-agent/core.ts → tools/subagent/temporary/coordinator.ts} +41 -49
  125. package/src/tools/subagent/temporary/core.ts +53 -0
  126. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/runner.ts +1 -1
  127. package/src/{layers/orchestration/temporary-agent → tools/subagent/temporary}/workspace.ts +1 -4
  128. package/src/{pi/tool/web-fetch.ts → tools/web/fetch.ts} +4 -13
  129. package/src/tools/web/index.ts +2 -0
  130. package/src/tools/web/search/api.ts +1320 -0
  131. package/src/tools/web/search/index.ts +2 -0
  132. package/src/{pi/tool/web-search/web-search.ts → tools/web/search/search.ts} +5 -7
  133. package/src/{pi/tool/workspace-sandbox.ts → tools/workspace/index.ts} +12 -272
  134. package/src/{pi/tool/workspace-revision.ts → tools/workspace/revision.ts} +2 -2
  135. package/src/db/milestone.repo.ts +0 -104
  136. package/src/db/settlement.repo.ts +0 -104
  137. package/src/kernel/approval-lifecycle.ts +0 -638
  138. package/src/kernel/interaction-lifecycle.ts +0 -433
  139. package/src/kernel/recoverable-chat-agent.ts +0 -1020
  140. package/src/kernel/state.ts +0 -91
  141. package/src/pi/assembly/index.ts +0 -19
  142. package/src/pi/runtime-adapter/execution.ts +0 -1163
  143. package/src/pi/runtime-adapter/index.ts +0 -293
  144. package/src/pi/runtime-adapter/recovery.ts +0 -982
  145. package/src/pi/session/index.ts +0 -24
  146. package/src/pi/tool/base.ts +0 -550
  147. package/src/pi/tool/compiler.ts +0 -483
  148. package/src/pi/tool/index.ts +0 -38
  149. package/src/pi/tool/schedule.ts +0 -261
  150. package/src/pi/tool/skill.ts +0 -565
  151. package/src/pi/tool/web-search/api.ts +0 -1318
  152. package/src/pi/tool/web-search/index.ts +0 -2
  153. package/src/pi/tool/workspace-state-connector.ts +0 -146
  154. package/src/pi/turn/approval.ts +0 -181
  155. package/src/pi/turn/index.ts +0 -82
  156. package/src/pi/turn/interaction.ts +0 -181
  157. package/src/runtime.ts +0 -4760
  158. package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
  159. package/src/{kernel → config}/degradation.ts +0 -0
  160. package/src/{kernel → config}/runtime-config.ts +0 -0
  161. package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
  162. package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
  163. package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
  164. package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
  165. package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
  166. package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
  167. package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
  168. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/contract.ts +0 -0
  169. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/extract/index.ts +0 -0
  170. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/fanout/index.ts +0 -0
  171. package/src/{layers/orchestration/subagents/agent-types → tools/subagent/types}/registry.ts +1 -1
  172. /package/src/{workspace-versioning.ts → tools/workspace/versioning.ts} +0 -0
@@ -0,0 +1,337 @@
1
+ import type { AgentToolResult, HookHandler } from "@earendil-works/pi-agent-core";
2
+ import type { ExecutionLevel } from "../../lib/execution-level";
3
+ import {
4
+ EXECUTION_LEVELS,
5
+ minExecutionLevel,
6
+ requiresExecutionApproval,
7
+ } from "../../lib/execution-level";
8
+ import type { RuntimeTelemetryCoordinator } from "../../telemetry/coordinator";
9
+ import type { ApprovalRepository } from "./approval";
10
+ import type { PiToolCandidate } from "./contracts";
11
+ import type { ToolInteractionRepository } from "./interaction";
12
+ import { UnattendedExecutionError } from "./unattended-error";
13
+
14
+ export interface HarnessDecisionSubmission {
15
+ readonly submissionId: string;
16
+ readonly requestId: string;
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;
23
+ }
24
+
25
+ export interface HarnessDecisionGateOptions {
26
+ readonly submission: HarnessDecisionSubmission;
27
+ readonly candidates: readonly PiToolCandidate[];
28
+ readonly approvals: ApprovalRepository;
29
+ readonly interactions: ToolInteractionRepository;
30
+ readonly transaction: <T>(run: () => T) => T;
31
+ readonly release: (reason: "approval" | "interaction") => void;
32
+ readonly telemetry?: Pick<RuntimeTelemetryCoordinator, "capture">;
33
+ readonly onChanged?: (
34
+ kind: "approval" | "interaction",
35
+ id: string,
36
+ ) => void | Promise<void>;
37
+ readonly gateTool?: (input: {
38
+ readonly toolCallId: string;
39
+ readonly toolName: string;
40
+ readonly input: unknown;
41
+ readonly requiredExecutionLevel: ExecutionLevel;
42
+ readonly signal: AbortSignal;
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;
48
+ }
49
+
50
+ function waitForRelease(signal: AbortSignal): Promise<never> {
51
+ return new Promise((_resolve, reject) => {
52
+ const abort = () => {
53
+ signal.removeEventListener("abort", abort);
54
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
55
+ };
56
+ if (signal.aborted) return abort();
57
+ signal.addEventListener("abort", abort, { once: true });
58
+ });
59
+ }
60
+
61
+ function operationSignal(signal: AbortSignal | undefined): AbortSignal {
62
+ if (!signal) throw new Error("Harness Tool hook has no operation signal");
63
+ return signal;
64
+ }
65
+
66
+ function stableInput(input: unknown): string {
67
+ return JSON.stringify(input);
68
+ }
69
+
70
+ function interactionResult(
71
+ response: unknown,
72
+ ): AgentToolResult<unknown> {
73
+ return {
74
+ content: [{ type: "text", text: JSON.stringify(response) }],
75
+ details: response,
76
+ };
77
+ }
78
+
79
+ /** Durable policy facts around Pi's before_tool hook; Pi owns execution state. */
80
+ export class HarnessDecisionGate {
81
+ private readonly candidates: ReadonlyMap<string, PiToolCandidate>;
82
+
83
+ constructor(private readonly options: HarnessDecisionGateOptions) {
84
+ this.candidates = new Map(
85
+ options.candidates.map((candidate) => [candidate.tool.name, candidate]),
86
+ );
87
+ }
88
+
89
+ readonly beforeTool: HookHandler<"before_tool"> = async (event, context) => {
90
+ const candidate = this.candidates.get(event.toolName);
91
+ if (!candidate) return undefined;
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 } };
98
+ }
99
+ const effectiveLevel = this.effectiveExecutionLevel();
100
+ const needsApproval = candidate.alwaysRequiresApproval ||
101
+ requiresExecutionApproval(effectiveLevel, requiredExecutionLevel);
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
+ }
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
+ }
118
+ if (existing?.status === "rejected") {
119
+ return { block: { reason: existing.reason ?? "Tool execution denied by user" } };
120
+ }
121
+ if (needsApproval && !unattendedAuthorized && existing?.status !== "approved") {
122
+ if (!existing) {
123
+ const prepared = candidate.confirmation
124
+ ? await candidate.confirmation.prepare(event.args, { intentId: executionId })
125
+ : undefined;
126
+ operationSignal(context.abortSignal).throwIfAborted();
127
+ const createdAt = Date.now();
128
+ this.options.transaction(() => this.options.approvals.insert({
129
+ executionId,
130
+ submissionId: this.options.submission.submissionId,
131
+ requestId: this.options.submission.requestId,
132
+ source: candidate.source ?? "action",
133
+ toolCallId: event.toolCallId,
134
+ toolName: event.toolName,
135
+ summary: candidate.summary ?? candidate.tool.label ?? event.toolName,
136
+ executionLevel: this.options.submission.executionLevel,
137
+ requiredExecutionLevel,
138
+ inputJson: stableInput(event.args),
139
+ confirmationJson: prepared === undefined ? null : JSON.stringify(prepared),
140
+ status: "pending",
141
+ createdAt,
142
+ }));
143
+ this.options.telemetry?.capture("approvalRequested", {
144
+ submissionId: this.options.submission.submissionId,
145
+ toolCallId: event.toolCallId,
146
+ executionId,
147
+ toolName: event.toolName,
148
+ summary: candidate.summary ?? candidate.tool.label ?? event.toolName,
149
+ input: event.args,
150
+ occurredAt: createdAt,
151
+ });
152
+ await this.options.onChanged?.("approval", executionId);
153
+ }
154
+ this.options.release("approval");
155
+ return waitForRelease(operationSignal(context.abortSignal));
156
+ }
157
+
158
+ if (!candidate.interaction) return undefined;
159
+ const interaction = this.options.interactions.find(executionId);
160
+ if (interaction?.status === "responded" || interaction?.status === "cancelled") {
161
+ return undefined;
162
+ }
163
+ if (interaction && (
164
+ interaction.requestId !== this.options.submission.requestId ||
165
+ interaction.toolName !== event.toolName ||
166
+ interaction.inputJson !== stableInput(event.args)
167
+ )) {
168
+ throw new Error(`Conflicting durable Tool interaction: ${event.toolCallId}`);
169
+ }
170
+ if (!interaction) {
171
+ const createdAt = Date.now();
172
+ this.options.transaction(() => this.options.interactions.insert({
173
+ interactionId: executionId,
174
+ submissionId: this.options.submission.submissionId,
175
+ requestId: this.options.submission.requestId,
176
+ toolCallId: event.toolCallId,
177
+ toolName: event.toolName,
178
+ inputJson: stableInput(event.args),
179
+ status: "pending",
180
+ createdAt,
181
+ }));
182
+ this.options.telemetry?.capture("interactionRequested", {
183
+ submissionId: this.options.submission.submissionId,
184
+ toolCallId: event.toolCallId,
185
+ interactionId: executionId,
186
+ toolName: event.toolName,
187
+ input: event.args,
188
+ occurredAt: createdAt,
189
+ });
190
+ await this.options.onChanged?.("interaction", executionId);
191
+ }
192
+ this.options.release("interaction");
193
+ return waitForRelease(operationSignal(context.abortSignal));
194
+ };
195
+
196
+ async authorize(
197
+ candidate: PiToolCandidate,
198
+ args: unknown,
199
+ invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
200
+ ): Promise<void> {
201
+ const requiredExecutionLevel = await this.resolveExecutionLevel(candidate, args);
202
+ await this.authorizeUnattended(
203
+ candidate,
204
+ args,
205
+ invocation,
206
+ requiredExecutionLevel,
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 };
304
+ }
305
+
306
+ resolveReplay(
307
+ candidate: PiToolCandidate,
308
+ toolCallId: string,
309
+ args: unknown,
310
+ ): AgentToolResult<unknown> | undefined {
311
+ if (!candidate.interaction) return undefined;
312
+ const interaction = this.options.interactions.find(
313
+ `${this.options.submission.submissionId}:${toolCallId}`,
314
+ );
315
+ if (!interaction || interaction.status === "pending") return undefined;
316
+ if (interaction.inputJson !== stableInput(args)) {
317
+ throw new Error(`Conflicting durable Tool interaction: ${toolCallId}`);
318
+ }
319
+ if (interaction.status === "cancelled") {
320
+ return {
321
+ content: [{
322
+ type: "text",
323
+ text: "The interaction was cancelled before the user answered.",
324
+ }],
325
+ details: { cancelled: true },
326
+ };
327
+ }
328
+ const response = JSON.parse(interaction.responseJson ?? "null");
329
+ if (!candidate.interaction.validateResponse(args, response)) {
330
+ throw new Error(`Stored Tool interaction response is invalid: ${toolCallId}`);
331
+ }
332
+ const settled = candidate.interaction.settle?.(args, response);
333
+ return settled
334
+ ? { ...settled, terminate: false }
335
+ : interactionResult(response);
336
+ }
337
+ }
@@ -0,0 +1,127 @@
1
+ import type { ApprovalDecision } from "../../submission/contracts";
2
+ import type { RuntimeTelemetryCoordinator } from "../../telemetry/coordinator";
3
+ import type { ApprovalRepository } from "./approval/repository";
4
+ import type { PiToolInteractionSpec } from "./contracts";
5
+ import type { StoredToolInteraction, ToolInteractionRepository } from "./interaction/repository";
6
+
7
+ /** Human decisions own validation and durable facts; the caller resumes Pi. */
8
+ export class HarnessHumanDecisions {
9
+ constructor(private readonly options: {
10
+ readonly approvals: ApprovalRepository;
11
+ readonly interactions: ToolInteractionRepository;
12
+ readonly transaction: <T>(run: () => T) => T;
13
+ readonly isSubmissionActive: (submissionId: string) => boolean;
14
+ readonly telemetry: Pick<RuntimeTelemetryCoordinator, "capture">;
15
+ }) { }
16
+
17
+ isWaitingForApproval(submissionId: string): boolean {
18
+ return this.options.approvals.listPendingForSubmission(submissionId).length > 0;
19
+ }
20
+
21
+ isWaitingForInteraction(submissionId: string): boolean {
22
+ return this.options.interactions.listPendingForSubmission(submissionId).length > 0;
23
+ }
24
+
25
+ async respond(
26
+ pending: StoredToolInteraction,
27
+ spec: PiToolInteractionSpec,
28
+ response: unknown,
29
+ prepareResponse: (input: {
30
+ toolCallId: string;
31
+ toolName: string;
32
+ toolInput: unknown;
33
+ response: unknown;
34
+ }) => Promise<unknown | null>,
35
+ ): Promise<boolean> {
36
+ if (!this.options.isSubmissionActive(pending.submissionId) ||
37
+ this.options.interactions.find(pending.interactionId)?.status !== "pending") return false;
38
+ let input: unknown;
39
+ try { input = JSON.parse(pending.inputJson); } catch { return false; }
40
+ if (!spec.validateResponse(input, response)) return false;
41
+ const prepared = await prepareResponse({
42
+ toolCallId: pending.toolCallId,
43
+ toolName: pending.toolName,
44
+ toolInput: input,
45
+ response,
46
+ });
47
+ if (prepared === null || !spec.validateResponse(input, prepared)) return false;
48
+ return this.options.transaction(() => {
49
+ // Preparation may await a Host operation while stop, steer, or another answer wins.
50
+ const current = this.options.interactions.find(pending.interactionId);
51
+ if (current?.status !== "pending" ||
52
+ !this.options.isSubmissionActive(current.submissionId)) return false;
53
+ const occurredAt = Date.now();
54
+ this.options.interactions.settle(pending.interactionId, "responded", occurredAt, JSON.stringify(prepared));
55
+ this.options.telemetry.capture("interactionResponded", {
56
+ submissionId: current.submissionId,
57
+ toolCallId: current.toolCallId,
58
+ interactionId: current.interactionId,
59
+ response: prepared,
60
+ occurredAt,
61
+ });
62
+ return true;
63
+ });
64
+ }
65
+
66
+ approve(executionId: string, decision: ApprovalDecision): { ok: boolean; submissionId?: string } {
67
+ if (decision.decision === "allow_level") {
68
+ throw new Error("allow_level must be applied by the User Agent Host before resuming the Tool");
69
+ }
70
+ return this.options.transaction(() => {
71
+ const pending = this.options.approvals.find(executionId);
72
+ if (!pending) return { ok: true };
73
+ if (pending.status !== "pending" || !this.options.isSubmissionActive(pending.submissionId)) {
74
+ return { ok: false };
75
+ }
76
+ const status = decision.decision === "allow_once" ? "approved" : "rejected";
77
+ const reason = decision.decision === "deny" ? decision.reason : undefined;
78
+ const occurredAt = Date.now();
79
+ this.options.approvals.decide(executionId, status, occurredAt, reason);
80
+ this.options.telemetry.capture("approvalDecided", {
81
+ submissionId: pending.submissionId,
82
+ toolCallId: pending.toolCallId,
83
+ executionId,
84
+ decision: status,
85
+ ...(reason ? { reason } : {}),
86
+ occurredAt,
87
+ });
88
+ return { ok: true, submissionId: pending.submissionId };
89
+ });
90
+ }
91
+
92
+ cancelInteractions(submissionId: string, reason: string): void {
93
+ this.options.transaction(() => {
94
+ for (const id of this.options.interactions.listPendingForSubmission(submissionId)) {
95
+ const pending = this.options.interactions.find(id)!;
96
+ const occurredAt = Date.now();
97
+ this.options.interactions.settle(id, "cancelled", occurredAt);
98
+ this.options.telemetry.capture("interactionCancelled", {
99
+ submissionId,
100
+ toolCallId: pending.toolCallId,
101
+ interactionId: id,
102
+ reason,
103
+ occurredAt,
104
+ });
105
+ }
106
+ });
107
+ }
108
+
109
+ cancelForSubmission(submissionId: string, reason: string): void {
110
+ this.options.transaction(() => {
111
+ for (const id of this.options.approvals.listPendingForSubmission(submissionId)) {
112
+ const pending = this.options.approvals.find(id)!;
113
+ const occurredAt = Date.now();
114
+ this.options.approvals.decide(id, "rejected", occurredAt, reason);
115
+ this.options.telemetry.capture("approvalDecided", {
116
+ submissionId,
117
+ toolCallId: pending.toolCallId,
118
+ executionId: id,
119
+ decision: "cancelled",
120
+ reason,
121
+ occurredAt,
122
+ });
123
+ }
124
+ this.cancelInteractions(submissionId, reason);
125
+ });
126
+ }
127
+ }
@@ -0,0 +1 @@
1
+ export * from "./repository";
@@ -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
+ }