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

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 (141) hide show
  1. package/package.json +3 -3
  2. package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
  3. package/src/adapter/cloudflare/agent.ts +1592 -0
  4. package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
  5. package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
  6. package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
  7. package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
  8. package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
  9. package/src/{db → adapter/cloudflare}/schema.ts +16 -83
  10. package/src/adapter/cloudflare/subagent/definition.ts +63 -33
  11. package/src/adapter/cloudflare/subagent/runner.ts +174 -120
  12. package/src/adapter/cloudflare/subagent/tools.ts +13 -14
  13. package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
  14. package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
  15. package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
  16. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
  17. package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
  18. package/src/config/activation.ts +140 -0
  19. package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
  20. package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
  21. package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
  22. package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
  23. package/src/{runtime-definition.ts → config/definition.ts} +13 -13
  24. package/src/config/harness-runtime.ts +182 -0
  25. package/src/config/index.ts +14 -0
  26. package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
  27. package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
  28. package/src/{kernel → config}/profile.ts +7 -0
  29. package/src/{kernel → config}/public-contracts.ts +6 -6
  30. package/src/{kernel → config}/runtime-load.ts +1 -1
  31. package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
  32. package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
  33. package/src/context/extension/index.ts +3 -0
  34. package/src/context/index.ts +6 -0
  35. package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
  36. package/src/conversation/harness-resume.ts +288 -0
  37. package/src/conversation/harness-stream.ts +98 -0
  38. package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
  39. package/src/conversation/index.ts +6 -0
  40. package/src/{pi → conversation}/message/contract.ts +3 -0
  41. package/src/{pi → conversation}/message/conversion.ts +75 -0
  42. package/src/{pi → conversation}/message/index.ts +1 -0
  43. package/src/{pi → conversation}/message/model-input.ts +1 -1
  44. package/src/{pi → conversation}/message/projection.ts +76 -10
  45. package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
  46. package/src/conversation/state.ts +175 -0
  47. package/src/harness/index.ts +7 -0
  48. package/src/harness/session-harness.ts +481 -0
  49. package/src/harness/submission-operation.ts +49 -0
  50. package/src/index.ts +38 -43
  51. package/src/lib/mcp.ts +1 -1
  52. package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
  53. package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
  54. package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
  55. package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
  56. package/src/session/index.ts +39 -0
  57. package/src/submission/admission.ts +275 -0
  58. package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
  59. package/src/submission/delivery.ts +212 -0
  60. package/src/submission/finalization.ts +111 -0
  61. package/src/submission/index.ts +9 -0
  62. package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
  63. package/src/{db/submission.repo.ts → submission/repository.ts} +62 -119
  64. package/src/submission/steer-delivery.ts +167 -0
  65. package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
  66. package/src/telemetry/contract.ts +12 -0
  67. package/src/telemetry/coordinator.ts +65 -0
  68. package/src/telemetry/delivery.ts +2 -2
  69. package/src/telemetry/index.ts +5 -0
  70. package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
  71. package/src/telemetry/recorder.ts +1 -1
  72. package/src/telemetry/runtime-telemetry.ts +12 -0
  73. package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
  74. package/src/telemetry/turn-events.ts +279 -0
  75. package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
  76. package/src/tools/approval/index.ts +1 -0
  77. package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
  78. package/src/{pi/tool → tools}/base.ts +14 -30
  79. package/src/tools/compiler.ts +327 -0
  80. package/src/{pi/tool → tools}/core-host.ts +3 -3
  81. package/src/{pi/tool → tools}/core.ts +3 -3
  82. package/src/tools/decision-gate.ts +227 -0
  83. package/src/{pi/tool → tools}/gateway.ts +1 -1
  84. package/src/tools/human-decisions.ts +127 -0
  85. package/src/tools/index.ts +30 -0
  86. package/src/tools/interaction/index.ts +1 -0
  87. package/src/{pi/tool → tools}/mcp.ts +1 -1
  88. package/src/{pi/tool → tools}/schedule.ts +3 -3
  89. package/src/{pi/tool → tools}/skill.ts +4 -4
  90. package/src/{kernel → tools}/subagent-runtime.ts +1 -1
  91. package/src/{pi/tool → tools}/subagent.ts +4 -4
  92. package/src/tools/temporary-agent/approvals.ts +126 -0
  93. package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
  94. package/src/tools/temporary-agent/core.ts +53 -0
  95. package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
  96. package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
  97. package/src/{pi/tool → tools}/web-search/api.ts +1 -1
  98. package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
  99. package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
  100. package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
  101. package/src/db/milestone.repo.ts +0 -104
  102. package/src/db/settlement.repo.ts +0 -104
  103. package/src/kernel/approval-lifecycle.ts +0 -638
  104. package/src/kernel/interaction-lifecycle.ts +0 -433
  105. package/src/kernel/recoverable-chat-agent.ts +0 -1020
  106. package/src/kernel/state.ts +0 -91
  107. package/src/pi/assembly/index.ts +0 -19
  108. package/src/pi/runtime-adapter/execution.ts +0 -1163
  109. package/src/pi/runtime-adapter/index.ts +0 -293
  110. package/src/pi/runtime-adapter/recovery.ts +0 -982
  111. package/src/pi/session/index.ts +0 -24
  112. package/src/pi/tool/compiler.ts +0 -483
  113. package/src/pi/tool/index.ts +0 -38
  114. package/src/pi/turn/approval.ts +0 -181
  115. package/src/pi/turn/index.ts +0 -82
  116. package/src/pi/turn/interaction.ts +0 -181
  117. package/src/runtime.ts +0 -4760
  118. /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
  119. /package/src/{kernel → config}/degradation.ts +0 -0
  120. /package/src/{kernel → config}/runtime-config.ts +0 -0
  121. /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
  122. /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
  123. /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
  124. /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
  125. /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
  126. /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
  127. /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
  128. /package/src/{pi/tool → tools}/declared.ts +0 -0
  129. /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
  130. /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
  131. /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
  132. /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
  133. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
  134. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
  135. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
  136. /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
  137. /package/src/{pi/tool → tools}/time.ts +0 -0
  138. /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
  139. /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
  140. /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
  141. /package/src/{workspace-versioning.ts → tools/workspace-versioning.ts} +0 -0
