@springbrand/agent-runtime 0.3.0-alpha.16 → 0.3.0-alpha.18
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 +6 -4
- package/src/adapter/cloudflare/agent.ts +81 -123
- package/src/adapter/cloudflare/subagent/definition.ts +21 -9
- package/src/adapter/cloudflare/subagent/runner.ts +42 -49
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- package/src/config/harness-runtime.ts +28 -15
- package/src/config/model-guidance.ts +16 -17
- package/src/config/ports.ts +2 -0
- package/src/conversation/harness-resume.ts +69 -234
- package/src/conversation/harness-stream.ts +85 -70
- package/src/conversation/history.ts +153 -153
- package/src/conversation/message/contract.ts +12 -0
- package/src/conversation/session-entry.ts +29 -0
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +251 -607
- package/src/harness/submission-operation.ts +15 -40
- package/src/models/index.ts +14 -3
- package/src/models/openrouter/protocol-response.ts +36 -112
- package/src/session/cloudflare/migration.ts +382 -924
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/submission/finalization.ts +29 -13
- package/src/submission/repository.ts +1 -22
- package/src/submission/steer-delivery.ts +32 -76
- package/src/telemetry/turn-events.ts +34 -24
- package/src/tools/runtime/compiler.ts +32 -50
- package/src/tools/runtime/decision-gate.ts +5 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import type { ExecutionLevel } from "../lib/execution-level";
|
|
4
4
|
import type { RuntimeUnattendedExecutionGrant } from "./unattended";
|
|
5
5
|
|
|
@@ -446,27 +446,6 @@ export class SubmissionRepository {
|
|
|
446
446
|
`;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
/** Recover ownership from the matching Pi operation, using only its active branch. */
|
|
450
|
-
associateSessionEntries(
|
|
451
|
-
submissionId: string,
|
|
452
|
-
snapshot: Pick<LaneSnapshot, "operation" | "lastResult" | "tipId" | "transcript">,
|
|
453
|
-
): void {
|
|
454
|
-
const boundary = snapshot.operation?.id === submissionId
|
|
455
|
-
? { fromTipId: snapshot.operation.fromTipId, tipId: snapshot.tipId }
|
|
456
|
-
: snapshot.lastResult?.operationId === submissionId
|
|
457
|
-
? snapshot.lastResult
|
|
458
|
-
: undefined;
|
|
459
|
-
if (!boundary) return;
|
|
460
|
-
const start = boundary.fromTipId === null
|
|
461
|
-
? 0
|
|
462
|
-
: snapshot.transcript.findIndex(({ id }) => id === boundary.fromTipId) + 1;
|
|
463
|
-
const end = boundary.tipId === null
|
|
464
|
-
? snapshot.transcript.length
|
|
465
|
-
: snapshot.transcript.findIndex(({ id }) => id === boundary.tipId) + 1;
|
|
466
|
-
for (const entry of snapshot.transcript.slice(start, end || undefined)) {
|
|
467
|
-
this.linkSessionEntry(entry.id, submissionId);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
449
|
|
|
471
450
|
listSessionEntrySubmissionLinks(): Map<string, string> {
|
|
472
451
|
const rows = this.sql<{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SubmissionRecord } from "@earendil-works/pi-durable";
|
|
2
|
+
export type CancelQueuedResult = { kind: "cancelled" | "already_consumed" | "not_found" };
|
|
2
3
|
import type { UIMessage } from "ai";
|
|
3
4
|
import type { ConversationHistory } from "../conversation/history";
|
|
4
5
|
import type { SubmitMessageOptions } from "./admission";
|
|
@@ -12,7 +13,8 @@ interface PendingSteerDeliveryOptions {
|
|
|
12
13
|
steers: SteerRepository;
|
|
13
14
|
messageUi: MessageUiRepository;
|
|
14
15
|
transaction<T>(run: () => T): T;
|
|
15
|
-
enqueue(
|
|
16
|
+
enqueue(steer: PendingSteer): Promise<SubmissionRecord>;
|
|
17
|
+
find(messageId: string): Promise<SubmissionRecord | undefined>;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** Reconciles Host steer receipts with Pi queue identities, without owning execution. */
|
|
@@ -20,66 +22,27 @@ export class PendingSteerDelivery {
|
|
|
20
22
|
constructor(private readonly options: PendingSteerDeliveryOptions) {}
|
|
21
23
|
|
|
22
24
|
async queue(steer: PendingSteer): Promise<void> {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
this.link(steer, entryId);
|
|
25
|
+
const record = await this.options.enqueue(steer);
|
|
26
|
+
this.options.transaction(() => this.options.steers.linkHarnessEntry(steer.messageId, String(record.id)));
|
|
27
|
+
this.settleRecord(record);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const steer = this.options.steers.findByHarnessEntryId(entryId);
|
|
29
|
+
settleRecord(record: SubmissionRecord): boolean {
|
|
30
|
+
if (!record.requestId?.startsWith("steer:") || record.entry === undefined) return false;
|
|
31
|
+
const steer = this.options.steers.findByMessageId(record.requestId.slice(6));
|
|
32
32
|
if (!steer) return false;
|
|
33
33
|
this.options.transaction(() => {
|
|
34
|
-
this.
|
|
34
|
+
if (steer.uiMessageJson) this.options.messageUi.upsert(String(record.entry), steer.uiMessageJson);
|
|
35
35
|
this.options.steers.deleteByMessageId(steer.messageId);
|
|
36
36
|
});
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const transcriptIds = new Set(snapshot.transcript.map(({ id }) => id));
|
|
47
|
-
const linkedIds = new Set(pending.flatMap(({ harnessEntryId }) =>
|
|
48
|
-
harnessEntryId ? [harnessEntryId] : []
|
|
49
|
-
));
|
|
50
|
-
const unlinked = snapshot.queues.filter((entry) =>
|
|
51
|
-
entry.kind === "steer" && entry.type === "message" &&
|
|
52
|
-
!linkedIds.has(entry.entryId)
|
|
53
|
-
);
|
|
54
|
-
// Both durable queues preserve acceptance order; identical text is not identity.
|
|
55
|
-
for (const steer of pending) {
|
|
56
|
-
if (steer.harnessEntryId && transcriptIds.has(steer.harnessEntryId)) {
|
|
57
|
-
this.settle(steer.harnessEntryId);
|
|
58
|
-
} else if (steer.harnessEntryId && queuedIds.has(steer.harnessEntryId)) {
|
|
59
|
-
this.options.transaction(() =>
|
|
60
|
-
this.persistSidecar(steer, steer.harnessEntryId!)
|
|
61
|
-
);
|
|
62
|
-
} else {
|
|
63
|
-
const queued = !steer.harnessEntryId ? unlinked.shift() : undefined;
|
|
64
|
-
if (queued) this.link(steer, queued.entryId);
|
|
65
|
-
else await this.queue(steer);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private link(steer: PendingSteer, entryId: string): void {
|
|
71
|
-
this.options.transaction(() => {
|
|
72
|
-
if (steer.harnessEntryId && steer.harnessEntryId !== entryId) {
|
|
73
|
-
this.options.messageUi.delete(steer.harnessEntryId);
|
|
74
|
-
}
|
|
75
|
-
this.options.steers.linkHarnessEntry(steer.messageId, entryId);
|
|
76
|
-
this.persistSidecar(steer, entryId);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private persistSidecar(steer: PendingSteer, entryId: string): void {
|
|
81
|
-
if (steer.uiMessageJson) {
|
|
82
|
-
this.options.messageUi.upsert(entryId, steer.uiMessageJson);
|
|
39
|
+
async reconcile(submissionId: string): Promise<void> {
|
|
40
|
+
for (const steer of this.options.steers.listForSubmission(submissionId)) {
|
|
41
|
+
const existing = await this.options.find(steer.messageId);
|
|
42
|
+
if (existing) {
|
|
43
|
+
this.options.transaction(() => this.options.steers.linkHarnessEntry(steer.messageId, String(existing.id)));
|
|
44
|
+
this.settleRecord(existing);
|
|
45
|
+
} else await this.queue(steer);
|
|
83
46
|
}
|
|
84
47
|
}
|
|
85
48
|
}
|
|
@@ -109,20 +72,18 @@ export async function finishPendingSteers(options: {
|
|
|
109
72
|
: undefined;
|
|
110
73
|
const requestId = `${options.submissionId}:steer:${steer.steerId}`;
|
|
111
74
|
if (options.aborted) {
|
|
112
|
-
options.transaction(() => {
|
|
75
|
+
const owner = options.transaction(() => {
|
|
113
76
|
let handoff = options.submissions.findByRequestId(requestId);
|
|
114
77
|
if (handoff?.status === "pending") {
|
|
115
78
|
const reason = submissionAbortReason(options.submissions.find(options.submissionId)) ?? "Cancelled";
|
|
116
79
|
options.submissions.updateAbortReason(handoff.submissionId, reason);
|
|
117
80
|
handoff = options.cancelHandoff(handoff.submissionId, reason);
|
|
118
81
|
}
|
|
82
|
+
return handoff?.status === "aborted" || handoff?.status === "skipped" ? handoff.submissionId : options.submissionId;
|
|
83
|
+
});
|
|
84
|
+
await options.conversation.append(steer.messageId, message, { submissionId: owner, ...(userMessage ? { userMessage } : {}) });
|
|
85
|
+
options.transaction(() => {
|
|
119
86
|
if (steer.harnessEntryId) options.messageUi.delete(steer.harnessEntryId);
|
|
120
|
-
options.conversation.append(steer.messageId, message, {
|
|
121
|
-
submissionId: handoff?.status === "aborted" || handoff?.status === "skipped"
|
|
122
|
-
? handoff.submissionId
|
|
123
|
-
: options.submissionId,
|
|
124
|
-
...(userMessage ? { userMessage } : {}),
|
|
125
|
-
});
|
|
126
87
|
options.steers.deleteByMessageId(steer.messageId);
|
|
127
88
|
});
|
|
128
89
|
continue;
|
|
@@ -130,8 +91,7 @@ export async function finishPendingSteers(options: {
|
|
|
130
91
|
if (steer.harnessEntryId) {
|
|
131
92
|
const cancelled = await options.cancelQueued(steer.harnessEntryId);
|
|
132
93
|
if (cancelled) {
|
|
133
|
-
if (
|
|
134
|
-
if (cancelled.value.kind === "already_consumed") {
|
|
94
|
+
if (cancelled.kind === "already_consumed") {
|
|
135
95
|
options.transaction(() => options.steers.deleteByMessageId(steer.messageId));
|
|
136
96
|
continue;
|
|
137
97
|
}
|
|
@@ -148,20 +108,16 @@ export async function finishPendingSteers(options: {
|
|
|
148
108
|
...(userMessage ? { userMessage } : {}),
|
|
149
109
|
});
|
|
150
110
|
options.waitUntil(submitted.completion);
|
|
151
|
-
options.transaction(() => {
|
|
152
|
-
let
|
|
111
|
+
const handoff = options.transaction(() => {
|
|
112
|
+
let current = options.submissions.find(submitted.receipt.submissionId) ?? submitted.receipt;
|
|
153
113
|
const abortReason = submissionAbortReason(options.submissions.find(options.submissionId));
|
|
154
|
-
if (abortReason &&
|
|
155
|
-
options.submissions.updateAbortReason(
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
if (handoff.status === "skipped" || handoff.status === "aborted") {
|
|
159
|
-
options.conversation.append(steer.messageId, message, {
|
|
160
|
-
submissionId: handoff.submissionId,
|
|
161
|
-
...(userMessage ? { userMessage } : {}),
|
|
162
|
-
});
|
|
114
|
+
if (abortReason && current.status === "pending" && current.requestId === requestId) {
|
|
115
|
+
options.submissions.updateAbortReason(current.submissionId, abortReason);
|
|
116
|
+
current = options.cancelHandoff(current.submissionId, abortReason);
|
|
163
117
|
}
|
|
164
|
-
|
|
118
|
+
return current;
|
|
165
119
|
});
|
|
120
|
+
if (handoff.status === "skipped" || handoff.status === "aborted") await options.conversation.append(steer.messageId, message, { submissionId: handoff.submissionId, ...(userMessage ? { userMessage } : {}) });
|
|
121
|
+
options.transaction(() => options.steers.deleteByMessageId(steer.messageId));
|
|
166
122
|
}
|
|
167
123
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionHarness } from "../harness/session-harness";
|
|
2
|
+
import { projectSessionEntry, type ProjectedSessionEntry as Entry } from "../conversation/session-entry";
|
|
3
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
4
|
import type {
|
|
3
5
|
RuntimeLifecycleFact,
|
|
4
6
|
RuntimeModelUsageEvent,
|
|
@@ -87,10 +89,22 @@ export class RuntimeTurnEvents {
|
|
|
87
89
|
} }, submission.completedAt ?? Date.now());
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/** Pi summarizes through completeSimple; account only its successful, measured response. */
|
|
93
|
+
compactionGeneration(submissionId: string, event: import("../models").PiGenerationLifecycleEvent): void {
|
|
94
|
+
if (event.type !== "finished" || event.outcome !== "success" || !event.output || event.output.stopReason === "error" || event.output.stopReason === "aborted") return;
|
|
95
|
+
const response = event.output;
|
|
96
|
+
this.save({ type: "model-usage", event: {
|
|
97
|
+
eventId: `${submissionId}:compaction:${event.generationId}`, submissionId, runId: submissionId,
|
|
98
|
+
kind: "compaction", api: response.api, provider: response.provider, model: response.model,
|
|
99
|
+
...(response.responseModel ? { responseModel: response.responseModel } : {}), usage: response.usage,
|
|
100
|
+
} }, event.timestamp);
|
|
101
|
+
this.options.waitUntil(this.drain());
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
private recordEntry(submissionId: string, entry: Entry, model: RequestedModel): void {
|
|
91
105
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
92
106
|
this.save({ type: "model-usage", event: {
|
|
93
|
-
eventId: `${submissionId}:model:${entry.
|
|
107
|
+
eventId: `${submissionId}:model:${entry.identity}`,
|
|
94
108
|
submissionId,
|
|
95
109
|
runId: submissionId,
|
|
96
110
|
kind: "assistant",
|
|
@@ -107,7 +121,7 @@ export class RuntimeTurnEvents {
|
|
|
107
121
|
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
108
122
|
if (entry.usage) {
|
|
109
123
|
this.save({ type: "model-usage", event: {
|
|
110
|
-
eventId: `${submissionId}:compaction:${entry.
|
|
124
|
+
eventId: `${submissionId}:compaction:${entry.identity}`,
|
|
111
125
|
submissionId,
|
|
112
126
|
runId: submissionId,
|
|
113
127
|
kind: "compaction",
|
|
@@ -131,28 +145,24 @@ export class RuntimeTurnEvents {
|
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
/** Live observation and recovery use exactly the same committed-entry projection. */
|
|
134
|
-
observe(
|
|
135
|
-
harness.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
toolCallId: event.toolCallId,
|
|
143
|
-
toolName: event.toolName,
|
|
144
|
-
input: event.args,
|
|
145
|
-
});
|
|
148
|
+
async observe(runtime: SessionHarness, submissionId: string, model: RequestedModel): Promise<void> {
|
|
149
|
+
runtime.harness.subscribeCommits((publication) => {
|
|
150
|
+
for (const change of publication.changes) {
|
|
151
|
+
if (change.type === "entry" && change.value.conversationId === runtime.conversation.id) {
|
|
152
|
+
this.recordEntry(submissionId, projectSessionEntry(change.value), model);
|
|
153
|
+
this.options.waitUntil(this.drain());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
146
156
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
toolCallId: event.toolCallId,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
157
|
+
const watch = await runtime.watch(BACKGROUND_CONTEXT);
|
|
158
|
+
watch.start(async (events) => {
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
if (event.type === "tool_execution_start") this.options.telemetry.capture("toolStarted", { submissionId, toolCallId: event.toolCallId, toolName: event.toolName, input: event.args });
|
|
161
|
+
if (event.type === "tool_execution_end") {
|
|
162
|
+
const result = event.entry?.model?.[0];
|
|
163
|
+
this.options.telemetry.capture("toolFinished", { submissionId, toolCallId: event.toolCallId, toolName: event.toolName, outcome: result?.role === "toolResult" && !result.isError ? "completed" : "failed", output: result, outputBytes: JSON.stringify(result ?? {}).length });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
156
166
|
});
|
|
157
167
|
}
|
|
158
168
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ToolRegistration } from "@earendil-works/pi-durable";
|
|
3
|
+
import { awaitWithContext } from "@earendil-works/chord/context";
|
|
2
4
|
import { boundDurableToolOutput, spillDurableToolOutput } from "../../context/memory/budget/gate";
|
|
3
5
|
import { artifactId, serializeOutput, sha256Hex, type ArtifactRef, type SpillWorkspace } from "../../lib/artifacts";
|
|
4
6
|
import type { CompileHarnessToolsOptions, PiToolCandidate } from "./contracts";
|
|
@@ -134,29 +136,6 @@ async function durableResult(
|
|
|
134
136
|
);
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
async function untilAborted<T>(
|
|
138
|
-
signal: AbortSignal | undefined,
|
|
139
|
-
run: () => T | Promise<T>,
|
|
140
|
-
): Promise<T> {
|
|
141
|
-
// ponytail: 这会立即释放 Turn,但无法杀死不配合的进程内副作用;需要物理终止的 Tool 必须放进可销毁的隔离执行器。
|
|
142
|
-
signal?.throwIfAborted();
|
|
143
|
-
if (!signal) return run();
|
|
144
|
-
let onAbort!: () => void;
|
|
145
|
-
const aborted = new Promise<never>((_resolve, reject) => {
|
|
146
|
-
onAbort = () => reject(
|
|
147
|
-
signal.reason ?? new DOMException("Aborted", "AbortError"),
|
|
148
|
-
);
|
|
149
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
150
|
-
});
|
|
151
|
-
try {
|
|
152
|
-
return await Promise.race([run(), aborted]);
|
|
153
|
-
} finally {
|
|
154
|
-
signal.removeEventListener("abort", onAbort);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
139
|
/**
|
|
161
140
|
* Convert the configured candidate surface once at the AgentHarness boundary.
|
|
162
141
|
* Schema, argument preparation, progress, cancellation, replay classification,
|
|
@@ -165,7 +144,7 @@ async function untilAborted<T>(
|
|
|
165
144
|
export function compileHarnessTools(
|
|
166
145
|
candidates: readonly PiToolCandidate[],
|
|
167
146
|
options: CompileHarnessToolsOptions = {},
|
|
168
|
-
):
|
|
147
|
+
): ToolRegistration<any, any>[] {
|
|
169
148
|
return candidates.map((candidate) => {
|
|
170
149
|
const source = candidate.tool;
|
|
171
150
|
const tool = source.constrainedSampling === false
|
|
@@ -185,16 +164,21 @@ export function compileHarnessTools(
|
|
|
185
164
|
!candidate.requiredExecutionLevelForInput &&
|
|
186
165
|
!candidate.interaction
|
|
187
166
|
? "safe" as const
|
|
188
|
-
: "
|
|
167
|
+
: "unsafe" as const,
|
|
189
168
|
...(candidate.deferLoading ? { deferLoading: true as const } : {}),
|
|
190
|
-
async execute(
|
|
191
|
-
toolCallId
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
169
|
+
async execute(args, api, context) {
|
|
170
|
+
const toolCallId = api.callId;
|
|
171
|
+
// AgentTool progress is a replacing snapshot (web_search sends its accumulated text), but Pi output only
|
|
172
|
+
// appends and has no replace operation: append a grown snapshot's suffix, and start a new line otherwise.
|
|
173
|
+
let shown = "";
|
|
174
|
+
const onUpdate = (update: AgentToolResult<unknown>) => {
|
|
175
|
+
const text = update.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("");
|
|
176
|
+
if (text && text !== shown) {
|
|
177
|
+
api.output(text.startsWith(shown) ? text.slice(shown.length) : shown ? `\n${text}` : text);
|
|
178
|
+
shown = text;
|
|
179
|
+
}
|
|
180
|
+
if (update.details !== undefined) void api.details(update.details, context).catch(() => {});
|
|
181
|
+
};
|
|
198
182
|
const signal = context.abortSignal ?? new AbortController().signal;
|
|
199
183
|
let result: AgentToolResult<unknown>;
|
|
200
184
|
try {
|
|
@@ -208,19 +192,17 @@ export function compileHarnessTools(
|
|
|
208
192
|
args,
|
|
209
193
|
);
|
|
210
194
|
if (replay) return durableResult(replay, candidate, options.workspace);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
options.workspace,
|
|
223
|
-
),
|
|
195
|
+
signal.throwIfAborted();
|
|
196
|
+
const execution = candidate.confirmation
|
|
197
|
+
? (() => {
|
|
198
|
+
const confirmed = options.resolveConfirmation?.(candidate, toolCallId, args);
|
|
199
|
+
if (!confirmed) throw new Error("User confirmation is required");
|
|
200
|
+
return candidate.confirmation.execute(confirmed.prepared, { intentId: confirmed.intentId, signal });
|
|
201
|
+
})()
|
|
202
|
+
: source.execute(toolCallId, args, signal, (update) => onUpdate(update));
|
|
203
|
+
result = await awaitWithContext(
|
|
204
|
+
Promise.resolve(execution).then((result) => durableResult(result, candidate, options.workspace)),
|
|
205
|
+
context,
|
|
224
206
|
);
|
|
225
207
|
} catch (cause) {
|
|
226
208
|
const message = errorMessage(cause);
|
|
@@ -239,7 +221,7 @@ export function compileHarnessTools(
|
|
|
239
221
|
source: candidate.source,
|
|
240
222
|
});
|
|
241
223
|
if (cause instanceof UnattendedExecutionError) {
|
|
242
|
-
return { ...failed, terminate: true };
|
|
224
|
+
return { ...failed, isError: true, control: { terminate: true as const } };
|
|
243
225
|
}
|
|
244
226
|
throw cause instanceof Error && cause.message === message
|
|
245
227
|
? cause
|
|
@@ -257,8 +239,8 @@ export function compileHarnessTools(
|
|
|
257
239
|
summary: candidate.summary,
|
|
258
240
|
source: candidate.source,
|
|
259
241
|
});
|
|
260
|
-
return result;
|
|
242
|
+
return { ...result, ...(result.terminate ? { control: { terminate: true as const } } : {}) };
|
|
261
243
|
},
|
|
262
|
-
} satisfies
|
|
244
|
+
} satisfies ToolRegistration<any, any>;
|
|
263
245
|
});
|
|
264
246
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentToolResult
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { ExecutionLevel } from "../../lib/execution-level";
|
|
3
3
|
import {
|
|
4
4
|
EXECUTION_LEVELS,
|
|
@@ -86,7 +86,7 @@ export class HarnessDecisionGate {
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
readonly beforeTool
|
|
89
|
+
readonly beforeTool = async (event: { toolName: string; toolCallId: string; args: unknown }, context: import("@earendil-works/chord").Context): Promise<{ block: string } | undefined> => {
|
|
90
90
|
const candidate = this.candidates.get(event.toolName);
|
|
91
91
|
if (!candidate) return undefined;
|
|
92
92
|
let requiredExecutionLevel: ExecutionLevel;
|
|
@@ -94,7 +94,7 @@ export class HarnessDecisionGate {
|
|
|
94
94
|
requiredExecutionLevel = await this.resolveExecutionLevel(candidate, event.args);
|
|
95
95
|
} catch (error) {
|
|
96
96
|
if (!(error instanceof UnattendedExecutionError)) throw error;
|
|
97
|
-
return { block:
|
|
97
|
+
return { block: error.message };
|
|
98
98
|
}
|
|
99
99
|
const effectiveLevel = this.effectiveExecutionLevel();
|
|
100
100
|
const needsApproval = candidate.alwaysRequiresApproval ||
|
|
@@ -108,7 +108,7 @@ export class HarnessDecisionGate {
|
|
|
108
108
|
}, requiredExecutionLevel);
|
|
109
109
|
} catch (error) {
|
|
110
110
|
if (!(error instanceof UnattendedExecutionError)) throw error;
|
|
111
|
-
return { block:
|
|
111
|
+
return { block: error.message };
|
|
112
112
|
}
|
|
113
113
|
const existing = this.options.approvals.find(executionId);
|
|
114
114
|
if (existing && (existing.requestId !== this.options.submission.requestId ||
|
|
@@ -116,7 +116,7 @@ export class HarnessDecisionGate {
|
|
|
116
116
|
throw new Error(`Conflicting durable Tool approval: ${event.toolCallId}`);
|
|
117
117
|
}
|
|
118
118
|
if (existing?.status === "rejected") {
|
|
119
|
-
return { block:
|
|
119
|
+
return { block: existing.reason ?? "Tool execution denied by user" };
|
|
120
120
|
}
|
|
121
121
|
if (needsApproval && !unattendedAuthorized && existing?.status !== "approved") {
|
|
122
122
|
if (!existing) {
|