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