@@ -0,0 +1,481 @@
1
+ import {
2
+ AgentHarness,
3
+ type AgentMessage,
4
+ type AgentHarness as PiAgentHarness,
5
+ type AgentHarnessOptions,
6
+ type AgentLane,
7
+ type DriveOutcome,
8
+ type DriveOptions,
9
+ type OpenOperation,
10
+ type OperationRequest,
11
+ } from "@earendil-works/pi-agent-core";
12
+ import type { Context } from "@earendil-works/pi-agent-core/harness/context";
13
+ import type { Entry, SettledAssistantMessage } from "@earendil-works/pi-agent-core/harness/session";
14
+ import {
15
+ isModelStreamStallMessage,
16
+ isOpenRouterServerToolTimeout,
17
+ isRecoverableAssistantError,
18
+ } from "../models";
19
+ import { projectToolOutputForModel } from "../context/memory/budget/gate";
20
+
21
+ export interface SessionHarnessLimits {
22
+ /** Complete model turns committed before one Worker instance releases. */
23
+ modelTurnsPerSlice?: number;
24
+ /** Complete working turns allowed before the no-tool final response. */
25
+ maxModelTurns?: number;
26
+ /** Maximum text turns allowed after the working or Tool failure budget. */
27
+ maxWrapUpModelTurns?: number;
28
+ wrapUpInstruction?: string;
29
+ }
30
+
31
+ export interface SessionHarnessOptions<TContext extends object | undefined>
32
+ extends AgentHarnessOptions<TContext> {
33
+ limits?: SessionHarnessLimits;
34
+ modelToolResultExemptions?: readonly string[];
35
+ /** Base provider Session identity, retained except after a server Tool timeout. */
36
+ modelSessionId?: string;
37
+ }
38
+
39
+ export type SessionHarnessRunResult = DriveOutcome | {
40
+ kind: "released";
41
+ operationId: string;
42
+ reason: "slice" | "budget" | "tool-failure" | "approval" | "interaction";
43
+ };
44
+
45
+ const DEFAULT_MODEL_TURNS_PER_SLICE = 30;
46
+ const DEFAULT_MAX_MODEL_TURNS = 300;
47
+ const DEFAULT_MAX_WRAP_UP_MODEL_TURNS = 2;
48
+ const RECOVERY_MAX_FAILURES = 5;
49
+ const STALL_MAX_FAILURES = 3;
50
+ const SERVER_TOOL_TIMEOUT_MAX_FAILURES = 2;
51
+ const DEFAULT_WRAP_UP_INSTRUCTION =
52
+ "Finish this request now without using tools. Summarize the completed work and any remaining limitation.";
53
+ const TOOL_FAILURE_WRAP_UP_INSTRUCTION =
54
+ "This turn reached its tool failure limit. Stop calling tools now. Reply with a final message that explains what failed and the exact next step the user should take. Do not start new work.";
55
+ const MAX_TOOL_RETRIES = 3;
56
+ const TURN_FAILURE_CEILING = 8;
57
+
58
+ function canonicalJson(value: unknown): string {
59
+ if (Array.isArray(value)) {
60
+ return `[${value.map(canonicalJson).join(",")}]`;
61
+ }
62
+ if (value !== null && typeof value === "object") {
63
+ return `{${Object.entries(value)
64
+ .filter(([, item]) => item !== undefined)
65
+ .sort(([left], [right]) => left.localeCompare(right))
66
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
67
+ .join(",")}}`;
68
+ }
69
+ return JSON.stringify(value) ?? "null";
70
+ }
71
+
72
+ function toolCallKey(toolName: string, args: unknown): string {
73
+ return `${toolName}\0${canonicalJson(args)}`;
74
+ }
75
+
76
+ function projectToolResultsForModel(
77
+ messages: readonly AgentMessage[],
78
+ exemptions: ReadonlySet<string>,
79
+ ): AgentMessage[] {
80
+ let changed = false;
81
+ const projected = messages.map((message) => {
82
+ if (
83
+ message.role !== "toolResult" ||
84
+ exemptions.has(message.toolName)
85
+ ) return message;
86
+ const payload = { content: message.content, details: message.details };
87
+ const next = projectToolOutputForModel(payload) as typeof payload;
88
+ if (next === payload) return message;
89
+ changed = true;
90
+ return { ...message, content: next.content, details: next.details };
91
+ });
92
+ return changed ? projected : [...messages];
93
+ }
94
+
95
+ function deferred<T>(): {
96
+ promise: Promise<T>;
97
+ resolve(value: T): void;
98
+ } {
99
+ let resolve!: (value: T) => void;
100
+ const promise = new Promise<T>((done) => {
101
+ resolve = done;
102
+ });
103
+ return { promise, resolve };
104
+ }
105
+
106
+ function positiveInteger(value: number | undefined, fallback: number): number {
107
+ const resolved = value ?? fallback;
108
+ if (!Number.isInteger(resolved) || resolved <= 0) {
109
+ throw new Error("Harness model turn limits must be positive integers");
110
+ }
111
+ return resolved;
112
+ }
113
+
114
+ /**
115
+ * The only adapter that owns a live Pi AgentHarness for a Session.
116
+ *
117
+ * Submission and Cloudflare lifecycle code may accept, drive, abort, watch, or
118
+ * close through this object. It deliberately does not know how Host config or
119
+ * business repositories are loaded.
120
+ */
121
+ export class SessionHarness<TContext extends object | undefined> {
122
+ private _openOperation: OpenOperation | null;
123
+ private readonly modelTurnsPerSlice: number;
124
+ private readonly maxModelTurns: number;
125
+ private readonly maxWrapUpModelTurns: number;
126
+ private readonly modelSessionId: string | undefined;
127
+ private readonly wrapUpInstruction: string;
128
+ private readonly configuredToolNames: string[];
129
+ private readonly modelToolResultExemptions: ReadonlySet<string>;
130
+ private readonly toolFailures = new Map<string, number>();
131
+ private totalToolFailures = 0;
132
+ private toolFailureWrapUp = false;
133
+ private toolFailureWrapUpAt: number | undefined;
134
+ private operationStartTip: string | null = null;
135
+ private turnsAtOpen = 0;
136
+ private wrappingUp = false;
137
+ private releaseReason:
138
+ | "slice"
139
+ | "budget"
140
+ | "tool-failure"
141
+ | "approval"
142
+ | "interaction"
143
+ | undefined;
144
+ private readonly releaseRequest = deferred<"approval" | "interaction">();
145
+ private readonly activeTools = new Set<string>();
146
+ private readonly toolsIdle = new Set<() => void>();
147
+
148
+ private constructor(
149
+ readonly harness: PiAgentHarness<TContext>,
150
+ readonly lane: AgentLane,
151
+ openOperation: OpenOperation | null,
152
+ options: SessionHarnessOptions<TContext>,
153
+ ) {
154
+ this._openOperation = openOperation;
155
+ this.modelTurnsPerSlice = positiveInteger(
156
+ options.limits?.modelTurnsPerSlice,
157
+ DEFAULT_MODEL_TURNS_PER_SLICE,
158
+ );
159
+ this.maxModelTurns = positiveInteger(
160
+ options.limits?.maxModelTurns,
161
+ DEFAULT_MAX_MODEL_TURNS,
162
+ );
163
+ this.maxWrapUpModelTurns = positiveInteger(
164
+ options.limits?.maxWrapUpModelTurns,
165
+ DEFAULT_MAX_WRAP_UP_MODEL_TURNS,
166
+ );
167
+ this.modelSessionId = options.modelSessionId;
168
+ this.wrapUpInstruction = options.limits?.wrapUpInstruction ??
169
+ DEFAULT_WRAP_UP_INSTRUCTION;
170
+ this.configuredToolNames = options.activeToolNames ??
171
+ (options.tools ?? []).map(({ name }) => name);
172
+ this.modelToolResultExemptions = new Set(
173
+ options.modelToolResultExemptions ?? [],
174
+ );
175
+ }
176
+
177
+ private async operationEntries(context: Context): Promise<Entry[]> {
178
+ const entries = await this.lane.findEntries({
179
+ ...(this.operationStartTip === null ? {} : { stopAtId: this.operationStartTip }),
180
+ type: "message",
181
+ order: "oldestFirst",
182
+ }, context);
183
+ return entries.filter(({ id }) => id !== this.operationStartTip);
184
+ }
185
+
186
+ private async restoreToolGovernance(context: Context): Promise<void> {
187
+ this.toolFailures.clear();
188
+ this.totalToolFailures = 0;
189
+ this.toolFailureWrapUp = false;
190
+ this.toolFailureWrapUpAt = undefined;
191
+ let turns = 0;
192
+ for (const entry of await this.operationEntries(context)) {
193
+ if (entry.type !== "message") continue;
194
+ const message = entry.message;
195
+ if (message.role === "assistant") {
196
+ turns += 1;
197
+ continue;
198
+ }
199
+ this.recordCanonicalToolOutcome(entry);
200
+ if (this.toolFailureWrapUp) this.toolFailureWrapUpAt ??= turns;
201
+ }
202
+ }
203
+
204
+ private recordCanonicalToolOutcome(entry: Entry): void {
205
+ if (entry.type !== "message" || entry.message.role !== "toolResult") return;
206
+ const message = entry.message;
207
+ if (entry.toolExecution?.status === "executed") {
208
+ this.recordToolOutcome(message.toolName, entry.toolExecution.args, message.isError);
209
+ } else if (entry.toolExecution?.status === "blocked" && entry.toolExecution.args !== undefined) {
210
+ const key = toolCallKey(message.toolName, entry.toolExecution.args);
211
+ if ((this.toolFailures.get(key) ?? 0) >= MAX_TOOL_RETRIES) this.toolFailureWrapUp = true;
212
+ }
213
+ }
214
+
215
+ private recordToolOutcome(toolName: string, args: unknown, isError: boolean): void {
216
+ const key = toolCallKey(toolName, args);
217
+ if (!isError) {
218
+ this.toolFailures.delete(key);
219
+ return;
220
+ }
221
+ this.toolFailures.set(key, (this.toolFailures.get(key) ?? 0) + 1);
222
+ this.totalToolFailures += 1;
223
+ if (this.totalToolFailures >= TURN_FAILURE_CEILING) this.toolFailureWrapUp = true;
224
+ }
225
+
226
+ private async shouldRetry(
227
+ message: SettledAssistantMessage,
228
+ context: Context,
229
+ recovery: boolean,
230
+ ): Promise<boolean> {
231
+ const stalled = isModelStreamStallMessage(message.errorMessage);
232
+ const serverToolTimeout = isOpenRouterServerToolTimeout(message);
233
+ if (!recovery && !stalled && !serverToolTimeout && !isRecoverableAssistantError(message)) return false;
234
+ const failures = 1 + (await this.operationEntries(context)).filter((entry) =>
235
+ entry.type === "message" && entry.message.role === "assistant" && entry.message.stopReason === "error"
236
+ ).length;
237
+ return failures < (stalled ? STALL_MAX_FAILURES
238
+ : serverToolTimeout ? SERVER_TOOL_TIMEOUT_MAX_FAILURES : RECOVERY_MAX_FAILURES);
239
+ }
240
+
241
+ private async shouldFinishRun(context: Context): Promise<boolean> {
242
+ const wrapUpAt = Math.min(this.maxModelTurns, this.toolFailureWrapUpAt ?? Infinity);
243
+ return await this.completedModelTurns(context) >= wrapUpAt + this.maxWrapUpModelTurns;
244
+ }
245
+
246
+ get openOperation(): OpenOperation | null {
247
+ return this._openOperation;
248
+ }
249
+
250
+ static async open<TContext extends object | undefined>(
251
+ options: SessionHarnessOptions<TContext>,
252
+ context: Context,
253
+ ): Promise<SessionHarness<TContext>> {
254
+ const {
255
+ limits: _limits,
256
+ modelToolResultExemptions: _modelToolResultExemptions,
257
+ modelSessionId: _modelSessionId,
258
+ ...harnessOptions
259
+ } = options;
260
+ let runtime!: SessionHarness<TContext>;
261
+ const { harness, open } = await AgentHarness.create({
262
+ ...harnessOptions,
263
+ retry: options.retry ?? { enabled: true, maxRetries: RECOVERY_MAX_FAILURES - 1, baseDelayMs: 1_000 },
264
+ recoveryPolicy: async (event, policyContext) =>
265
+ await runtime.shouldRetry(event.message, policyContext, event.recovery) &&
266
+ (options.recoveryPolicy === undefined || await options.recoveryPolicy(event, policyContext)),
267
+ shouldFinishRun: async (event, policyContext) =>
268
+ await runtime.shouldFinishRun(policyContext) ||
269
+ (options.shouldFinishRun !== undefined && await options.shouldFinishRun(event, policyContext)),
270
+ }, context);
271
+ const foreign = open.find((operation) => operation.lane !== "main");
272
+ if (foreign) {
273
+ await harness.close(context);
274
+ throw new Error(`Unexpected open Harness lane: ${foreign.lane}`);
275
+ }
276
+ if (open.length > 1) {
277
+ await harness.close(context);
278
+ throw new Error("Session has multiple open operations on the main lane");
279
+ }
280
+ runtime = new SessionHarness(
281
+ harness,
282
+ await harness.lane("main", context),
283
+ open[0] ?? null,
284
+ options,
285
+ );
286
+ await runtime.install(context);
287
+ return runtime;
288
+ }
289
+
290
+ private async install(context: Context): Promise<void> {
291
+ if (this._openOperation) {
292
+ const watch = await this.lane.watch(context);
293
+ this.operationStartTip = watch.snapshot.operation?.fromTipId ?? null;
294
+ watch.unsubscribe();
295
+ await this.restoreToolGovernance(context);
296
+ this.turnsAtOpen = await this.completedModelTurns(context);
297
+ if (this.turnsAtOpen >= this.maxModelTurns) {
298
+ this.wrappingUp = true;
299
+ }
300
+ if (this.wrappingUp || this.toolFailureWrapUp) {
301
+ await this.lane.setActiveTools([], context);
302
+ }
303
+ }
304
+ this.harness.hooks.on("before_request", async (event, requestContext) => {
305
+ if (event.step !== "assistant") return;
306
+ const resets = (await this.operationEntries(requestContext)).filter((entry) =>
307
+ entry.type === "message" && entry.message.role === "assistant" && isOpenRouterServerToolTimeout(entry.message)
308
+ ).length;
309
+ if (resets > 0) {
310
+ return { streamOptions: { sessionId: `${this.modelSessionId ?? event.runId}:server-tool-recovery:${resets}` } };
311
+ }
312
+ if (this.modelSessionId !== undefined) return { streamOptions: { sessionId: this.modelSessionId } };
313
+ });
314
+ this.harness.hooks.on("transform_context", (event) => {
315
+ const messages = projectToolResultsForModel(
316
+ event.messages,
317
+ this.modelToolResultExemptions,
318
+ );
319
+ const instruction = this.toolFailureWrapUp
320
+ ? TOOL_FAILURE_WRAP_UP_INSTRUCTION
321
+ : this.wrappingUp
322
+ ? this.wrapUpInstruction
323
+ : undefined;
324
+ return {
325
+ messages: instruction
326
+ ? [...messages, {
327
+ role: "user" as const,
328
+ content: instruction,
329
+ timestamp: 1,
330
+ }]
331
+ : messages,
332
+ };
333
+ });
334
+ this.harness.hooks.on("before_tool", (event) => {
335
+ if (this.toolFailureWrapUp) {
336
+ return {
337
+ block: {
338
+ reason: "This turn reached its tool failure limit. Stop calling tools now.",
339
+ terminate: true,
340
+ },
341
+ };
342
+ }
343
+ if (
344
+ (this.toolFailures.get(toolCallKey(event.toolName, event.args)) ?? 0) <
345
+ MAX_TOOL_RETRIES
346
+ ) return;
347
+ this.toolFailureWrapUp = true;
348
+ return {
349
+ block: {
350
+ reason:
351
+ `This exact ${event.toolName} call has already failed ${MAX_TOOL_RETRIES} times — change the input or approach instead of retrying it verbatim.`,
352
+ },
353
+ };
354
+ });
355
+ this.harness.events.on("entry_added", (event) => {
356
+ if (event.lane === this.lane.name) this.recordCanonicalToolOutcome(event.entry);
357
+ });
358
+ this.harness.events.on("turn_end", async (_event, eventContext) => {
359
+ const total = await this.completedModelTurns(eventContext);
360
+ if (this.toolFailureWrapUp && this.toolFailureWrapUpAt === undefined) {
361
+ this.toolFailureWrapUpAt = total;
362
+ this.releaseReason = "tool-failure";
363
+ } else if (this.wrappingUp || this.toolFailureWrapUp) {
364
+ return;
365
+ } else if (total >= this.maxModelTurns) {
366
+ this.releaseReason = "budget";
367
+ } else if (total - this.turnsAtOpen >= this.modelTurnsPerSlice) {
368
+ this.releaseReason = "slice";
369
+ } else {
370
+ return;
371
+ }
372
+ await this.harness.close(eventContext);
373
+ });
374
+ this.harness.events.on("tool_start", (event) => {
375
+ this.activeTools.add(event.toolCallId);
376
+ });
377
+ this.harness.events.on("tool_end", (event) => {
378
+ this.activeTools.delete(event.toolCallId);
379
+ if (this.activeTools.size === 0) {
380
+ for (const resolve of this.toolsIdle) resolve();
381
+ this.toolsIdle.clear();
382
+ }
383
+ });
384
+ }
385
+
386
+ /** Ask the active drive to detach after already-started parallel Tools settle. */
387
+ requestHumanRelease(reason: "approval" | "interaction"): void {
388
+ this.releaseRequest.resolve(reason);
389
+ }
390
+
391
+ private async waitForToolsToSettle(): Promise<void> {
392
+ if (this.activeTools.size === 0) return;
393
+ await new Promise<void>((resolve) => this.toolsIdle.add(resolve));
394
+ }
395
+
396
+ private async completedModelTurns(context: Context): Promise<number> {
397
+ return (await this.operationEntries(context)).filter((entry) =>
398
+ entry.type === "message" && entry.message.role === "assistant"
399
+ ).length;
400
+ }
401
+
402
+ async run(
403
+ request: OperationRequest | undefined,
404
+ context: Context,
405
+ driveOptions: Omit<DriveOptions, "operationId"> = {},
406
+ ): Promise<SessionHarnessRunResult> {
407
+ let operationId = this.openOperation?.operationId;
408
+ if (request) {
409
+ if (operationId) {
410
+ throw new Error(`Harness operation ${operationId} is already open`);
411
+ }
412
+ this.operationStartTip = await this.lane.getTipId(context);
413
+ this.turnsAtOpen = 0;
414
+ this.wrappingUp = false;
415
+ this.toolFailures.clear();
416
+ this.totalToolFailures = 0;
417
+ this.toolFailureWrapUp = false;
418
+ this.toolFailureWrapUpAt = undefined;
419
+ this.releaseReason = undefined;
420
+ await this.lane.setActiveTools(this.configuredToolNames, context);
421
+ const admission = await this.lane.accept(request, context);
422
+ if (!admission.ok) throw admission.error;
423
+ operationId = admission.value.operationId;
424
+ this._openOperation = {
425
+ lane: "main",
426
+ operationId,
427
+ kind: admission.value.kind,
428
+ startedAt: admission.value.startedAt,
429
+ };
430
+ }
431
+ if (!operationId) throw new Error("No Harness operation to drive");
432
+ try {
433
+ const driving = this.lane.drive({ ...driveOptions, operationId }, context);
434
+ const winner = await Promise.race([
435
+ driving.then((result) => ({ kind: "drive" as const, result })),
436
+ this.releaseRequest.promise.then((reason) => ({
437
+ kind: "release" as const,
438
+ reason,
439
+ })),
440
+ ]);
441
+ if (winner.kind === "release") {
442
+ this.releaseReason = winner.reason;
443
+ await this.waitForToolsToSettle();
444
+ await this.harness.close(context);
445
+ await driving.catch(() => undefined);
446
+ return { kind: "released", operationId, reason: winner.reason };
447
+ }
448
+ const result = winner.result;
449
+ if (!result.ok) throw result.error;
450
+ if (result.value.kind === "settled") {
451
+ this._openOperation = null;
452
+ }
453
+ return result.value;
454
+ } catch (error) {
455
+ if (
456
+ this.releaseReason && error instanceof Error &&
457
+ error.name === "HarnessClosed"
458
+ ) {
459
+ return { kind: "released", operationId, reason: this.releaseReason };
460
+ }
461
+ throw error;
462
+ }
463
+ }
464
+
465
+ async requestAbort(operationId: string, context: Context): Promise<boolean> {
466
+ const result = await this.lane.requestAbort(operationId, context);
467
+ if (!result.ok) throw result.error;
468
+ return result.value.newlyRequested;
469
+ }
470
+
471
+ close(context: Context): Promise<void> {
472
+ return this.harness.close(context);
473
+ }
474
+ }
475
+
476
+ export function openSessionHarness<TContext extends object | undefined>(
477
+ options: SessionHarnessOptions<TContext>,
478
+ context: Context,
479
+ ): Promise<SessionHarness<TContext>> {
480
+ return SessionHarness.open(options, context);
481
+ }
@@ -0,0 +1,49 @@
1
+ import type { OperationRequest, OperationResultRecord } from "@earendil-works/pi-agent-core";
2
+ import type { Context } from "@earendil-works/pi-agent-core/harness/context";
3
+ import type { PiCanonicalUserInput } from "../conversation/message/contract";
4
+ import type { StoredSubmission } from "../submission/repository";
5
+ import type { SessionHarness } from "./session-harness";
6
+
7
+ /** Recover the stable operation before accepting new input or navigating its branch. */
8
+ export async function prepareSubmissionOperation(
9
+ runtime: SessionHarness<object | undefined>,
10
+ submission: Pick<StoredSubmission, "submissionId" | "queuedInputJson" | "regenerateMessageId">,
11
+ regenerationTarget: (messageId: string) => string | null,
12
+ context: Context,
13
+ ): Promise<{ kind: "run"; request?: OperationRequest } | { kind: "settled"; outcome: OperationResultRecord }> {
14
+ const id = submission.submissionId;
15
+ const navigationId = `${id}:regenerate`;
16
+ if (runtime.openOperation?.operationId === navigationId) {
17
+ const navigation = await runtime.run(undefined, context);
18
+ if (navigation.kind !== "settled" || navigation.outcome.status !== "completed") {
19
+ throw new Error("Could not recover the regeneration branch navigation");
20
+ }
21
+ }
22
+ if (runtime.openOperation) {
23
+ if (runtime.openOperation.operationId !== id) {
24
+ throw new Error(`Submission ${id} does not own the open Harness operation`);
25
+ }
26
+ return { kind: "run" };
27
+ }
28
+ const watch = await runtime.lane.watch(context);
29
+ const previous = watch.snapshot.lastResult;
30
+ watch.unsubscribe();
31
+ if (previous?.operationId === id) return { kind: "settled", outcome: previous };
32
+ if (!submission.queuedInputJson) {
33
+ throw new Error(`Submission ${id} has no open Harness operation or queued input`);
34
+ }
35
+ const prompt = JSON.parse(submission.queuedInputJson) as PiCanonicalUserInput;
36
+ if (prompt.role !== "user") throw new Error("Queued SpringBrand user message is invalid");
37
+ if (submission.regenerateMessageId && previous?.operationId !== navigationId) {
38
+ const navigation = await runtime.run({
39
+ kind: "navigation",
40
+ operationId: navigationId,
41
+ targetId: regenerationTarget(submission.regenerateMessageId),
42
+ options: { summarize: false },
43
+ }, context);
44
+ if (navigation.kind !== "settled" || navigation.outcome.status !== "completed") {
45
+ throw new Error("Could not navigate to the regeneration branch");
46
+ }
47
+ }
48
+ return { kind: "run", request: { kind: "prompt", operationId: id, prompt } };
49
+ }
package/src/index.ts CHANGED
@@ -9,14 +9,13 @@
9
9
  * - `Session` 是本项目的聊天会话边界,由一个 `UniversalAgent` facet 承载。
