omk-agent-core 0.98.1 → 0.98.3
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/CHANGELOG.md +644 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -5
- package/dist/agent.js.map +1 -1
- package/dist/effects/effect-journal.d.ts +43 -0
- package/dist/effects/effect-journal.d.ts.map +1 -0
- package/dist/effects/effect-journal.js +186 -0
- package/dist/effects/effect-journal.js.map +1 -0
- package/dist/effects/effect-recovery.d.ts +70 -0
- package/dist/effects/effect-recovery.d.ts.map +1 -0
- package/dist/effects/effect-recovery.js +120 -0
- package/dist/effects/effect-recovery.js.map +1 -0
- package/dist/effects/effect-transitions.d.ts +34 -0
- package/dist/effects/effect-transitions.d.ts.map +1 -0
- package/dist/effects/effect-transitions.js +148 -0
- package/dist/effects/effect-transitions.js.map +1 -0
- package/dist/effects/effect-types.d.ts +135 -0
- package/dist/effects/effect-types.d.ts.map +1 -0
- package/dist/effects/effect-types.js +32 -0
- package/dist/effects/effect-types.js.map +1 -0
- package/dist/harness/abort-delivery.d.ts +26 -0
- package/dist/harness/abort-delivery.d.ts.map +1 -0
- package/dist/harness/abort-delivery.js +36 -0
- package/dist/harness/abort-delivery.js.map +1 -0
- package/dist/harness/agent-harness.d.ts +38 -6
- package/dist/harness/agent-harness.d.ts.map +1 -1
- package/dist/harness/agent-harness.js +327 -325
- package/dist/harness/agent-harness.js.map +1 -1
- package/dist/harness/canonical-digest.d.ts +32 -0
- package/dist/harness/canonical-digest.d.ts.map +1 -0
- package/dist/harness/canonical-digest.js +164 -0
- package/dist/harness/canonical-digest.js.map +1 -0
- package/dist/harness/compaction/operation.d.ts +7 -0
- package/dist/harness/compaction/operation.d.ts.map +1 -1
- package/dist/harness/compaction/operation.js.map +1 -1
- package/dist/harness/deferred-commands.d.ts +53 -0
- package/dist/harness/deferred-commands.d.ts.map +1 -0
- package/dist/harness/deferred-commands.js +96 -0
- package/dist/harness/deferred-commands.js.map +1 -0
- package/dist/harness/harness-session.d.ts +5 -58
- package/dist/harness/harness-session.d.ts.map +1 -1
- package/dist/harness/harness-session.js +15 -18
- package/dist/harness/harness-session.js.map +1 -1
- package/dist/harness/operation-lifecycle-controller.d.ts +68 -0
- package/dist/harness/operation-lifecycle-controller.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-controller.js +199 -0
- package/dist/harness/operation-lifecycle-controller.js.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts +19 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.js +201 -0
- package/dist/harness/operation-lifecycle-reducer.js.map +1 -0
- package/dist/harness/operation-lifecycle-types.d.ts +130 -0
- package/dist/harness/operation-lifecycle-types.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-types.js +34 -0
- package/dist/harness/operation-lifecycle-types.js.map +1 -0
- package/dist/harness/operation-outcome.d.ts +79 -0
- package/dist/harness/operation-outcome.d.ts.map +1 -0
- package/dist/harness/operation-outcome.js +167 -0
- package/dist/harness/operation-outcome.js.map +1 -0
- package/dist/harness/operation-trace-divergence.d.ts +60 -0
- package/dist/harness/operation-trace-divergence.d.ts.map +1 -0
- package/dist/harness/operation-trace-divergence.js +199 -0
- package/dist/harness/operation-trace-divergence.js.map +1 -0
- package/dist/harness/operation-trace.d.ts +134 -0
- package/dist/harness/operation-trace.d.ts.map +1 -0
- package/dist/harness/operation-trace.js +161 -0
- package/dist/harness/operation-trace.js.map +1 -0
- package/dist/harness/session-write-coordinator.d.ts +76 -0
- package/dist/harness/session-write-coordinator.d.ts.map +1 -0
- package/dist/harness/session-write-coordinator.js +126 -0
- package/dist/harness/session-write-coordinator.js.map +1 -0
- package/dist/harness/subscriber-fanout.d.ts +50 -0
- package/dist/harness/subscriber-fanout.d.ts.map +1 -0
- package/dist/harness/subscriber-fanout.js +92 -0
- package/dist/harness/subscriber-fanout.js.map +1 -0
- package/dist/harness/tree-navigation.d.ts +45 -0
- package/dist/harness/tree-navigation.d.ts.map +1 -0
- package/dist/harness/tree-navigation.js +59 -0
- package/dist/harness/tree-navigation.js.map +1 -0
- package/dist/harness/types.d.ts +34 -3
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/listener-delivery.d.ts +22 -0
- package/dist/listener-delivery.d.ts.map +1 -0
- package/dist/listener-delivery.js +36 -0
- package/dist/listener-delivery.js.map +1 -0
- package/package.json +4 -3
|
@@ -1,36 +1,30 @@
|
|
|
1
1
|
import { isContextOverflow, streamSimple, } from "omk-ai";
|
|
2
2
|
import { runAgentLoop, runAgentLoopContinue } from "../agent-loop.js";
|
|
3
|
-
import {
|
|
3
|
+
import { assertAbortAllowed, describeAbortDelivery } from "./abort-delivery.js";
|
|
4
|
+
import { collectEntriesForBranchSummary } from "./compaction/branch-summarization.js";
|
|
4
5
|
import { compact, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, prepareCompaction, shouldCompact, } from "./compaction/compaction.js";
|
|
6
|
+
import { DeferredCommandQueue } from "./deferred-commands.js";
|
|
5
7
|
import { HarnessSessionFacade } from "./harness-session.js";
|
|
6
8
|
import { convertToLlm, createFailureMessage, createUserMessage } from "./messages.js";
|
|
7
9
|
import { findDuplicateNames } from "./name-validation.js";
|
|
10
|
+
import { OperationLifecycleController, } from "./operation-lifecycle-controller.js";
|
|
11
|
+
import { PROMPT_FAMILY_KINDS, } from "./operation-lifecycle-types.js";
|
|
12
|
+
import { classifyAssistantOutcome, classifyAttemptFailure, classifyAttemptOutcome, classifyNavigateTreeOutcome, collectStepErrors, combineBoundaryErrors, normalizeHarnessError, resolveOperationFailure, resolveOperationOutcome, } from "./operation-outcome.js";
|
|
8
13
|
import { formatPromptTemplateInvocation } from "./prompt-templates.js";
|
|
14
|
+
import { uuidv7 } from "./session/uuid.js";
|
|
15
|
+
import { SessionWriteCoordinator } from "./session-write-coordinator.js";
|
|
9
16
|
import { formatSkillInvocation } from "./skills.js";
|
|
10
17
|
import { applyStreamOptionsPatch, cloneStreamOptions, mergeHeaders } from "./stream-options.js";
|
|
18
|
+
import { SubscriberFanout } from "./subscriber-fanout.js";
|
|
11
19
|
import { createSummarizationRetry } from "./summarization-retry.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
function normalizeHarnessError(error, fallbackCode) {
|
|
15
|
-
if (error instanceof AgentHarnessError)
|
|
16
|
-
return error;
|
|
17
|
-
const cause = toError(error);
|
|
18
|
-
if (cause instanceof SessionError)
|
|
19
|
-
return new AgentHarnessError("session", cause.message, cause);
|
|
20
|
-
if (cause instanceof CompactionError)
|
|
21
|
-
return new AgentHarnessError("compaction", cause.message, cause);
|
|
22
|
-
if (cause instanceof BranchSummaryError)
|
|
23
|
-
return new AgentHarnessError("branch_summary", cause.message, cause);
|
|
24
|
-
return new AgentHarnessError(fallbackCode, cause.message, cause);
|
|
25
|
-
}
|
|
20
|
+
import { resolveNavigationTarget, runBranchSummary } from "./tree-navigation.js";
|
|
21
|
+
import { AgentHarnessError, toError } from "./types.js";
|
|
26
22
|
export class AgentHarness {
|
|
27
23
|
env;
|
|
28
24
|
session;
|
|
29
25
|
sessionFacade;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
runPromise;
|
|
33
|
-
pendingSessionWrites = [];
|
|
26
|
+
lifecycle;
|
|
27
|
+
sessionWrites;
|
|
34
28
|
model;
|
|
35
29
|
thinkingLevel;
|
|
36
30
|
systemPrompt;
|
|
@@ -46,10 +40,17 @@ export class AgentHarness {
|
|
|
46
40
|
followUpQueueMode;
|
|
47
41
|
nextTurnQueue = [];
|
|
48
42
|
handlers = new Map();
|
|
43
|
+
subscribers = new SubscriberFanout();
|
|
44
|
+
deferredCommands = new DeferredCommandQueue(() => this.lifecycle.getSnapshot().tag === "idle");
|
|
49
45
|
constructor(options) {
|
|
50
46
|
this.env = options.env;
|
|
51
47
|
this.session = options.session;
|
|
52
|
-
this.
|
|
48
|
+
this.sessionWrites = new SessionWriteCoordinator(this.session);
|
|
49
|
+
this.lifecycle = new OperationLifecycleController({
|
|
50
|
+
createOperationId: () => uuidv7(),
|
|
51
|
+
now: () => Date.now(),
|
|
52
|
+
});
|
|
53
|
+
this.sessionFacade = new HarnessSessionFacade(this.session, () => this.currentPhase(), this.sessionWrites);
|
|
53
54
|
this.resources = options.resources ?? {};
|
|
54
55
|
this.streamOptions = cloneStreamOptions(options.streamOptions);
|
|
55
56
|
this.compactionSettings = { ...DEFAULT_COMPACTION_SETTINGS, ...options.compaction };
|
|
@@ -70,24 +71,15 @@ export class AgentHarness {
|
|
|
70
71
|
this.followUpQueueMode = options.followUpMode ?? "one-at-a-time";
|
|
71
72
|
}
|
|
72
73
|
async emitOwn(event, signal) {
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await listener(event, signal);
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
throw normalizeHarnessError(error, "hook");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
74
|
+
await this.emitAny(event, signal);
|
|
81
75
|
}
|
|
76
|
+
/** Subscriber fan-out; the self-wait barrier lives in `SubscriberFanout`. */
|
|
82
77
|
async emitAny(event, signal) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
throw normalizeHarnessError(error, "hook");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
78
|
+
await this.subscribers.emit(event, this.lifecycle.getCurrentOperation()?.operationId, signal);
|
|
79
|
+
}
|
|
80
|
+
/** Fail closed when an awaited listener tries to wait on its own operation. */
|
|
81
|
+
rejectCurrentOperationSelfWait(api) {
|
|
82
|
+
this.subscribers.assertNotSelfWait(api, this.lifecycle.getCurrentOperation()?.operationId);
|
|
91
83
|
}
|
|
92
84
|
async emitHook(event) {
|
|
93
85
|
const handlers = this.handlers.get(event.type);
|
|
@@ -156,23 +148,92 @@ export class AgentHarness {
|
|
|
156
148
|
nextTurn: [...this.nextTurnQueue],
|
|
157
149
|
});
|
|
158
150
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Facade write-gate vocabulary mapped from lifecycle state. `settling` maps
|
|
153
|
+
* to "idle": the queue is drained by the settlement finalizer first, and
|
|
154
|
+
* listener writes persist after it through the coordinator tail.
|
|
155
|
+
*/
|
|
156
|
+
currentPhase() {
|
|
157
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
158
|
+
if (snapshot.tag !== "active")
|
|
159
|
+
return "idle";
|
|
160
|
+
switch (snapshot.operation.kind) {
|
|
161
|
+
case "manual_compaction":
|
|
162
|
+
return "compaction";
|
|
163
|
+
case "tree_navigation":
|
|
164
|
+
return "branch_summary";
|
|
165
|
+
default:
|
|
166
|
+
return "turn";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Config writes persist immediately outside an active operation and queue during one. */
|
|
170
|
+
async persistConfigChange(write) {
|
|
171
|
+
if (this.lifecycle.getSnapshot().tag !== "active") {
|
|
172
|
+
await this.sessionWrites.persistAfterPending(write);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
this.sessionWrites.enqueue(write);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Single wrapper for every public operation: begin a lease, run the body,
|
|
180
|
+
* then settle exactly once. The final flush and the `settled` event happen
|
|
181
|
+
* inside the settling barrier; a finalizer failure never reports success.
|
|
182
|
+
*/
|
|
183
|
+
async runOperation(kind, fallbackCode, body, classifyResult) {
|
|
184
|
+
const lease = this.lifecycle.begin(kind);
|
|
185
|
+
let result;
|
|
186
|
+
let bodyError;
|
|
187
|
+
// Everything after a successful begin() runs inside one capture region. A
|
|
188
|
+
// throwing `operation_started` listener must not escape before settle(),
|
|
189
|
+
// or the lifecycle would stay active and wedge the harness at "busy".
|
|
190
|
+
try {
|
|
191
|
+
await this.emitOwn({ type: "operation_started", operation: lease.operation });
|
|
192
|
+
result = await body(lease);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
bodyError = error;
|
|
196
|
+
}
|
|
197
|
+
// The final flush precedes classification: a persistence failure after a
|
|
198
|
+
// provider success must never record or report a completed operation.
|
|
199
|
+
let flushError;
|
|
200
|
+
try {
|
|
201
|
+
await this.sessionWrites.flush();
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
flushError = error;
|
|
205
|
+
}
|
|
206
|
+
const outcome = resolveOperationOutcome({
|
|
207
|
+
signalAborted: lease.signal.aborted,
|
|
208
|
+
result,
|
|
209
|
+
bodyError,
|
|
210
|
+
flushError,
|
|
211
|
+
classifyResult,
|
|
212
|
+
fallbackCode,
|
|
163
213
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
}
|
|
214
|
+
let settleError;
|
|
215
|
+
try {
|
|
216
|
+
await this.lifecycle.settle(lease, outcome, async () => {
|
|
217
|
+
await this.emitOwn({
|
|
218
|
+
type: "settled",
|
|
219
|
+
nextTurnCount: this.nextTurnQueue.length,
|
|
220
|
+
operationId: lease.operation.operationId,
|
|
221
|
+
outcome,
|
|
222
|
+
attemptCount: this.lifecycle.getAttemptSummaries(lease).length,
|
|
223
|
+
}, lease.signal);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
settleError = error;
|
|
228
|
+
}
|
|
229
|
+
// Deferred commands registered from this operation's callbacks run now that
|
|
230
|
+
// the lifecycle is idle. Not awaited: this call's result must not depend on
|
|
231
|
+
// work a listener scheduled.
|
|
232
|
+
void this.deferredCommands.drain();
|
|
233
|
+
const failure = resolveOperationFailure({ bodyError, flushError, settleError, fallbackCode });
|
|
234
|
+
if (failure !== undefined)
|
|
235
|
+
throw failure;
|
|
236
|
+
return result;
|
|
176
237
|
}
|
|
177
238
|
async createTurnState() {
|
|
178
239
|
const context = await this.session.buildContext();
|
|
@@ -258,7 +319,7 @@ export class AgentHarness {
|
|
|
258
319
|
throw normalizeHarnessError(error, "hook");
|
|
259
320
|
}
|
|
260
321
|
}
|
|
261
|
-
createLoopConfig(getTurnState, setTurnState) {
|
|
322
|
+
createLoopConfig(getTurnState, setTurnState, lease) {
|
|
262
323
|
const turnState = getTurnState();
|
|
263
324
|
return {
|
|
264
325
|
model: turnState.model,
|
|
@@ -292,7 +353,13 @@ export class AgentHarness {
|
|
|
292
353
|
: undefined;
|
|
293
354
|
},
|
|
294
355
|
prepareNextTurn: async () => {
|
|
295
|
-
await this.
|
|
356
|
+
await this.sessionWrites.flush();
|
|
357
|
+
if (lease) {
|
|
358
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
359
|
+
if (snapshot.tag === "active" && snapshot.stage === "save_point") {
|
|
360
|
+
this.lifecycle.setStage(lease, "attempt_running");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
296
363
|
const nextTurnState = await this.createTurnState();
|
|
297
364
|
setTurnState(nextTurnState);
|
|
298
365
|
return {
|
|
@@ -316,40 +383,7 @@ export class AgentHarness {
|
|
|
316
383
|
if (missing.length > 0)
|
|
317
384
|
throw new AgentHarnessError("invalid_argument", `Unknown tool(s): ${missing.join(", ")}`);
|
|
318
385
|
}
|
|
319
|
-
async
|
|
320
|
-
while (this.pendingSessionWrites.length > 0) {
|
|
321
|
-
const write = this.pendingSessionWrites[0];
|
|
322
|
-
if (write.type === "message") {
|
|
323
|
-
await this.session.appendMessage(write.message);
|
|
324
|
-
}
|
|
325
|
-
else if (write.type === "model_change") {
|
|
326
|
-
await this.session.appendModelChange(write.provider, write.modelId);
|
|
327
|
-
}
|
|
328
|
-
else if (write.type === "thinking_level_change") {
|
|
329
|
-
await this.session.appendThinkingLevelChange(write.thinkingLevel);
|
|
330
|
-
}
|
|
331
|
-
else if (write.type === "active_tools_change") {
|
|
332
|
-
await this.session.appendActiveToolsChange(write.activeToolNames);
|
|
333
|
-
}
|
|
334
|
-
else if (write.type === "custom") {
|
|
335
|
-
await this.session.appendCustomEntry(write.customType, write.data);
|
|
336
|
-
}
|
|
337
|
-
else if (write.type === "custom_message") {
|
|
338
|
-
await this.session.appendCustomMessageEntry(write.customType, write.content, write.display, write.details);
|
|
339
|
-
}
|
|
340
|
-
else if (write.type === "label") {
|
|
341
|
-
await this.session.appendLabel(write.targetId, write.label);
|
|
342
|
-
}
|
|
343
|
-
else if (write.type === "session_info") {
|
|
344
|
-
await this.session.appendSessionName(write.name ?? "");
|
|
345
|
-
}
|
|
346
|
-
else if (write.type === "leaf") {
|
|
347
|
-
await this.session.getStorage().setLeafId(write.targetId);
|
|
348
|
-
}
|
|
349
|
-
this.pendingSessionWrites.shift();
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
async handleAgentEvent(event, signal) {
|
|
386
|
+
async handleAgentEvent(event, signal, lease) {
|
|
353
387
|
if (event.type === "message_end") {
|
|
354
388
|
await this.session.appendMessage(event.message);
|
|
355
389
|
await this.emitAny(event, signal);
|
|
@@ -363,39 +397,48 @@ export class AgentHarness {
|
|
|
363
397
|
catch (error) {
|
|
364
398
|
eventError = error;
|
|
365
399
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
400
|
+
// The flush runs even after a failing listener so accepted writes are
|
|
401
|
+
// not stranded; a failing flush must then report next to that listener
|
|
402
|
+
// error, not in place of it.
|
|
403
|
+
const hadPendingMutations = this.sessionWrites.hasPending();
|
|
404
|
+
let flushError;
|
|
405
|
+
try {
|
|
406
|
+
await this.sessionWrites.flush();
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
flushError = error;
|
|
410
|
+
}
|
|
411
|
+
if (lease) {
|
|
412
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
413
|
+
if (snapshot.tag === "active" && snapshot.stage === "attempt_running") {
|
|
414
|
+
this.lifecycle.setStage(lease, "save_point");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const failure = combineBoundaryErrors([eventError, flushError], "turn_end listener failed and the save-point flush failed", "hook");
|
|
418
|
+
if (failure !== undefined)
|
|
419
|
+
throw failure;
|
|
370
420
|
await this.emitOwn({ type: "save_point", hadPendingMutations });
|
|
371
421
|
return;
|
|
372
422
|
}
|
|
373
423
|
if (event.type === "agent_end") {
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
await this.flushPendingSessionWrites();
|
|
378
|
-
if (this.runAbortController && this.runAbortController.signal === signal) {
|
|
379
|
-
this.runAbortController = undefined;
|
|
380
|
-
}
|
|
381
|
-
this.phase = "idle";
|
|
424
|
+
// agent_end is an attempt event: flush its accepted writes, but lifecycle
|
|
425
|
+
// settlement and the settled event belong to OperationLease.settle().
|
|
426
|
+
await this.sessionWrites.flush();
|
|
382
427
|
await this.emitAny(event, signal);
|
|
383
|
-
await this.emitOwn({ type: "settled", nextTurnCount: this.nextTurnQueue.length }, signal);
|
|
384
428
|
return;
|
|
385
429
|
}
|
|
386
430
|
await this.emitAny(event, signal);
|
|
387
431
|
}
|
|
388
|
-
async emitRunFailure(model, error, aborted, signal, completedMessages) {
|
|
432
|
+
async emitRunFailure(model, error, aborted, signal, completedMessages, lease) {
|
|
389
433
|
const failureMessage = createFailureMessage(model, error, aborted);
|
|
390
434
|
const messages = [...completedMessages, failureMessage];
|
|
391
|
-
await this.handleAgentEvent({ type: "message_start", message: failureMessage }, signal);
|
|
392
|
-
await this.handleAgentEvent({ type: "message_end", message: failureMessage }, signal);
|
|
393
|
-
await this.handleAgentEvent({ type: "turn_end", message: failureMessage, toolResults: [] }, signal);
|
|
394
|
-
await this.handleAgentEvent({ type: "agent_end", messages }, signal);
|
|
435
|
+
await this.handleAgentEvent({ type: "message_start", message: failureMessage }, signal, lease);
|
|
436
|
+
await this.handleAgentEvent({ type: "message_end", message: failureMessage }, signal, lease);
|
|
437
|
+
await this.handleAgentEvent({ type: "turn_end", message: failureMessage, toolResults: [] }, signal, lease);
|
|
438
|
+
await this.handleAgentEvent({ type: "agent_end", messages }, signal, lease);
|
|
395
439
|
return messages;
|
|
396
440
|
}
|
|
397
|
-
async executeTurn(turnState, text, options) {
|
|
398
|
-
const runOwner = this.runPromise;
|
|
441
|
+
async executeTurn(lease, turnState, text, options) {
|
|
399
442
|
let messages = [createUserMessage(text, options?.images)];
|
|
400
443
|
if (this.nextTurnQueue.length > 0) {
|
|
401
444
|
const queuedMessages = this.nextTurnQueue.splice(0);
|
|
@@ -417,65 +460,99 @@ export class AgentHarness {
|
|
|
417
460
|
});
|
|
418
461
|
if (beforeResult?.messages)
|
|
419
462
|
messages = [...messages, ...beforeResult.messages];
|
|
420
|
-
const result = await this.executeAgentRun(turnState, this.createContext(turnState, beforeResult?.systemPrompt), messages);
|
|
421
|
-
return await this.recoverContextOverflow(
|
|
463
|
+
const result = await this.executeAgentRun(lease, "initial", turnState, this.createContext(turnState, beforeResult?.systemPrompt), messages);
|
|
464
|
+
return await this.recoverContextOverflow(lease, result);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Sole attempt boundary: begin, announce, run, classify, close, announce, flush.
|
|
468
|
+
*
|
|
469
|
+
* Once `beginAttempt()` succeeds the attempt is closed exactly once on every
|
|
470
|
+
* path, so `count(attempt_started) == count(attempt_finished)` holds even when
|
|
471
|
+
* the `attempt_started` observer throws. `attempt_finished` is emitted only
|
|
472
|
+
* after the attempt is already closed, so a throwing observer can fail the
|
|
473
|
+
* operation but can never reopen committed attempt state. The closing flush
|
|
474
|
+
* is not a `finally`: a `finally` that awaits a throwing flush would replace
|
|
475
|
+
* the body error, hiding the provider or listener failure from the audit trail.
|
|
476
|
+
*/
|
|
477
|
+
async runAttempt(lease, reason, body, classify) {
|
|
478
|
+
const attemptLease = this.lifecycle.beginAttempt(lease, reason);
|
|
479
|
+
let result;
|
|
480
|
+
let bodyError;
|
|
481
|
+
try {
|
|
482
|
+
await this.emitOwn({ type: "attempt_started", attempt: attemptLease.attempt }, lease.signal);
|
|
483
|
+
result = await body(attemptLease);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
bodyError = error;
|
|
487
|
+
}
|
|
488
|
+
const outcome = bodyError === undefined ? classify(result) : classifyAttemptFailure(bodyError);
|
|
489
|
+
this.lifecycle.finishAttempt(lease, attemptLease, outcome);
|
|
490
|
+
let observerError;
|
|
491
|
+
try {
|
|
492
|
+
await this.emitOwn({ type: "attempt_finished", summary: this.lifecycle.getAttemptSummary(lease, attemptLease) }, lease.signal);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
observerError = error;
|
|
496
|
+
}
|
|
497
|
+
let flushError;
|
|
498
|
+
try {
|
|
499
|
+
await this.sessionWrites.flush();
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
flushError = error;
|
|
503
|
+
}
|
|
504
|
+
const failure = combineBoundaryErrors([bodyError, observerError, flushError], "Attempt failed and its attempt_finished reporting or closing flush failed", "unknown");
|
|
505
|
+
if (failure !== undefined)
|
|
506
|
+
throw failure;
|
|
507
|
+
return result;
|
|
422
508
|
}
|
|
423
|
-
async executeAgentRun(turnState, context, initialMessages) {
|
|
509
|
+
async executeAgentRun(lease, reason, turnState, context, initialMessages) {
|
|
424
510
|
let activeTurnState = turnState;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
511
|
+
return await this.runAttempt(lease, reason, async (attemptLease) => {
|
|
512
|
+
const signal = attemptLease.signal;
|
|
513
|
+
const getTurnState = () => activeTurnState;
|
|
514
|
+
const setTurnState = (nextTurnState) => {
|
|
515
|
+
activeTurnState = nextTurnState;
|
|
516
|
+
};
|
|
517
|
+
const completedMessages = [];
|
|
518
|
+
const emit = async (event) => {
|
|
519
|
+
if (event.type === "message_end")
|
|
520
|
+
completedMessages.push(event.message);
|
|
521
|
+
await this.handleAgentEvent(event, signal, lease);
|
|
522
|
+
};
|
|
523
|
+
let newMessages;
|
|
438
524
|
try {
|
|
439
|
-
const loopConfig = this.createLoopConfig(getTurnState, setTurnState);
|
|
525
|
+
const loopConfig = this.createLoopConfig(getTurnState, setTurnState, lease);
|
|
440
526
|
const streamFn = this.createStreamFn(getTurnState);
|
|
441
|
-
|
|
442
|
-
? await runAgentLoop(initialMessages, context, loopConfig, emit,
|
|
443
|
-
: await runAgentLoopContinue(context, loopConfig, emit,
|
|
527
|
+
newMessages = initialMessages
|
|
528
|
+
? await runAgentLoop(initialMessages, context, loopConfig, emit, signal, streamFn)
|
|
529
|
+
: await runAgentLoopContinue(context, loopConfig, emit, signal, streamFn);
|
|
444
530
|
}
|
|
445
531
|
catch (error) {
|
|
446
532
|
try {
|
|
447
|
-
|
|
533
|
+
newMessages = await this.emitRunFailure(activeTurnState.model, error, signal.aborted, signal, completedMessages, lease);
|
|
448
534
|
}
|
|
449
535
|
catch (failureError) {
|
|
450
536
|
const cause = new AggregateError([toError(error), toError(failureError)], "Agent run failed and failure reporting failed");
|
|
451
537
|
throw new AgentHarnessError("unknown", cause.message, cause);
|
|
452
538
|
}
|
|
453
539
|
}
|
|
454
|
-
})();
|
|
455
|
-
try {
|
|
456
|
-
const newMessages = await runResultPromise;
|
|
457
540
|
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
458
541
|
const message = newMessages[i];
|
|
459
542
|
if (message.role === "assistant")
|
|
460
543
|
return message;
|
|
461
544
|
}
|
|
462
545
|
throw new AgentHarnessError("invalid_state", "AgentHarness prompt completed without an assistant message");
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
async recoverContextOverflow(message, runOwner) {
|
|
475
|
-
if (this.runPromise !== runOwner ||
|
|
476
|
-
this.phase !== "idle" ||
|
|
477
|
-
!this.compactionSettings.enabled ||
|
|
478
|
-
!isContextOverflow(message, this.model.contextWindow)) {
|
|
546
|
+
}, (message) => classifyAttemptOutcome(message, activeTurnState.model.contextWindow));
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* One-shot overflow recovery inside the originating operation. The lease is
|
|
550
|
+
* proof that this operation still owns the harness, so no run-ownership or
|
|
551
|
+
* phase re-check is needed; a strict lifecycle makes a newer operation
|
|
552
|
+
* starting mid-recovery impossible.
|
|
553
|
+
*/
|
|
554
|
+
async recoverContextOverflow(lease, message) {
|
|
555
|
+
if (!this.compactionSettings.enabled || !isContextOverflow(message, this.model.contextWindow)) {
|
|
479
556
|
return message;
|
|
480
557
|
}
|
|
481
558
|
if (!this.getApiKeyAndHeaders)
|
|
@@ -491,16 +568,15 @@ export class AgentHarness {
|
|
|
491
568
|
return message;
|
|
492
569
|
}
|
|
493
570
|
await this.session.moveTo(leaf.parentId);
|
|
494
|
-
this.
|
|
571
|
+
this.lifecycle.setStage(lease, "recovering_overflow");
|
|
495
572
|
try {
|
|
496
|
-
const compacted = await this.runCompaction({ automatic: true });
|
|
573
|
+
const compacted = await this.runCompaction({ automatic: true, signal: lease.signal });
|
|
497
574
|
if (!compacted) {
|
|
498
575
|
await this.session.moveTo(leafId);
|
|
499
|
-
this.phase = "idle";
|
|
500
576
|
return message;
|
|
501
577
|
}
|
|
502
578
|
const turnState = await this.createTurnState();
|
|
503
|
-
return await this.executeAgentRun(turnState, this.createContext(turnState));
|
|
579
|
+
return await this.executeAgentRun(lease, "context_overflow_recovery", turnState, this.createContext(turnState));
|
|
504
580
|
}
|
|
505
581
|
catch (error) {
|
|
506
582
|
await this.session.moveTo(leafId);
|
|
@@ -508,76 +584,49 @@ export class AgentHarness {
|
|
|
508
584
|
}
|
|
509
585
|
}
|
|
510
586
|
async prompt(text, options) {
|
|
511
|
-
|
|
512
|
-
throw new AgentHarnessError("busy", "AgentHarness is busy");
|
|
513
|
-
this.phase = "turn";
|
|
514
|
-
const { runPromise, finishRunPromise } = this.startRunPromise();
|
|
515
|
-
try {
|
|
587
|
+
return this.runOperation("prompt", "unknown", async (lease) => {
|
|
516
588
|
const turnState = await this.createTurnState();
|
|
517
|
-
return await this.executeTurn(turnState, text, options);
|
|
518
|
-
}
|
|
519
|
-
catch (error) {
|
|
520
|
-
// Only reset the phase if this call still owns the run; a listener may
|
|
521
|
-
// have started the next run while this one was unwinding.
|
|
522
|
-
if (this.runPromise === runPromise)
|
|
523
|
-
this.phase = "idle";
|
|
524
|
-
throw normalizeHarnessError(error, "unknown");
|
|
525
|
-
}
|
|
526
|
-
finally {
|
|
527
|
-
finishRunPromise();
|
|
528
|
-
}
|
|
589
|
+
return await this.executeTurn(lease, turnState, text, options);
|
|
590
|
+
}, classifyAssistantOutcome);
|
|
529
591
|
}
|
|
530
592
|
async skill(name, additionalInstructions) {
|
|
531
|
-
|
|
532
|
-
throw new AgentHarnessError("busy", "AgentHarness is busy");
|
|
533
|
-
this.phase = "turn";
|
|
534
|
-
const { runPromise, finishRunPromise } = this.startRunPromise();
|
|
535
|
-
try {
|
|
593
|
+
return this.runOperation("skill", "unknown", async (lease) => {
|
|
536
594
|
const turnState = await this.createTurnState();
|
|
537
595
|
const skill = (turnState.resources.skills ?? []).find((candidate) => candidate.name === name);
|
|
538
596
|
if (!skill)
|
|
539
597
|
throw new AgentHarnessError("invalid_argument", `Unknown skill: ${name}`);
|
|
540
|
-
return await this.executeTurn(turnState, formatSkillInvocation(skill, additionalInstructions));
|
|
541
|
-
}
|
|
542
|
-
catch (error) {
|
|
543
|
-
if (this.runPromise === runPromise)
|
|
544
|
-
this.phase = "idle";
|
|
545
|
-
throw normalizeHarnessError(error, "unknown");
|
|
546
|
-
}
|
|
547
|
-
finally {
|
|
548
|
-
finishRunPromise();
|
|
549
|
-
}
|
|
598
|
+
return await this.executeTurn(lease, turnState, formatSkillInvocation(skill, additionalInstructions));
|
|
599
|
+
}, classifyAssistantOutcome);
|
|
550
600
|
}
|
|
551
601
|
async promptFromTemplate(name, args = []) {
|
|
552
|
-
|
|
553
|
-
throw new AgentHarnessError("busy", "AgentHarness is busy");
|
|
554
|
-
this.phase = "turn";
|
|
555
|
-
const { runPromise, finishRunPromise } = this.startRunPromise();
|
|
556
|
-
try {
|
|
602
|
+
return this.runOperation("prompt_template", "unknown", async (lease) => {
|
|
557
603
|
const turnState = await this.createTurnState();
|
|
558
604
|
const template = (turnState.resources.promptTemplates ?? []).find((candidate) => candidate.name === name);
|
|
559
605
|
if (!template)
|
|
560
606
|
throw new AgentHarnessError("invalid_argument", `Unknown prompt template: ${name}`);
|
|
561
|
-
return await this.executeTurn(turnState, formatPromptTemplateInvocation(template, args));
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
607
|
+
return await this.executeTurn(lease, turnState, formatPromptTemplateInvocation(template, args));
|
|
608
|
+
}, classifyAssistantOutcome);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Steering and follow-up input is consumed only by a running agent attempt.
|
|
612
|
+
* A structural operation (`compact`, `navigateTree`) runs none, so accepting
|
|
613
|
+
* input there would silently inject it into an unrelated later prompt.
|
|
614
|
+
*/
|
|
615
|
+
expectQueueConsumer(action) {
|
|
616
|
+
const snapshot = this.lifecycle.getSnapshot();
|
|
617
|
+
if (snapshot.tag !== "active")
|
|
618
|
+
throw new AgentHarnessError("invalid_state", `Cannot ${action} while idle`);
|
|
619
|
+
if (!PROMPT_FAMILY_KINDS.includes(snapshot.operation.kind)) {
|
|
620
|
+
throw new AgentHarnessError("invalid_state", `Cannot ${action} during ${snapshot.operation.kind}: no agent attempt can consume it`);
|
|
570
621
|
}
|
|
571
622
|
}
|
|
572
623
|
async steer(text, options) {
|
|
573
|
-
|
|
574
|
-
throw new AgentHarnessError("invalid_state", "Cannot steer while idle");
|
|
624
|
+
this.expectQueueConsumer("steer");
|
|
575
625
|
this.steerQueue.push(createUserMessage(text, options?.images));
|
|
576
626
|
await this.emitQueueUpdate();
|
|
577
627
|
}
|
|
578
628
|
async followUp(text, options) {
|
|
579
|
-
|
|
580
|
-
throw new AgentHarnessError("invalid_state", "Cannot follow up while idle");
|
|
629
|
+
this.expectQueueConsumer("follow up");
|
|
581
630
|
this.followUpQueue.push(createUserMessage(text, options?.images));
|
|
582
631
|
await this.emitQueueUpdate();
|
|
583
632
|
}
|
|
@@ -629,6 +678,7 @@ export class AgentHarness {
|
|
|
629
678
|
if (!compactResult.ok)
|
|
630
679
|
throw compactResult.error;
|
|
631
680
|
const result = compactResult.value;
|
|
681
|
+
options.beforeCommit?.();
|
|
632
682
|
const entryId = await this.session.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, provided !== undefined);
|
|
633
683
|
const entry = await this.session.getEntry(entryId);
|
|
634
684
|
if (entry?.type === "compaction") {
|
|
@@ -647,28 +697,26 @@ export class AgentHarness {
|
|
|
647
697
|
return { ...context, messages: convertToLlm(persisted.messages) };
|
|
648
698
|
}
|
|
649
699
|
async compact(customInstructions) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
700
|
+
return this.runOperation("manual_compaction", "compaction", async (lease) => {
|
|
701
|
+
this.lifecycle.setStage(lease, "structural_running");
|
|
702
|
+
const result = await this.runCompaction({
|
|
703
|
+
automatic: false,
|
|
704
|
+
customInstructions,
|
|
705
|
+
beforeCommit: () => {
|
|
706
|
+
this.lifecycle.setStage(lease, "committing");
|
|
707
|
+
},
|
|
708
|
+
});
|
|
655
709
|
if (!result)
|
|
656
710
|
throw new AgentHarnessError("compaction", "Nothing to compact");
|
|
657
711
|
return result;
|
|
658
|
-
}
|
|
659
|
-
catch (error) {
|
|
660
|
-
throw normalizeHarnessError(error, "compaction");
|
|
661
|
-
}
|
|
662
|
-
finally {
|
|
663
|
-
this.phase = "idle";
|
|
664
|
-
}
|
|
712
|
+
});
|
|
665
713
|
}
|
|
666
714
|
async navigateTree(targetId, options) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
this.phase = "branch_summary";
|
|
670
|
-
try {
|
|
715
|
+
return this.runOperation("tree_navigation", "branch_summary", async (lease) => {
|
|
716
|
+
this.lifecycle.setStage(lease, "structural_running");
|
|
671
717
|
const oldLeafId = await this.session.getLeafId();
|
|
718
|
+
// No-op navigation mutates nothing, so it completes without ever
|
|
719
|
+
// entering the `committing` stage.
|
|
672
720
|
if (oldLeafId === targetId)
|
|
673
721
|
return { cancelled: false };
|
|
674
722
|
const targetEntry = await this.session.getEntry(targetId);
|
|
@@ -699,52 +747,24 @@ export class AgentHarness {
|
|
|
699
747
|
const auth = await this.getApiKeyAndHeaders?.(model);
|
|
700
748
|
if (!auth)
|
|
701
749
|
throw new AgentHarnessError("auth", "No auth available for branch summary");
|
|
702
|
-
const branchSummary = await
|
|
750
|
+
const branchSummary = await runBranchSummary({
|
|
751
|
+
entries,
|
|
703
752
|
model,
|
|
704
753
|
apiKey: auth.apiKey,
|
|
705
754
|
headers: auth.headers,
|
|
706
|
-
signal: new AbortController().signal,
|
|
707
755
|
customInstructions: hookResult?.customInstructions ?? options?.customInstructions,
|
|
708
756
|
replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,
|
|
709
|
-
|
|
757
|
+
summarizationRetry: this.streamOptions.summarizationRetry,
|
|
758
|
+
emit: (event) => this.emitOwn(event),
|
|
710
759
|
});
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
modifiedFiles: branchSummary.value.modifiedFiles,
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
let editorText;
|
|
723
|
-
let newLeafId;
|
|
724
|
-
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
725
|
-
newLeafId = targetEntry.parentId;
|
|
726
|
-
const content = targetEntry.message.content;
|
|
727
|
-
editorText =
|
|
728
|
-
typeof content === "string"
|
|
729
|
-
? content
|
|
730
|
-
: content
|
|
731
|
-
.filter((c) => c.type === "text")
|
|
732
|
-
.map((c) => c.text)
|
|
733
|
-
.join("");
|
|
734
|
-
}
|
|
735
|
-
else if (targetEntry.type === "custom_message") {
|
|
736
|
-
newLeafId = targetEntry.parentId;
|
|
737
|
-
editorText =
|
|
738
|
-
typeof targetEntry.content === "string"
|
|
739
|
-
? targetEntry.content
|
|
740
|
-
: targetEntry.content
|
|
741
|
-
.filter((c) => c.type === "text")
|
|
742
|
-
.map((c) => c.text)
|
|
743
|
-
.join("");
|
|
744
|
-
}
|
|
745
|
-
else {
|
|
746
|
-
newLeafId = targetId;
|
|
747
|
-
}
|
|
760
|
+
if (branchSummary.cancelled)
|
|
761
|
+
return { cancelled: true };
|
|
762
|
+
summaryText = branchSummary.summary;
|
|
763
|
+
summaryDetails = branchSummary.details;
|
|
764
|
+
}
|
|
765
|
+
const { newLeafId, editorText } = resolveNavigationTarget(targetEntry, targetId);
|
|
766
|
+
// Single declared commit point of a tree navigation.
|
|
767
|
+
this.lifecycle.setStage(lease, "committing");
|
|
748
768
|
const summaryId = await this.session.moveTo(newLeafId, summaryText
|
|
749
769
|
? { summary: summaryText, details: summaryDetails, fromHook: hookResult?.summary !== undefined }
|
|
750
770
|
: undefined);
|
|
@@ -761,13 +781,7 @@ export class AgentHarness {
|
|
|
761
781
|
fromHook: hookResult?.summary !== undefined,
|
|
762
782
|
});
|
|
763
783
|
return { cancelled: false, editorText, summaryEntry };
|
|
764
|
-
}
|
|
765
|
-
catch (error) {
|
|
766
|
-
throw normalizeHarnessError(error, "branch_summary");
|
|
767
|
-
}
|
|
768
|
-
finally {
|
|
769
|
-
this.phase = "idle";
|
|
770
|
-
}
|
|
784
|
+
}, classifyNavigateTreeOutcome);
|
|
771
785
|
}
|
|
772
786
|
getModel() {
|
|
773
787
|
return this.model;
|
|
@@ -775,12 +789,9 @@ export class AgentHarness {
|
|
|
775
789
|
async setModel(model) {
|
|
776
790
|
try {
|
|
777
791
|
const previousModel = this.model;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
else {
|
|
782
|
-
this.pendingSessionWrites.push({ type: "model_change", provider: model.provider, modelId: model.id });
|
|
783
|
-
}
|
|
792
|
+
const nextProvider = model.provider;
|
|
793
|
+
const nextModelId = model.id;
|
|
794
|
+
await this.persistConfigChange({ type: "model_change", provider: nextProvider, modelId: nextModelId });
|
|
784
795
|
this.model = model;
|
|
785
796
|
await this.emitOwn({ type: "model_update", model, previousModel, source: "set" });
|
|
786
797
|
}
|
|
@@ -794,12 +805,7 @@ export class AgentHarness {
|
|
|
794
805
|
async setThinkingLevel(level) {
|
|
795
806
|
try {
|
|
796
807
|
const previousLevel = this.thinkingLevel;
|
|
797
|
-
|
|
798
|
-
await this.session.appendThinkingLevelChange(level);
|
|
799
|
-
}
|
|
800
|
-
else {
|
|
801
|
-
this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level });
|
|
802
|
-
}
|
|
808
|
+
await this.persistConfigChange({ type: "thinking_level_change", thinkingLevel: level });
|
|
803
809
|
this.thinkingLevel = level;
|
|
804
810
|
await this.emitOwn({ type: "thinking_level_update", level, previousLevel });
|
|
805
811
|
}
|
|
@@ -818,12 +824,7 @@ export class AgentHarness {
|
|
|
818
824
|
this.validateToolNames(nextActiveToolNames, nextTools);
|
|
819
825
|
const previousToolNames = [...this.tools.keys()];
|
|
820
826
|
const previousActiveToolNames = [...this.activeToolNames];
|
|
821
|
-
|
|
822
|
-
await this.session.appendActiveToolsChange(nextActiveToolNames);
|
|
823
|
-
}
|
|
824
|
-
else {
|
|
825
|
-
this.pendingSessionWrites.push({ type: "active_tools_change", activeToolNames: [...nextActiveToolNames] });
|
|
826
|
-
}
|
|
827
|
+
await this.persistConfigChange({ type: "active_tools_change", activeToolNames: [...nextActiveToolNames] });
|
|
827
828
|
this.tools = nextTools;
|
|
828
829
|
this.activeToolNames = [...nextActiveToolNames];
|
|
829
830
|
await this.emitOwn({
|
|
@@ -844,16 +845,12 @@ export class AgentHarness {
|
|
|
844
845
|
}
|
|
845
846
|
async setActiveTools(toolNames) {
|
|
846
847
|
try {
|
|
847
|
-
|
|
848
|
+
const nextActiveToolNames = [...toolNames];
|
|
849
|
+
this.validateToolNames(nextActiveToolNames);
|
|
848
850
|
const previousToolNames = [...this.tools.keys()];
|
|
849
851
|
const previousActiveToolNames = [...this.activeToolNames];
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
else {
|
|
854
|
-
this.pendingSessionWrites.push({ type: "active_tools_change", activeToolNames: [...toolNames] });
|
|
855
|
-
}
|
|
856
|
-
this.activeToolNames = [...toolNames];
|
|
852
|
+
await this.persistConfigChange({ type: "active_tools_change", activeToolNames: [...nextActiveToolNames] });
|
|
853
|
+
this.activeToolNames = [...nextActiveToolNames];
|
|
857
854
|
await this.emitOwn({
|
|
858
855
|
type: "tools_update",
|
|
859
856
|
toolNames: [...this.tools.keys()],
|
|
@@ -899,31 +896,44 @@ export class AgentHarness {
|
|
|
899
896
|
async setStreamOptions(streamOptions) {
|
|
900
897
|
this.streamOptions = cloneStreamOptions(streamOptions);
|
|
901
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* Deliver the abort signal to the current operation without waiting for it to
|
|
901
|
+
* settle. Safe from an operation's own callbacks: nothing here awaits
|
|
902
|
+
* settlement, so it cannot form the cycle `abort()` has to refuse.
|
|
903
|
+
*/
|
|
904
|
+
requestAbort() {
|
|
905
|
+
assertAbortAllowed(this.lifecycle.getSnapshot());
|
|
906
|
+
return describeAbortDelivery(this.lifecycle.requestAbort());
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Queue work to run once the harness is idle and return its ref immediately.
|
|
910
|
+
*
|
|
911
|
+
* This is the callback-safe way to schedule follow-up work: awaiting
|
|
912
|
+
* `waitForIdle()` or `abort()` from a callback of the operation being settled
|
|
913
|
+
* deadlocks, because settlement awaits that callback. The ref reports the
|
|
914
|
+
* command's outcome and cancels it while it is still queued.
|
|
915
|
+
*/
|
|
916
|
+
async runWhenIdle(command) {
|
|
917
|
+
return this.deferredCommands.enqueue(command);
|
|
918
|
+
}
|
|
902
919
|
async abort() {
|
|
903
|
-
|
|
904
|
-
|
|
920
|
+
// Aborting awaits the captured operation's settlement, so a listener of that
|
|
921
|
+
// same operation must never reach the wait below.
|
|
922
|
+
this.rejectCurrentOperationSelfWait("abort()");
|
|
923
|
+
assertAbortAllowed(this.lifecycle.getSnapshot());
|
|
924
|
+
// Capture the current operation before delivering any signal: an operation
|
|
925
|
+
// started later by a settlement listener is never this call's target.
|
|
926
|
+
const capture = this.lifecycle.requestAbort();
|
|
905
927
|
const clearedSteer = this.steerQueue.splice(0);
|
|
906
928
|
const clearedFollowUp = this.followUpQueue.splice(0);
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
try {
|
|
916
|
-
await this.waitForIdle();
|
|
917
|
-
}
|
|
918
|
-
catch (error) {
|
|
919
|
-
errors.push(toError(error));
|
|
920
|
-
}
|
|
921
|
-
try {
|
|
922
|
-
await this.emitOwn({ type: "abort", clearedSteer, clearedFollowUp });
|
|
923
|
-
}
|
|
924
|
-
catch (error) {
|
|
925
|
-
errors.push(toError(error));
|
|
926
|
-
}
|
|
929
|
+
const errors = await collectStepErrors([
|
|
930
|
+
() => this.emitQueueUpdate(),
|
|
931
|
+
async () => {
|
|
932
|
+
if (capture.target)
|
|
933
|
+
await capture.target.settled;
|
|
934
|
+
},
|
|
935
|
+
() => this.emitOwn({ type: "abort", clearedSteer, clearedFollowUp }),
|
|
936
|
+
]);
|
|
927
937
|
if (errors.length > 0) {
|
|
928
938
|
const cause = errors.length === 1 ? errors[0] : new AggregateError(errors, "Abort completed with errors");
|
|
929
939
|
throw normalizeHarnessError(cause, "hook");
|
|
@@ -931,20 +941,12 @@ export class AgentHarness {
|
|
|
931
941
|
return { clearedSteer, clearedFollowUp };
|
|
932
942
|
}
|
|
933
943
|
async waitForIdle() {
|
|
934
|
-
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
await this.runPromise;
|
|
938
|
-
}
|
|
944
|
+
this.rejectCurrentOperationSelfWait("waitForIdle()");
|
|
945
|
+
// Delegates to the lifecycle: resolves once no operation is active or settling.
|
|
946
|
+
await this.lifecycle.waitForIdle();
|
|
939
947
|
}
|
|
940
948
|
subscribe(listener) {
|
|
941
|
-
|
|
942
|
-
if (!handlers) {
|
|
943
|
-
handlers = new Set();
|
|
944
|
-
this.handlers.set(SUBSCRIBER_EVENT_TYPE, handlers);
|
|
945
|
-
}
|
|
946
|
-
handlers.add(listener);
|
|
947
|
-
return () => handlers.delete(listener);
|
|
949
|
+
return this.subscribers.subscribe(listener);
|
|
948
950
|
}
|
|
949
951
|
on(type, handler) {
|
|
950
952
|
let handlers = this.handlers.get(type);
|