@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.20
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.
- package/package.json +11 -9
- package/src/adapter/cloudflare/agent.ts +177 -122
- package/src/adapter/cloudflare/define-agent.ts +47 -26
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +25 -13
- package/src/adapter/cloudflare/subagent/runner.ts +43 -50
- package/src/adapter/cloudflare/subagent/tools.ts +4 -4
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +48 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +51 -13
- package/src/config/model-guidance.ts +20 -0
- package/src/config/pinning.ts +39 -5
- package/src/config/ports.ts +57 -7
- package/src/config/public-contracts.ts +2 -2
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-resume.ts +89 -238
- package/src/conversation/harness-stream.ts +87 -71
- package/src/conversation/history.ts +154 -152
- package/src/conversation/message/contract.ts +12 -0
- package/src/conversation/message/conversion.ts +3 -3
- package/src/conversation/message/projection.ts +61 -21
- package/src/conversation/session-entry.ts +29 -0
- package/src/conversation/state.ts +23 -2
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +255 -446
- package/src/harness/submission-operation.ts +15 -40
- package/src/index.ts +18 -8
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/model-protocol/openrouter.ts +41 -0
- package/src/models/index.ts +200 -35
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/openrouter/openai.ts +200 -0
- package/src/models/openrouter/protocol-response.ts +85 -0
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +382 -864
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +12 -1
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +36 -15
- package/src/submission/repository.ts +64 -25
- package/src/submission/steer-delivery.ts +32 -76
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +35 -25
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- package/src/tools/assistant/ask-user.ts +317 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/runtime/compiler.ts +246 -0
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1322 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/compiler.ts +0 -327
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
|
@@ -1,481 +1,290 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
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";
|
|
2
|
+
Harness, GenerationTask, ToolTask, AgentDoc,
|
|
3
|
+
} from "@earendil-works/pi-durable";
|
|
14
4
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
createRegistry, defineExtension, hook, section, watchEvents,
|
|
6
|
+
type ConversationRetryPolicy, type AgentEventStream, type CompactionPolicy, type Conversation,
|
|
7
|
+
type EntryId, type EntryRecord, type GenerationHooks, type Harness as NativeHarness,
|
|
8
|
+
type SettledSubmissionRecord, type Storage, type Submission,
|
|
9
|
+
type ToolHooks, type ToolRegistration,
|
|
10
|
+
} from "@earendil-works/pi-durable";
|
|
11
|
+
import type { Context } from "@earendil-works/chord";
|
|
12
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
13
|
+
import type { Api, Message, Model, Models, ModelThinkingLevel, UserMessage } from "@earendil-works/pi-ai";
|
|
14
|
+
import { RuntimeConversation } from "../session";
|
|
19
15
|
import { projectToolOutputForModel } from "../context/memory/budget/gate";
|
|
16
|
+
import type { ArtifactRef } from "../lib/artifacts";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_MAX_WRAP_UP_MODEL_TURNS, TOOL_FAILURE_WRAP_UP_INSTRUCTION, ModelRecovery, ToolGovernance,
|
|
19
|
+
artifactManifestMessage, artifactRefFromMessage,
|
|
20
|
+
} from "./governance";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_WRAP_UP_INSTRUCTION = "Finish this request now without using tools. Summarize the completed work and any remaining limitation.";
|
|
20
23
|
|
|
21
24
|
export interface SessionHarnessLimits {
|
|
22
|
-
/** Complete model turns committed before one Worker instance releases. */
|
|
23
25
|
modelTurnsPerSlice?: number;
|
|
24
|
-
/** Complete working turns allowed before the no-tool final response. */
|
|
25
26
|
maxModelTurns?: number;
|
|
26
|
-
/**
|
|
27
|
+
/** Text turns allowed after the working or Tool failure budget before the Submission is stopped. */
|
|
27
28
|
maxWrapUpModelTurns?: number;
|
|
28
29
|
wrapUpInstruction?: string;
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
export interface SessionHarnessOptions {
|
|
32
|
+
storage: Storage | Promise<Storage>;
|
|
33
|
+
models: Models;
|
|
34
|
+
model: Model<Api>;
|
|
35
|
+
thinkingLevel?: ModelThinkingLevel;
|
|
36
|
+
systemPrompt?: string;
|
|
37
|
+
tools?: readonly ToolRegistration<any, any>[];
|
|
38
|
+
activeToolNames?: readonly string[];
|
|
39
|
+
onDemandToolNames?: readonly string[];
|
|
34
40
|
modelToolResultExemptions?: readonly string[];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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];
|
|
41
|
+
toolsToActivate?: (name: string, result: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>, isError: boolean) => readonly string[];
|
|
42
|
+
compaction?: Partial<CompactionPolicy>;
|
|
43
|
+
streamOptions?: { maxRetries?: number };
|
|
44
|
+
retry?: Partial<ConversationRetryPolicy>;
|
|
45
|
+
toolExecution?: "parallel" | "sequential";
|
|
46
|
+
limits?: SessionHarnessLimits;
|
|
47
|
+
/** Per-Submission provider error classification and provider Session identity. */
|
|
48
|
+
recovery?: ModelRecovery;
|
|
49
|
+
beforeTool?: ToolHooks["beforeTool"];
|
|
50
|
+
afterResponse?: GenerationHooks["afterResponse"];
|
|
93
51
|
}
|
|
94
52
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const promise = new Promise<T>((done) => {
|
|
101
|
-
resolve = done;
|
|
102
|
-
});
|
|
103
|
-
return { promise, resolve };
|
|
53
|
+
/** Host terminal projection; native Submission records remain authoritative. */
|
|
54
|
+
export interface SessionRunOutcome {
|
|
55
|
+
status: "completed" | "failed" | "aborted" | "declined";
|
|
56
|
+
endedAt: number;
|
|
57
|
+
error?: { code: string; message: string };
|
|
104
58
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
59
|
+
export type SessionHarnessRunResult =
|
|
60
|
+
| { kind: "settled"; outcome: SessionRunOutcome }
|
|
61
|
+
| { kind: "released"; operationId: string; reason: "slice" | "approval" | "interaction" };
|
|
62
|
+
export interface SessionRunRequest { operationId: string; prompt?: UserMessage | string; }
|
|
63
|
+
export function submissionOutcome(record: SettledSubmissionRecord): SessionRunOutcome {
|
|
64
|
+
return {
|
|
65
|
+
status: record.status === "done" ? "completed" : record.reason === "aborted" ? "aborted" : "failed",
|
|
66
|
+
endedAt: Date.now(),
|
|
67
|
+
...(record.status === "unanswered" ? { error: { code: record.reason, message: typeof record.detail === "string" ? record.detail : record.reason } } : {}),
|
|
68
|
+
};
|
|
112
69
|
}
|
|
113
70
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
71
|
+
/** The host binds its ports; Pi owns generation, Tool tasks, waiting and recovery. */
|
|
72
|
+
export class SessionHarness {
|
|
73
|
+
private submission?: Submission;
|
|
74
|
+
private requestId?: string;
|
|
75
|
+
private release!: (reason: "approval" | "interaction" | "slice") => void;
|
|
135
76
|
private turnsAtOpen = 0;
|
|
136
|
-
private
|
|
137
|
-
private
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
77
|
+
private governance = new ToolGovernance();
|
|
78
|
+
private readonly artifactRefs = new Map<string, ArtifactRef>();
|
|
79
|
+
private wrapUpStarted = false;
|
|
80
|
+
/** Completed model turns of the current Submission. */
|
|
81
|
+
private ownedTurns = 0;
|
|
82
|
+
private readonly released = new Promise<"approval" | "interaction" | "slice">((resolve) => { this.release = resolve; });
|
|
148
83
|
private constructor(
|
|
149
|
-
readonly harness:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
options: SessionHarnessOptions
|
|
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
|
-
}
|
|
84
|
+
readonly harness: NativeHarness,
|
|
85
|
+
public conversation: Conversation,
|
|
86
|
+
readonly storage: Storage,
|
|
87
|
+
private readonly options: SessionHarnessOptions,
|
|
88
|
+
) {}
|
|
185
89
|
|
|
186
|
-
|
|
187
|
-
this.
|
|
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
|
-
}
|
|
90
|
+
get openOperation(): { operationId: string } | null {
|
|
91
|
+
return this.submission && this.requestId ? { operationId: this.requestId } : null;
|
|
202
92
|
}
|
|
203
93
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
94
|
+
static async open(options: SessionHarnessOptions, context: Context): Promise<SessionHarness> {
|
|
95
|
+
const storage = await options.storage;
|
|
96
|
+
const registry = createRegistry<ToolRegistration<any, any>>();
|
|
97
|
+
let runtime!: SessionHarness;
|
|
98
|
+
const exemptions = new Set(options.modelToolResultExemptions ?? []);
|
|
99
|
+
const extension = defineExtension({
|
|
100
|
+
name: "springbrand.runtime", tools: options.tools ?? [],
|
|
101
|
+
sections: [section("system", () => options.systemPrompt, { tag: false })],
|
|
102
|
+
hooks: [
|
|
103
|
+
hook(GenerationTask, {
|
|
104
|
+
beforeRequest: ({ messages }) => {
|
|
105
|
+
const projected = messages.map((message): Message => {
|
|
106
|
+
if (message.role !== "toolResult" || exemptions.has(message.toolName)) return message;
|
|
107
|
+
const next = projectToolOutputForModel({ content: message.content, details: message.details }) as Pick<typeof message, "content" | "details">;
|
|
108
|
+
return { ...message, ...next };
|
|
109
|
+
});
|
|
110
|
+
const manifest = artifactManifestMessage(runtime.artifactRefs);
|
|
111
|
+
const instruction = runtime.governance.wrapUp
|
|
112
|
+
? TOOL_FAILURE_WRAP_UP_INSTRUCTION
|
|
113
|
+
: runtime.wrapUpStarted ? options.limits?.wrapUpInstruction ?? DEFAULT_WRAP_UP_INSTRUCTION : undefined;
|
|
114
|
+
return { messages: [
|
|
115
|
+
...projected,
|
|
116
|
+
...(manifest ? [manifest] : []),
|
|
117
|
+
...(instruction ? [{ role: "user" as const, content: instruction, timestamp: 1 }] : []),
|
|
118
|
+
] };
|
|
119
|
+
},
|
|
120
|
+
afterResponse: async (message, api, hookContext) => {
|
|
121
|
+
options.recovery?.classify(message);
|
|
122
|
+
await options.afterResponse?.(message, api, hookContext);
|
|
123
|
+
},
|
|
124
|
+
afterTools: async (_assistant, _results, _api, hookContext) => {
|
|
125
|
+
const entries = await runtime.entries(hookContext);
|
|
126
|
+
const turns = runtime.ownedTurns = (await runtime.ownedEntries(entries, hookContext)).filter((entry) => entry.kind === "pi.assistant").length;
|
|
127
|
+
const total = entries.filter((entry) => entry.kind === "pi.assistant").length;
|
|
128
|
+
const maxTurns = options.limits?.maxModelTurns ?? 300;
|
|
129
|
+
const wrapUpAt = Math.min(maxTurns, runtime.governance.wrapUpAt ?? Infinity);
|
|
130
|
+
if (turns >= wrapUpAt + (options.limits?.maxWrapUpModelTurns ?? DEFAULT_MAX_WRAP_UP_MODEL_TURNS)) {
|
|
131
|
+
// The wrap-up is still calling Tools it no longer has; end the Submission instead of looping.
|
|
132
|
+
void runtime.conversation.abort(BACKGROUND_CONTEXT).catch(() => {});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (turns >= wrapUpAt && !runtime.wrapUpStarted) {
|
|
136
|
+
runtime.wrapUpStarted = true;
|
|
137
|
+
// Tools return on the next Submission's configure; the instruction is per request, never stored.
|
|
138
|
+
await runtime.conversation.configure({ tools: [] }, hookContext);
|
|
139
|
+
}
|
|
140
|
+
if (total - runtime.turnsAtOpen >= (options.limits?.modelTurnsPerSlice ?? 30)) runtime.release("slice");
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
hook(ToolTask, {
|
|
144
|
+
beforeTool: async (call, api, hookContext) => {
|
|
145
|
+
// Calls belong to the assistant turn after the last completed Tool round.
|
|
146
|
+
const block = runtime.governance.admit(call, runtime.ownedTurns + 1);
|
|
147
|
+
if (block !== undefined) return { block };
|
|
148
|
+
return options.beforeTool?.(call, api, hookContext);
|
|
149
|
+
},
|
|
150
|
+
afterTool: async (call, result) => {
|
|
151
|
+
runtime.governance.record(call, result.isError === true, runtime.ownedTurns + 1);
|
|
152
|
+
const ref = artifactRefFromMessage({ role: "toolResult", details: result.details });
|
|
153
|
+
if (ref) runtime.artifactRefs.set(ref.artifactId, ref);
|
|
154
|
+
const compiled = new Set(options.tools?.map((tool) => tool.name) ?? []);
|
|
155
|
+
const names = (options.toolsToActivate?.(call.name, { content: [...result.content ?? []], details: result.details }, result.isError === true) ?? [])
|
|
156
|
+
.filter((name) => compiled.has(name));
|
|
157
|
+
return names.length ? { ...result, control: { ...result.control, addTools: names } } : undefined;
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
registry.install(extension);
|
|
163
|
+
const harness = await Harness.open(storage, {
|
|
164
|
+
models: options.models, registry,
|
|
165
|
+
settings: {
|
|
166
|
+
stream: options.streamOptions, compaction: options.compaction, toolExecution: options.toolExecution,
|
|
167
|
+
// Read for every retry decision, after afterResponse has classified that response.
|
|
168
|
+
get retry() {
|
|
169
|
+
const retry = { maxRetries: 4, ...options.retry };
|
|
170
|
+
return options.recovery ? { ...retry, enabled: (retry.enabled ?? true) && options.recovery.retryAllowed } : retry;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}, context);
|
|
174
|
+
const pointer = await harness.snapshot(RuntimeConversation, context);
|
|
175
|
+
const conversation = pointer?.id ? await harness.conversation(pointer.id, context) : await harness.root(context);
|
|
176
|
+
if (!conversation) { await harness.close(context); throw new Error("The persisted Pi conversation is missing"); }
|
|
177
|
+
if (!pointer?.id) await harness.commit(async (tx) => { (await tx.doc(RuntimeConversation)).id = conversation.id; }, context);
|
|
178
|
+
runtime = new SessionHarness(harness, conversation, storage, options);
|
|
179
|
+
runtime.turnsAtOpen = (await runtime.entries(context)).filter((entry) => entry.kind === "pi.assistant").length;
|
|
180
|
+
const unfinished = (await harness.inspect(context)).submissions.filter((record) => record.conversationId === conversation.id && record.type === "input" && !record.requestId?.startsWith("steer:") && (record.status === "placed" || record.status === "queued"));
|
|
181
|
+
if (unfinished.length > 1) { await harness.close(context); throw new Error("Multiple host runs are active in one Pi conversation"); }
|
|
182
|
+
if (unfinished[0]) {
|
|
183
|
+
runtime.submission = await harness.submission(unfinished[0].id, context);
|
|
184
|
+
runtime.requestId = unfinished[0].requestId;
|
|
212
185
|
}
|
|
186
|
+
await runtime.restoreGovernance(context);
|
|
187
|
+
return runtime;
|
|
213
188
|
}
|
|
214
189
|
|
|
215
|
-
|
|
216
|
-
|
|
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;
|
|
190
|
+
async submissionFor(requestId: string, context: Context) {
|
|
191
|
+
return this.storage.submissionByRequest(this.conversation.id, requestId, context);
|
|
224
192
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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);
|
|
193
|
+
async entries(context: Context = BACKGROUND_CONTEXT): Promise<EntryRecord[]> {
|
|
194
|
+
const entries: EntryRecord[] = [];
|
|
195
|
+
let cursor;
|
|
196
|
+
do {
|
|
197
|
+
const page = await this.conversation.entries({ order: "ascending" }, 200, cursor, context);
|
|
198
|
+
entries.push(...page.items); cursor = page.next;
|
|
199
|
+
} while (cursor);
|
|
200
|
+
return entries;
|
|
239
201
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
return
|
|
202
|
+
/** Entries of the active Submission, from its input onward. */
|
|
203
|
+
async ownedEntries(entries: readonly EntryRecord[], context: Context): Promise<EntryRecord[]> {
|
|
204
|
+
const receipt = await this.submission?.status(context);
|
|
205
|
+
return receipt?.entry ? entries.filter((entry) => entry.id >= receipt.entry!) : [...entries];
|
|
244
206
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
207
|
+
private async restoreGovernance(context: Context): Promise<void> {
|
|
208
|
+
const entries = await this.entries(context);
|
|
209
|
+
this.artifactRefs.clear();
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
const message = entry.model?.[0];
|
|
212
|
+
const ref = message ? artifactRefFromMessage(message) : undefined;
|
|
213
|
+
if (ref) this.artifactRefs.set(ref.artifactId, ref);
|
|
214
|
+
}
|
|
215
|
+
this.governance = new ToolGovernance();
|
|
216
|
+
this.wrapUpStarted = false;
|
|
217
|
+
this.ownedTurns = 0;
|
|
218
|
+
if (!this.submission) return;
|
|
219
|
+
const owned = await this.ownedEntries(entries, context);
|
|
220
|
+
this.governance.restore(owned);
|
|
221
|
+
this.ownedTurns = owned.filter((entry) => entry.kind === "pi.assistant").length;
|
|
222
|
+
const wrapUpAt = Math.min(this.options.limits?.maxModelTurns ?? 300, this.governance.wrapUpAt ?? Infinity);
|
|
223
|
+
this.wrapUpStarted = this.ownedTurns >= wrapUpAt;
|
|
248
224
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
context
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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)),
|
|
225
|
+
watch(context: Context): Promise<AgentEventStream> { return watchEvents(this.harness, this.conversation.id, context); }
|
|
226
|
+
requestHumanRelease(reason: "approval" | "interaction"): void { this.release(reason); }
|
|
227
|
+
async fork(at: EntryId | null, context: Context): Promise<void> {
|
|
228
|
+
await this.conversation.abort(context);
|
|
229
|
+
this.submission = undefined;
|
|
230
|
+
this.requestId = undefined;
|
|
231
|
+
const created = await this.harness.commit(async (tx) => {
|
|
232
|
+
const fork = at === null ? await tx.createConversation({ ownership: { kind: "ownerless" } }) : await tx.forkConversation(this.conversation.id, at, { ownership: { kind: "ownerless" } });
|
|
233
|
+
(await tx.doc(RuntimeConversation)).id = fork.id;
|
|
234
|
+
return fork;
|
|
270
235
|
}, context);
|
|
271
|
-
|
|
272
|
-
|
|
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;
|
|
236
|
+
this.conversation = (await this.harness.conversation(created.id, context))!;
|
|
237
|
+
this.turnsAtOpen = (await this.entries(context)).filter((entry) => entry.kind === "pi.assistant").length;
|
|
288
238
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
239
|
+
async admit(request: SessionRunRequest, context: Context): Promise<void> {
|
|
240
|
+
if (this.submission && this.requestId !== request.operationId) throw new Error("A native Pi Submission is already active");
|
|
241
|
+
const existing = await this.submissionFor(request.operationId, context);
|
|
242
|
+
if (existing) {
|
|
243
|
+
this.requestId = request.operationId;
|
|
244
|
+
this.submission = await this.harness.submission(existing.id, context);
|
|
245
|
+
} else {
|
|
246
|
+
if (request.prompt === undefined) throw new Error("A new native Pi Submission needs input");
|
|
247
|
+
const names = new Set(this.options.activeToolNames ?? this.options.tools?.map((tool) => tool.name) ?? []);
|
|
248
|
+
const previous = await this.harness.snapshot(AgentDoc, this.conversation.id, context);
|
|
249
|
+
if (Array.isArray(previous?.tools)) for (const name of previous.tools) if (this.options.onDemandToolNames?.includes(name)) names.add(name);
|
|
250
|
+
await this.conversation.configure({ model: { provider: this.options.model.provider, modelId: this.options.model.id }, thinkingLevel: this.options.thinkingLevel ?? "off", tools: this.options.tools?.filter((tool) => names.has(tool.name)) ?? [],
|
|
251
|
+
// Earlier runtimes stored the wrap-up instruction in the agent; it applies to one Submission only.
|
|
252
|
+
instructions: null }, context);
|
|
253
|
+
this.requestId = request.operationId;
|
|
254
|
+
this.submission = await this.conversation.submit({ type: "input", requestId: request.operationId, content: typeof request.prompt === "string" ? request.prompt : request.prompt.content, whenBusy: "reject" }, context);
|
|
303
255
|
}
|
|
304
|
-
this.
|
|
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;
|
|
256
|
+
await this.restoreGovernance(context);
|
|
400
257
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
context
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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;
|
|
258
|
+
async run(request: SessionRunRequest | undefined, context: Context): Promise<SessionHarnessRunResult> {
|
|
259
|
+
if (request) await this.admit(request, context);
|
|
260
|
+
if (!this.submission || !this.requestId) throw new Error("No native Pi Submission to resume");
|
|
261
|
+
const waiting = this.submission.wait(context).then(async (record) => {
|
|
262
|
+
// One input can be answered while Pi is still working on a later steer.
|
|
263
|
+
await this.conversation.waitForIdle(context);
|
|
264
|
+
let cursor;
|
|
265
|
+
do {
|
|
266
|
+
const page = await this.storage.scanSubmissions({ conversationId: this.conversation.id, order: "descending" }, 200, cursor, context);
|
|
267
|
+
for (const next of page.items) {
|
|
268
|
+
if (next.id <= record.id) return record;
|
|
269
|
+
if (next.type === "input" && next.requestId?.startsWith("steer:") && (next.status === "done" || next.status === "unanswered")) return next as SettledSubmissionRecord;
|
|
270
|
+
}
|
|
271
|
+
cursor = page.next;
|
|
272
|
+
} while (cursor);
|
|
273
|
+
return record;
|
|
274
|
+
});
|
|
275
|
+
waiting.catch(() => {});
|
|
276
|
+
const result = await Promise.race([waiting.then((record) => ({ record })), this.released.then((reason) => ({ reason }))]);
|
|
277
|
+
if ("record" in result) {
|
|
278
|
+
this.submission = undefined;
|
|
279
|
+
return { kind: "settled", outcome: submissionOutcome(result.record) };
|
|
462
280
|
}
|
|
281
|
+
// A pending approval is before intent. Let already-started parallel effects commit before detaching.
|
|
282
|
+
const executing = (await this.harness.inspect(context)).tasks.filter(({ record }) => record.kind === "pi.tool" && record.state.status === "running" && (record.state.checkpoint as { phase?: string }).phase === "execute");
|
|
283
|
+
await Promise.all(executing.map(({ record }) => this.harness.waitForTask(record.id, context)));
|
|
284
|
+
await this.harness.close(BACKGROUND_CONTEXT);
|
|
285
|
+
return { kind: "released", operationId: this.requestId, reason: result.reason };
|
|
463
286
|
}
|
|
464
|
-
|
|
465
|
-
async
|
|
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);
|
|
287
|
+
async abort(context: Context): Promise<void> { await this.conversation.abort(context); }
|
|
288
|
+
async close(context: Context): Promise<void> { await this.harness.close(context); }
|
|
481
289
|
}
|
|
290
|
+
export const openSessionHarness = SessionHarness.open;
|