10
10
  * - `Turn` 是 Session 中从一次提示被接纳到输出终态的一次执行。
11
11
  * - `Host` 是承载 Runtime 的 Cloudflare Agent,并负责提供生命周期、存储和网络边界。
12
- * - `Kernel` 是 Runtime 内部连接 Cloudflare Agent 生命周期与 Pi 执行的核心实现。
13
- * - `Pi` 是负责模型、Tool 循环和 canonical transcript 的执行层。
12
+ * - `Harness` 是 Pi 提供的唯一模型、Tool 与 canonical transcript 执行引擎。
13
+ * - `Cloudflare adapter` 只连接 Durable Object 生命周期、RPC 和 Harness。
14
14
  * - `Workspace` 是 Tool 可以读取或修改的受控文件系统视图。
15
15
  * - `Memory` 是可跨 Turn 读取或写入的长期上下文能力。
16
16
  * - `Tool` 是模型在 Turn 中可以调用的一项受控能力。
17
17
  * - `Tool Candidate` 是尚未完成治理和编译的内部 Tool 定义。
18
- * - `Tool Settlement` 是 Tool 调用完成后写回模型循环的统一结果。
19
- * - `Governance` 是对 Tool 可见性、审批和执行条件的集中约束。
18
+ * - `Tool decision` 是宿主持久化的审批或交互事实;Pi 仍保存执行位置与 Tool 结果。
20
19
  * - `Artifact` 是 Runtime 保存并对外引用的结构化执行产物。
21
20
  * - `Extension` 是 Pi 在装配期加载、并在 Turn 中参与执行的扩展模块。
22
21
  * - `SubAgent` 是由父 Agent 发起、但拥有独立执行上下文的子任务执行者。
@@ -38,21 +37,21 @@
38
37
  * Snapshot 不允许替换已选集合,但不承诺把每个 Port 的内部对象深冻结。
39
38
  *
40
39
  * 正常调用顺序是 `assemble → validate → freeze → guard → commit`。
41
- * Runtime 独占跨能力校验和原子提交。
40
+ * Config 装配独占跨能力校验,Harness 独占执行状态提交。
42
41
  *
43
42
  * @packageDocumentation
44
43
  */
45
- export * from "./kernel/bindings";
46
- export * from "./kernel/extensions";
47
- export * from "./kernel/profile";
48
- export * from "./kernel/receipts";
49
- export * from "./kernel/runtime-config";
44
+ export * from "./config/ports";
45
+ export * from "./context/extension/contracts";
46
+ export * from "./config/profile";
47
+ export * from "./submission/contracts";
48
+ export * from "./config/runtime-config";
50
49
  export * from "./telemetry";
51
- export type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
52
- export * from "./runtime-agent";
53
- export type { ToolInteractionResponsePreparation } from "./runtime";
54
- export * from "./kernel/state";
55
- export * from "./workspace-versioning";
50
+ export type { RuntimeAssemblyView } from "./config/assembly-view";
51
+ export * from "./adapter/cloudflare/define-agent";
52
+ export type { ToolInteractionResponsePreparation } from "./adapter/cloudflare/agent";
53
+ export * from "./conversation/state";
54
+ export * from "./tools/workspace-versioning";
56
55
  export * from "./lib/execution-level";
57
56
  export type {
58
57
  RuntimeAssemblyInput,
@@ -61,8 +60,8 @@ export type {
61
60
  RuntimeProfileContribution,
62
61
  RuntimeSkillContribution,
63
62
  RuntimeToolSurfacePolicy,
64
- } from "./runtime-assembler";
65
- export { assembleRuntimeSnapshot } from "./runtime-assembler";
63
+ } from "./config/assembler";
64
+ export { assembleRuntimeSnapshot } from "./config/assembler";
66
65
  export type {
67
66
  ResolvedConnectors,
68
67
  ResolvedResources,
@@ -71,7 +70,7 @@ export type {
71
70
  RuntimeSkillBodyTransform,
72
71
  RuntimeToolBindings,
73
72
  ToolAssemblyResult,
74
- } from "./runtime-definition";
73
+ } from "./config/definition";
75
74
  export type { ModelOption } from "./lib/model-catalog";
76
75
  export {
77
76
  assembleSubagentPrompt,
@@ -85,66 +84,62 @@ export type {
85
84
  } from "./lib/host-actions";
86
85
  export {
87
86
  AGENT_TYPES,
88
- } from "./layers/orchestration/subagents/agent-types/registry";
87
+ } from "./tools/subagent-types/registry";
89
88
  export type {
90
89
  AgentType,
91
- } from "./layers/orchestration/subagents/agent-types/contract";
92
- export { withProviderRetry } from "./pi/runtime-adapter";
93
- export { compilePiTools } from "./pi/tool";
90
+ } from "./tools/subagent-types/contract";
94
91
  export type {
95
- CompilePiToolsOptions,
96
92
  PiToolCandidate,
97
- SettledPiToolCall,
98
93
  ToolExposureMode,
99
- } from "./pi/tool";
100
- export { basePiToolCandidates } from "./pi/tool";
101
- export { createPiDeclaredToolCandidate } from "./pi/tool";
94
+ } from "./tools";
95
+ export { basePiToolCandidates } from "./tools";
96
+ export { createPiDeclaredToolCandidate } from "./tools";
102
97
  export type {
103
98
  PiDeclaredTool,
104
99
  PiDeclaredToolCallContext,
105
100
  PiDeclaredToolPolicy,
106
- } from "./pi/tool";
107
- export { memoryPiToolCandidate } from "./pi/tool";
108
- export { GET_TIME_TOOL_NAME, getTimePiToolCandidate } from "./pi/tool";
109
- export { schedulePiToolCandidates } from "./pi/tool";
101
+ } from "./tools";
102
+ export { memoryPiToolCandidate } from "./tools";
103
+ export { GET_TIME_TOOL_NAME, getTimePiToolCandidate } from "./tools";
104
+ export { schedulePiToolCandidates } from "./tools";
110
105
  export {
111
106
  sandboxPiToolCandidates,
112
107
  workspaceRevisionPiToolCandidate,
113
108
  workspacePiToolCandidates,
114
- } from "./pi/tool";
115
- export { subagentPiToolCandidates } from "./pi/tool";
116
- export { skillPiToolCandidates } from "./pi/tool";
117
- export type { PiSkillBinding } from "./pi/tool";
109
+ } from "./tools";
110
+ export { subagentPiToolCandidates } from "./tools";
111
+ export { skillPiToolCandidates } from "./tools";
112
+ export type { PiSkillBinding } from "./tools";
118
113
  export {
119
114
  BROWSER_EXECUTE_TOOL_NAME,
120
115
  browserExecutionPiToolCandidate,
121
116
  codeExecutionPiToolCandidate,
122
- } from "./pi/tool";
117
+ } from "./tools";
123
118
  export {
124
119
  createBrowserExecutionFactory,
125
120
  createWorkspaceCodeExecutionFactory,
126
- } from "./pi/tool";
121
+ } from "./tools";
127
122
  export {
128
123
  assemblePiExtensions,
129
124
  loadPiExtension,
130
- } from "./pi/assembly";
125
+ } from "./context";
131
126
  export type {
132
127
  TemporaryAgentApprovalDecision,
133
128
  TemporaryAgentApprovalRequest,
134
129
  TemporaryAgentLaunch,
135
130
  TemporaryAgentRequest,
136
131
  TemporaryAgentRunContext,
137
- } from "./layers/orchestration/temporary-agent/core";
138
- export { TEMPORARY_AGENT_LAUNCH_KEY } from "./layers/orchestration/temporary-agent/core";
132
+ } from "./tools/temporary-agent/core";
133
+ export { TEMPORARY_AGENT_LAUNCH_KEY } from "./tools/temporary-agent/core";
139
134
  export {
140
135
  temporaryAgentExtensionIsSafe,
141
136
  temporaryAgentToolAllowed,
142
- } from "./layers/orchestration/temporary-agent/runner";
137
+ } from "./tools/temporary-agent/runner";
143
138
  export {
144
139
  createTemporaryAgentWorkspace,
145
- } from "./layers/orchestration/temporary-agent/workspace";
140
+ } from "./tools/temporary-agent/workspace";
146
141
  export {
147
142
  createPiModels,
148
143
  modelRequestUrl,
149
144
  resolvePiModel,
150
- } from "./pi/runtime-adapter/models";
145
+ } from "./models";