@leanandmean/coding-agent 0.74.1-scramjet.53 → 0.74.1-scramjet.55
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/core/agent-session.d.ts +31 -15
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +535 -215
- package/dist/core/agent-session.js.map +1 -1
- package/docs/models.md +2 -0
- package/docs/rpc.md +2 -2
- package/package.json +4 -4
|
@@ -18,6 +18,7 @@ import { basename, dirname, resolve } from "node:path";
|
|
|
18
18
|
// acknowledgement key (#341).
|
|
19
19
|
import { generateHarnessToolCallId } from "@leanandmean/agent";
|
|
20
20
|
import { clampThinkingLevel, cleanupSessionResources, flattenSystemPrompt, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@leanandmean/ai";
|
|
21
|
+
import { validateResponsesProviderFailure } from "@leanandmean/ai/openai-responses";
|
|
21
22
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
22
23
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
23
24
|
import { sleep } from "../utils/sleep.js";
|
|
@@ -59,6 +60,33 @@ export function parseSkillBlock(text) {
|
|
|
59
60
|
// ============================================================================
|
|
60
61
|
/** Standard thinking levels */
|
|
61
62
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
63
|
+
function createDeferred() {
|
|
64
|
+
let resolve;
|
|
65
|
+
let reject;
|
|
66
|
+
const promise = new Promise((res, rej) => {
|
|
67
|
+
resolve = res;
|
|
68
|
+
reject = rej;
|
|
69
|
+
});
|
|
70
|
+
promise.catch(() => { });
|
|
71
|
+
return { promise, resolve, reject, settled: false };
|
|
72
|
+
}
|
|
73
|
+
function resolveDeferred(deferred, value) {
|
|
74
|
+
if (deferred.settled)
|
|
75
|
+
return false;
|
|
76
|
+
deferred.settled = true;
|
|
77
|
+
deferred.resolve(value);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function rejectDeferred(deferred, error) {
|
|
81
|
+
if (deferred.settled)
|
|
82
|
+
return false;
|
|
83
|
+
deferred.settled = true;
|
|
84
|
+
deferred.reject(error);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function settleDeferred(deferred, error) {
|
|
88
|
+
return error ? rejectDeferred(deferred, error) : resolveDeferred(deferred, undefined);
|
|
89
|
+
}
|
|
62
90
|
export class AgentSession {
|
|
63
91
|
agent;
|
|
64
92
|
sessionManager;
|
|
@@ -92,11 +120,12 @@ export class AgentSession {
|
|
|
92
120
|
// Branch summarization state
|
|
93
121
|
_branchSummaryAbortController = undefined;
|
|
94
122
|
// Retry state
|
|
95
|
-
|
|
96
|
-
_retryAttempt = 0;
|
|
123
|
+
_retry = undefined;
|
|
97
124
|
_runRetryCount = 0;
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
_activeAgentRunSettlement = undefined;
|
|
126
|
+
_pendingAgentRunStart = undefined;
|
|
127
|
+
_unsettledAgentRuns = new Set();
|
|
128
|
+
_unsettledRetryChains = new Set();
|
|
100
129
|
// Bash execution state
|
|
101
130
|
_bashAbortController = undefined;
|
|
102
131
|
_pendingBashMessages = [];
|
|
@@ -268,20 +297,17 @@ export class AgentSession {
|
|
|
268
297
|
followUp: [...this._followUpMessages],
|
|
269
298
|
});
|
|
270
299
|
}
|
|
271
|
-
// Track last assistant message for auto-compaction check
|
|
272
|
-
_lastAssistantMessage = undefined;
|
|
273
300
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
274
301
|
_handleAgentEvent = (event) => {
|
|
275
302
|
this._captureOutputThroughput(event);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this._agentEventQueue = this._agentEventQueue.then(() => this._processAgentEventTracked(event), () => this._processAgentEventTracked(event));
|
|
303
|
+
const runSettlement = this._captureAgentRunSettlement(event);
|
|
304
|
+
const harnessAck = this._harnessPersistenceAckForEvent(event);
|
|
305
|
+
const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck), () => this._processAgentEventTracked(event, runSettlement, harnessAck));
|
|
306
|
+
this._agentEventQueue = processing;
|
|
307
|
+
if (harnessAck)
|
|
308
|
+
harnessAck.persistenceTail = processing;
|
|
283
309
|
// Keep queue alive if an event handler fails
|
|
284
|
-
|
|
310
|
+
processing.catch(() => { });
|
|
285
311
|
// SCRAMJET-DIVERGENCE: turn_end tool/prompt changes must settle before Agent snapshots
|
|
286
312
|
// live state for the next provider request (#524).
|
|
287
313
|
if (event.type === "turn_end")
|
|
@@ -315,94 +341,103 @@ export class AgentSession {
|
|
|
315
341
|
this._outputThroughputTracker.reset();
|
|
316
342
|
this._outputThroughputGeneration = undefined;
|
|
317
343
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
344
|
+
// SCRAMJET-DIVERGENCE: every Agent run owns persistence state and a one-shot settlement token (#553).
|
|
345
|
+
_captureAgentRunSettlement(event) {
|
|
346
|
+
if (event.type === "agent_start") {
|
|
347
|
+
// A retry continuation's run adopts the chain its originating prompt awaits; the chain stays
|
|
348
|
+
// bound to the retry phase until this start consumes it, however late Agent emits it.
|
|
349
|
+
const chain = this._retry?.phase === "continuing" ? this._retry.chain : createDeferred();
|
|
350
|
+
this._unsettledRetryChains.add(chain);
|
|
351
|
+
const run = { ...createDeferred(), chain };
|
|
352
|
+
this._activeAgentRunSettlement = run;
|
|
353
|
+
this._unsettledAgentRuns.add(run);
|
|
354
|
+
const reservation = this._pendingAgentRunStart;
|
|
355
|
+
if (reservation) {
|
|
356
|
+
this._pendingAgentRunStart = undefined;
|
|
357
|
+
reservation.run = run;
|
|
358
|
+
resolveDeferred(reservation, run);
|
|
359
|
+
}
|
|
360
|
+
return run;
|
|
361
|
+
}
|
|
362
|
+
const run = this._activeAgentRunSettlement;
|
|
363
|
+
if (event.type === "agent_end" && this._activeAgentRunSettlement === run) {
|
|
364
|
+
this._activeAgentRunSettlement = undefined;
|
|
365
|
+
}
|
|
366
|
+
return run;
|
|
367
|
+
}
|
|
368
|
+
_settleAgentRun(run, error) {
|
|
369
|
+
if (settleDeferred(run, error))
|
|
370
|
+
this._unsettledAgentRuns.delete(run);
|
|
371
|
+
}
|
|
372
|
+
_settleRetryChain(chain, error) {
|
|
373
|
+
if (settleDeferred(chain, error))
|
|
374
|
+
this._unsettledRetryChains.delete(chain);
|
|
375
|
+
}
|
|
376
|
+
_completeAgentRun(run, error) {
|
|
377
|
+
this._settleAgentRun(run, error);
|
|
378
|
+
this._settleRetryChain(run.chain, error);
|
|
379
|
+
}
|
|
380
|
+
_reserveAgentRunStart() {
|
|
381
|
+
if (this._pendingAgentRunStart)
|
|
382
|
+
throw new Error("An Agent prompt is already waiting to start.");
|
|
383
|
+
const reservation = createDeferred();
|
|
384
|
+
this._pendingAgentRunStart = reservation;
|
|
385
|
+
return reservation;
|
|
386
|
+
}
|
|
387
|
+
_rejectAgentRunStart(reservation, error) {
|
|
388
|
+
if (this._pendingAgentRunStart === reservation)
|
|
389
|
+
this._pendingAgentRunStart = undefined;
|
|
390
|
+
rejectDeferred(reservation, error);
|
|
391
|
+
}
|
|
392
|
+
// SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
|
|
393
|
+
// invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
|
|
394
|
+
async _processAgentEventTracked(event, run, harnessAck) {
|
|
359
395
|
try {
|
|
360
|
-
await this._processAgentEvent(event);
|
|
396
|
+
await this._processAgentEvent(event, run, harnessAck !== undefined);
|
|
361
397
|
}
|
|
362
398
|
catch (err) {
|
|
363
|
-
|
|
364
|
-
|
|
399
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
400
|
+
if (harnessAck) {
|
|
401
|
+
harnessAck.firstPersistenceError ??= error;
|
|
402
|
+
}
|
|
403
|
+
else if (run) {
|
|
404
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
405
|
+
run.assistantPersistenceError = error;
|
|
406
|
+
}
|
|
407
|
+
this._releaseRetryForFailedRun(error);
|
|
408
|
+
this._completeAgentRun(run, error);
|
|
365
409
|
}
|
|
366
410
|
throw err;
|
|
367
411
|
}
|
|
368
|
-
// Resolving here counts as "persisted" only because SessionManager persistence is synchronous
|
|
369
|
-
// (appendFileSync): _processAgentEvent has already written the tool-result row by the time it
|
|
370
|
-
// returns. If _persist ever becomes async, this would resolve before persistence completes.
|
|
371
|
-
if (resolvesAck) {
|
|
372
|
-
ack?.resolve();
|
|
373
|
-
}
|
|
374
412
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// events, or the tool-result message. Returns undefined for events unrelated to a pending harness
|
|
378
|
-
// invocation; a returned id that has no pending acknowledgement (an ordinary model tool call)
|
|
379
|
-
// resolves to no-op at the call site.
|
|
380
|
-
_harnessAckIdForEvent(event) {
|
|
413
|
+
_harnessPersistenceAckForEvent(event) {
|
|
414
|
+
let toolCallId;
|
|
381
415
|
switch (event.type) {
|
|
382
416
|
case "tool_execution_start":
|
|
383
417
|
case "tool_execution_update":
|
|
384
418
|
case "tool_execution_end":
|
|
385
|
-
|
|
419
|
+
toolCallId = event.toolCallId;
|
|
420
|
+
break;
|
|
386
421
|
case "message_start":
|
|
387
422
|
case "message_end": {
|
|
388
423
|
const message = event.message;
|
|
389
424
|
if (message.role === "toolResult") {
|
|
390
|
-
|
|
425
|
+
toolCallId = message.toolCallId;
|
|
391
426
|
}
|
|
392
|
-
if (message.role === "assistant") {
|
|
427
|
+
else if (message.role === "assistant") {
|
|
393
428
|
for (const block of message.content) {
|
|
394
429
|
if (block.type === "toolCall" && this._harnessPersistenceAcks.has(block.id)) {
|
|
395
|
-
|
|
430
|
+
toolCallId = block.id;
|
|
431
|
+
break;
|
|
396
432
|
}
|
|
397
433
|
}
|
|
398
434
|
}
|
|
399
|
-
|
|
435
|
+
break;
|
|
400
436
|
}
|
|
401
|
-
default:
|
|
402
|
-
return undefined;
|
|
403
437
|
}
|
|
438
|
+
return toolCallId === undefined ? undefined : this._harnessPersistenceAcks.get(toolCallId);
|
|
404
439
|
}
|
|
405
|
-
async _processAgentEvent(event) {
|
|
440
|
+
async _processAgentEvent(event, run, fromHarnessInvocation = false) {
|
|
406
441
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
407
442
|
// This ensures the UI sees the updated queue state
|
|
408
443
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -429,6 +464,8 @@ export class AgentSession {
|
|
|
429
464
|
await this._emitExtensionEvent(event);
|
|
430
465
|
// Notify all listeners
|
|
431
466
|
this._emit(event);
|
|
467
|
+
if (this._disposed)
|
|
468
|
+
return;
|
|
432
469
|
// Handle session persistence
|
|
433
470
|
if (event.type === "message_end") {
|
|
434
471
|
// Check if this is a custom message from extensions
|
|
@@ -439,8 +476,15 @@ export class AgentSession {
|
|
|
439
476
|
else if (event.message.role === "user" ||
|
|
440
477
|
event.message.role === "assistant" ||
|
|
441
478
|
event.message.role === "toolResult") {
|
|
442
|
-
//
|
|
443
|
-
|
|
479
|
+
// SCRAMJET-DIVERGENCE: retry settlement classifies the exact JSON-safe assistant snapshot
|
|
480
|
+
// that crossed the persistence boundary, never a later-mutated live Agent message (#553).
|
|
481
|
+
const persistedMessage = event.message.role === "assistant"
|
|
482
|
+
? JSON.parse(JSON.stringify(event.message))
|
|
483
|
+
: event.message;
|
|
484
|
+
const messageEntryId = this.sessionManager.appendMessage(persistedMessage);
|
|
485
|
+
if (persistedMessage.role === "assistant" && !fromHarnessInvocation && run) {
|
|
486
|
+
run.persistedAssistantSnapshot = persistedMessage;
|
|
487
|
+
}
|
|
444
488
|
if (event.message.role === "user") {
|
|
445
489
|
const sessionEntries = this._inputSessionEntries.get(event.message);
|
|
446
490
|
this._inputSessionEntries.delete(event.message);
|
|
@@ -467,47 +511,88 @@ export class AgentSession {
|
|
|
467
511
|
}
|
|
468
512
|
}
|
|
469
513
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// Reset retry counter immediately on successful assistant response
|
|
478
|
-
// This prevents accumulation across multiple LLM calls within a turn
|
|
479
|
-
if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
|
|
480
|
-
this._emit({
|
|
481
|
-
type: "auto_retry_end",
|
|
482
|
-
success: true,
|
|
483
|
-
attempt: this._retryAttempt,
|
|
484
|
-
});
|
|
485
|
-
this._retryAttempt = 0;
|
|
486
|
-
}
|
|
514
|
+
if (event.message.role === "assistant" &&
|
|
515
|
+
event.message.stopReason !== "error" &&
|
|
516
|
+
event.message.stopReason !== "aborted" &&
|
|
517
|
+
!fromHarnessInvocation) {
|
|
518
|
+
this._overflowRecoveryAttempted = false;
|
|
519
|
+
if (this._retry)
|
|
520
|
+
this._retry = { ...this._retry, attempt: 0 };
|
|
487
521
|
}
|
|
488
522
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
523
|
+
if (event.type === "agent_end") {
|
|
524
|
+
if (this._disposed || !run)
|
|
525
|
+
return;
|
|
526
|
+
if (run.assistantPersistenceError) {
|
|
527
|
+
const error = run.assistantPersistenceError;
|
|
528
|
+
run.assistantPersistenceError = undefined;
|
|
529
|
+
run.persistedAssistantSnapshot = undefined;
|
|
530
|
+
this._completeAgentRun(run, error);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const msg = run.persistedAssistantSnapshot;
|
|
534
|
+
run.persistedAssistantSnapshot = undefined;
|
|
535
|
+
if (!msg) {
|
|
536
|
+
this._releaseRetryForFailedRun(new Error("Retry continuation ended without an assistant message."));
|
|
537
|
+
this._completeAgentRun(run);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (msg.stopReason === "aborted" && this._retry) {
|
|
541
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
542
|
+
this._completeAgentRun(run);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (msg.stopReason !== "error") {
|
|
546
|
+
if (this._retry) {
|
|
547
|
+
const attempt = this._runRetryCount;
|
|
548
|
+
this._finishRetry({
|
|
549
|
+
schemaVersion: 1,
|
|
550
|
+
outcome: "succeeded",
|
|
551
|
+
attemptsCompleted: this._bounded(attempt),
|
|
552
|
+
cumulativeErrors: this._bounded(attempt),
|
|
553
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
554
|
+
}
|
|
555
|
+
this._completeAgentRun(run);
|
|
556
|
+
await this._checkCompaction(msg);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const classification = this._classifyRetry(msg);
|
|
560
|
+
if (classification.kind === "context_overflow") {
|
|
561
|
+
this._finishRetryWithoutSuccess({
|
|
562
|
+
schemaVersion: 1,
|
|
563
|
+
outcome: "not_attempted",
|
|
564
|
+
reason: "context_overflow_compaction",
|
|
565
|
+
evidence: "context_overflow",
|
|
566
|
+
}, msg);
|
|
567
|
+
this._completeAgentRun(run);
|
|
568
|
+
await this._checkCompaction(msg);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const settings = this.settingsManager.getRetrySettings();
|
|
572
|
+
if (!settings.enabled) {
|
|
573
|
+
this._finishRetryWithoutSuccess({
|
|
574
|
+
schemaVersion: 1,
|
|
575
|
+
outcome: "not_attempted",
|
|
576
|
+
reason: "retry_disabled",
|
|
577
|
+
evidence: classification.evidence,
|
|
578
|
+
}, msg);
|
|
579
|
+
this._completeAgentRun(run);
|
|
580
|
+
await this._checkCompaction(msg);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (classification.kind === "retry") {
|
|
584
|
+
const didRetry = await this._handleRetryableError(msg, classification.evidence, run);
|
|
496
585
|
if (didRetry)
|
|
497
|
-
return;
|
|
586
|
+
return;
|
|
498
587
|
}
|
|
499
|
-
|
|
588
|
+
else {
|
|
589
|
+
const { kind: _kind, ...cause } = classification;
|
|
590
|
+
this._finishRetryWithoutSuccess({ schemaVersion: 1, outcome: "not_attempted", ...cause }, msg);
|
|
591
|
+
}
|
|
592
|
+
this._completeAgentRun(run);
|
|
500
593
|
await this._checkCompaction(msg);
|
|
501
594
|
}
|
|
502
595
|
}
|
|
503
|
-
/** Resolve the pending retry promise */
|
|
504
|
-
_resolveRetry() {
|
|
505
|
-
if (this._retryResolve) {
|
|
506
|
-
this._retryResolve();
|
|
507
|
-
this._retryResolve = undefined;
|
|
508
|
-
this._retryPromise = undefined;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
596
|
/** Extract text content from a message */
|
|
512
597
|
_getUserMessageText(message) {
|
|
513
598
|
if (message.role !== "user")
|
|
@@ -665,12 +750,49 @@ export class AgentSession {
|
|
|
665
750
|
* Call this when completely done with the session.
|
|
666
751
|
*/
|
|
667
752
|
dispose() {
|
|
668
|
-
// SCRAMJET-DIVERGENCE: idempotent disposal
|
|
753
|
+
// SCRAMJET-DIVERGENCE: idempotent disposal revokes harness invocations and closes retry settlement (#341, #553).
|
|
669
754
|
// A second call must not re-reject already-settled acknowledgements or re-run teardown.
|
|
670
755
|
if (this._disposed)
|
|
671
756
|
return;
|
|
672
757
|
this._disposed = true;
|
|
673
758
|
this._resetOutputThroughput();
|
|
759
|
+
const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
|
|
760
|
+
const unsettledRuns = [...this._unsettledAgentRuns];
|
|
761
|
+
const retry = this._retry;
|
|
762
|
+
const unclassifiedError = unsettledRuns.find((run) => run.persistedAssistantSnapshot?.stopReason === "error")?.persistedAssistantSnapshot;
|
|
763
|
+
if (retry)
|
|
764
|
+
this.agent.abort();
|
|
765
|
+
try {
|
|
766
|
+
if (retry) {
|
|
767
|
+
this._appendAutoRetryRecord({
|
|
768
|
+
schemaVersion: 1,
|
|
769
|
+
outcome: "cancelled",
|
|
770
|
+
reason: "session_disposed",
|
|
771
|
+
attempt: this._bounded(retry.attempt),
|
|
772
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
else if (unclassifiedError) {
|
|
776
|
+
const classification = this._classifyRetry(unclassifiedError);
|
|
777
|
+
this._appendAutoRetryRecord({
|
|
778
|
+
schemaVersion: 1,
|
|
779
|
+
outcome: "not_attempted",
|
|
780
|
+
reason: "session_disposed",
|
|
781
|
+
evidence: classification.kind === "context_overflow" ? "context_overflow" : classification.evidence,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
this._reportRetryFailure(error);
|
|
787
|
+
}
|
|
788
|
+
if (retry?.phase === "backoff")
|
|
789
|
+
retry.controller.abort();
|
|
790
|
+
this._retry = undefined;
|
|
791
|
+
this._activeAgentRunSettlement = undefined;
|
|
792
|
+
const pendingAgentRunStart = this._pendingAgentRunStart;
|
|
793
|
+
if (pendingAgentRunStart)
|
|
794
|
+
this._rejectAgentRunStart(pendingAgentRunStart, retryDisposeError);
|
|
795
|
+
const pendingRetryContinuation = retry?.phase === "continuing" ? retry.chain : undefined;
|
|
674
796
|
this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
|
|
675
797
|
// Reject pending persistence acknowledgements before revoking Agent-side invocations, so a caller
|
|
676
798
|
// awaiting invokeHarnessTool sees rejection rather than a promise that never settles. Already-
|
|
@@ -678,10 +800,17 @@ export class AgentSession {
|
|
|
678
800
|
// roll back arbitrary side effects.
|
|
679
801
|
const disposeError = new Error("AgentSession disposed before the harness tool invocation settled.");
|
|
680
802
|
for (const ack of this._harnessPersistenceAcks.values()) {
|
|
681
|
-
|
|
803
|
+
this._settleHarnessPersistenceAck(ack, disposeError);
|
|
682
804
|
}
|
|
683
805
|
this._harnessPersistenceAcks.clear();
|
|
684
806
|
this.agent.rejectUnsettledHarnessTools(disposeError);
|
|
807
|
+
const retryChains = new Set(this._unsettledRetryChains);
|
|
808
|
+
if (pendingRetryContinuation)
|
|
809
|
+
retryChains.add(pendingRetryContinuation);
|
|
810
|
+
for (const run of unsettledRuns)
|
|
811
|
+
this._settleAgentRun(run, retryDisposeError);
|
|
812
|
+
for (const chain of retryChains)
|
|
813
|
+
this._settleRetryChain(chain, retryDisposeError);
|
|
685
814
|
this._disconnectFromAgent();
|
|
686
815
|
this._eventListeners = [];
|
|
687
816
|
cleanupSessionResources(this.sessionId);
|
|
@@ -753,7 +882,7 @@ export class AgentSession {
|
|
|
753
882
|
}
|
|
754
883
|
/** Current retry attempt (0 if not retrying) */
|
|
755
884
|
get retryAttempt() {
|
|
756
|
-
return this.
|
|
885
|
+
return this._retry?.attempt ?? 0;
|
|
757
886
|
}
|
|
758
887
|
/**
|
|
759
888
|
* Get the names of currently active tools.
|
|
@@ -842,13 +971,12 @@ export class AgentSession {
|
|
|
842
971
|
throw new Error(`Cannot invoke harness tool "${name}": no tool with that name is registered.`);
|
|
843
972
|
}
|
|
844
973
|
const toolCallId = this._allocateHarnessToolCallId(options?.toolCallId);
|
|
845
|
-
// Register
|
|
846
|
-
//
|
|
847
|
-
// persistence failure rejects, and the tool-call-id-keyed handler resolves after the matching
|
|
848
|
-
// tool-result is persisted.
|
|
974
|
+
// Register before Agent execution can emit events. The coordinator retains attribution until Agent
|
|
975
|
+
// execution and the invocation's final queued persistence task both settle.
|
|
849
976
|
const ack = this._registerHarnessPersistenceAck(toolCallId);
|
|
977
|
+
void this._completeHarnessToolInvocation(this.agent.runHarnessTool(tool, args, { toolCallId }), ack);
|
|
850
978
|
try {
|
|
851
|
-
await
|
|
979
|
+
await ack.promise;
|
|
852
980
|
}
|
|
853
981
|
finally {
|
|
854
982
|
// Identity-checked cleanup: only drop the entry if it is still this invocation's ack (disposal
|
|
@@ -881,17 +1009,27 @@ export class AgentSession {
|
|
|
881
1009
|
}
|
|
882
1010
|
return id;
|
|
883
1011
|
}
|
|
1012
|
+
async _completeHarnessToolInvocation(execution, ack) {
|
|
1013
|
+
let executionError;
|
|
1014
|
+
try {
|
|
1015
|
+
await execution;
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
executionError = error instanceof Error ? error : new Error(String(error));
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
await ack.persistenceTail;
|
|
1022
|
+
}
|
|
1023
|
+
catch { }
|
|
1024
|
+
this._settleHarnessPersistenceAck(ack, ack.firstPersistenceError ?? executionError);
|
|
1025
|
+
}
|
|
1026
|
+
_settleHarnessPersistenceAck(ack, error) {
|
|
1027
|
+
settleDeferred(ack, error);
|
|
1028
|
+
}
|
|
884
1029
|
// SCRAMJET-DIVERGENCE: harness-tool persisted-settlement (#341). Register a deferred acknowledgement
|
|
885
|
-
// keyed by tool-call id
|
|
886
|
-
// registers it, so a synchronous disposal rejection can never strand it as an unhandled rejection.
|
|
1030
|
+
// keyed by tool-call id before any invocation event can be emitted.
|
|
887
1031
|
_registerHarnessPersistenceAck(toolCallId) {
|
|
888
|
-
|
|
889
|
-
let reject;
|
|
890
|
-
const promise = new Promise((res, rej) => {
|
|
891
|
-
resolve = res;
|
|
892
|
-
reject = rej;
|
|
893
|
-
});
|
|
894
|
-
const ack = { promise, resolve, reject };
|
|
1032
|
+
const ack = { ...createDeferred(), persistenceTail: Promise.resolve() };
|
|
895
1033
|
this._harnessPersistenceAcks.set(toolCallId, ack);
|
|
896
1034
|
return ack;
|
|
897
1035
|
}
|
|
@@ -1036,6 +1174,7 @@ export class AgentSession {
|
|
|
1036
1174
|
return;
|
|
1037
1175
|
}
|
|
1038
1176
|
}
|
|
1177
|
+
this._assertRetryNotOwningIdleAgent();
|
|
1039
1178
|
// Emit input event for extension interception (before skill/template expansion)
|
|
1040
1179
|
let currentText = text;
|
|
1041
1180
|
let currentImages = options?.images;
|
|
@@ -1157,9 +1296,7 @@ export class AgentSession {
|
|
|
1157
1296
|
}
|
|
1158
1297
|
acceptInput?.();
|
|
1159
1298
|
preflightResult?.(true);
|
|
1160
|
-
this.
|
|
1161
|
-
await this.agent.prompt(messages);
|
|
1162
|
-
await this.waitForRetry();
|
|
1299
|
+
await this._runAgentPrompt(messages);
|
|
1163
1300
|
}
|
|
1164
1301
|
/**
|
|
1165
1302
|
* Try to execute an extension command. Returns true if command was found and executed.
|
|
@@ -1337,7 +1474,7 @@ export class AgentSession {
|
|
|
1337
1474
|
}
|
|
1338
1475
|
}
|
|
1339
1476
|
else if (options?.triggerTurn) {
|
|
1340
|
-
await this.
|
|
1477
|
+
await this._runAgentPrompt(appMessage);
|
|
1341
1478
|
}
|
|
1342
1479
|
else {
|
|
1343
1480
|
this.agent.state.messages.push(appMessage);
|
|
@@ -2312,128 +2449,311 @@ export class AgentSession {
|
|
|
2312
2449
|
// =========================================================================
|
|
2313
2450
|
// Auto-Retry
|
|
2314
2451
|
// =========================================================================
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
2318
|
-
*/
|
|
2319
|
-
_isRetryableError(message) {
|
|
2320
|
-
if (message.stopReason !== "error" || !message.errorMessage)
|
|
2321
|
-
return false;
|
|
2322
|
-
// Context overflow is handled by compaction, not retry
|
|
2452
|
+
// SCRAMJET-DIVERGENCE: persisted post-extension snapshots authoritatively classify and settle retries (#553).
|
|
2453
|
+
_classifyRetry(message) {
|
|
2323
2454
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2324
|
-
if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity)))
|
|
2325
|
-
return
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2455
|
+
if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
2456
|
+
return { kind: "context_overflow" };
|
|
2457
|
+
}
|
|
2458
|
+
const structured = validateResponsesProviderFailure(message.diagnostics);
|
|
2459
|
+
if (structured.status === "valid") {
|
|
2460
|
+
if (structured.retryDisposition === "transient")
|
|
2461
|
+
return { kind: "retry", evidence: "provider_failure" };
|
|
2462
|
+
return {
|
|
2463
|
+
kind: "do_not_retry",
|
|
2464
|
+
reason: structured.retryDisposition === "non_transient" ? "structured_non_transient" : "structured_unknown",
|
|
2465
|
+
evidence: "provider_failure",
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
if (structured.status === "malformed") {
|
|
2469
|
+
return { kind: "do_not_retry", reason: "malformed_provider_diagnostic", evidence: "none" };
|
|
2470
|
+
}
|
|
2471
|
+
if (structured.status === "duplicate") {
|
|
2472
|
+
return { kind: "do_not_retry", reason: "duplicate_provider_diagnostic", evidence: "none" };
|
|
2473
|
+
}
|
|
2474
|
+
if (!message.errorMessage) {
|
|
2475
|
+
return { kind: "do_not_retry", reason: "missing_error_evidence", evidence: "none" };
|
|
2476
|
+
}
|
|
2477
|
+
if (this._isLegacyRetryableError(message.errorMessage))
|
|
2478
|
+
return { kind: "retry", evidence: "legacy_text" };
|
|
2479
|
+
return { kind: "do_not_retry", reason: "legacy_non_retryable", evidence: "legacy_text" };
|
|
2480
|
+
}
|
|
2481
|
+
_isLegacyRetryableError(errorMessage) {
|
|
2482
|
+
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(errorMessage);
|
|
2483
|
+
}
|
|
2484
|
+
_bounded(value) {
|
|
2485
|
+
return Math.min(65_535, Math.max(0, Number.isFinite(value) ? Math.trunc(value) : 65_535));
|
|
2486
|
+
}
|
|
2487
|
+
_appendAutoRetryRecord(record) {
|
|
2488
|
+
this.sessionManager.appendCustomEntry("coding-agent:auto-retry", record);
|
|
2329
2489
|
}
|
|
2330
2490
|
/**
|
|
2331
|
-
*
|
|
2332
|
-
*
|
|
2491
|
+
* Persist a terminal record, emit `auto_retry_end` when a retry phase was active, and clear the phase.
|
|
2492
|
+
* The phase is released before persistence is attempted so a persistence failure can never strand it.
|
|
2333
2493
|
*/
|
|
2334
|
-
|
|
2335
|
-
const
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2494
|
+
_finishRetry(record, end) {
|
|
2495
|
+
const retry = this._retry;
|
|
2496
|
+
this._retry = undefined;
|
|
2497
|
+
try {
|
|
2498
|
+
this._appendAutoRetryRecord(record);
|
|
2339
2499
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2500
|
+
catch (error) {
|
|
2501
|
+
if (retry) {
|
|
2502
|
+
this._emit({
|
|
2503
|
+
type: "auto_retry_end",
|
|
2504
|
+
success: false,
|
|
2505
|
+
attempt: end?.attempt ?? retry.attempt,
|
|
2506
|
+
finalError: "Retry outcome persistence failed",
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
throw this._reportRetryFailure(error);
|
|
2510
|
+
}
|
|
2511
|
+
if (retry && end)
|
|
2512
|
+
this._emit(end);
|
|
2513
|
+
}
|
|
2514
|
+
_finishRetryWithoutSuccess(record, message) {
|
|
2515
|
+
const retry = this._retry;
|
|
2516
|
+
if (retry?.attempt === 0) {
|
|
2517
|
+
// The continuation already produced a successful message, so the retry recovered and this
|
|
2518
|
+
// later failure is a fresh, non-retried error; close the retry as succeeded first.
|
|
2519
|
+
const attempt = this._runRetryCount;
|
|
2520
|
+
this._finishRetry({
|
|
2521
|
+
schemaVersion: 1,
|
|
2522
|
+
outcome: "succeeded",
|
|
2523
|
+
attemptsCompleted: this._bounded(attempt),
|
|
2524
|
+
cumulativeErrors: this._bounded(attempt),
|
|
2525
|
+
}, { type: "auto_retry_end", success: true, attempt });
|
|
2526
|
+
this._finishRetry(record);
|
|
2527
|
+
return;
|
|
2346
2528
|
}
|
|
2347
|
-
this.
|
|
2529
|
+
this._finishRetry(record, retry
|
|
2530
|
+
? { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: message.errorMessage }
|
|
2531
|
+
: undefined);
|
|
2532
|
+
}
|
|
2533
|
+
/** A run that ends without a classifiable snapshot cannot continue the retry; close it so nothing stays wedged. */
|
|
2534
|
+
_releaseRetryForFailedRun(error) {
|
|
2535
|
+
const retry = this._retry;
|
|
2536
|
+
if (!retry)
|
|
2537
|
+
return;
|
|
2538
|
+
try {
|
|
2539
|
+
this._finishRetry({
|
|
2540
|
+
schemaVersion: 1,
|
|
2541
|
+
outcome: "failed",
|
|
2542
|
+
reason: "run_failed",
|
|
2543
|
+
attempt: this._bounded(retry.attempt),
|
|
2544
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2545
|
+
}, { type: "auto_retry_end", success: false, attempt: retry.attempt, finalError: error.message });
|
|
2546
|
+
}
|
|
2547
|
+
catch {
|
|
2548
|
+
// Already reported and the phase is released; the run settles with its own error.
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
_reportRetryFailure(error) {
|
|
2552
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
2553
|
+
this._extensionRunner.emitError({
|
|
2554
|
+
extensionPath: "agent-session:auto-retry",
|
|
2555
|
+
event: "retry_settlement",
|
|
2556
|
+
error: normalized.message,
|
|
2557
|
+
stack: normalized.stack,
|
|
2558
|
+
});
|
|
2559
|
+
return normalized;
|
|
2560
|
+
}
|
|
2561
|
+
async _handleRetryableError(message, evidence, run) {
|
|
2562
|
+
const settings = this.settingsManager.getRetrySettings();
|
|
2563
|
+
const attempt = (this._retry?.attempt ?? 0) + 1;
|
|
2348
2564
|
this._runRetryCount++;
|
|
2349
2565
|
const cumulativeCap = settings.maxRetries * 2;
|
|
2566
|
+
if (attempt > settings.maxRetries) {
|
|
2567
|
+
const attemptsCompleted = attempt - 1;
|
|
2568
|
+
this._finishRetry({
|
|
2569
|
+
schemaVersion: 1,
|
|
2570
|
+
outcome: "exhausted",
|
|
2571
|
+
reason: "attempt_limit",
|
|
2572
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2573
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2574
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2575
|
+
}, { type: "auto_retry_end", success: false, attempt: attemptsCompleted, finalError: message.errorMessage });
|
|
2576
|
+
return false;
|
|
2577
|
+
}
|
|
2350
2578
|
if (this._runRetryCount > cumulativeCap) {
|
|
2351
|
-
this.
|
|
2579
|
+
const attemptsCompleted = this._runRetryCount - 1;
|
|
2580
|
+
this._finishRetry({
|
|
2581
|
+
schemaVersion: 1,
|
|
2582
|
+
outcome: "exhausted",
|
|
2583
|
+
reason: "cumulative_limit",
|
|
2584
|
+
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2585
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2586
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2587
|
+
}, {
|
|
2352
2588
|
type: "auto_retry_end",
|
|
2353
2589
|
success: false,
|
|
2354
|
-
attempt:
|
|
2355
|
-
finalError: `Repeated retry failures (${
|
|
2590
|
+
attempt: attemptsCompleted,
|
|
2591
|
+
finalError: `Repeated retry failures (${attemptsCompleted} total attempts this prompt). ${message.errorMessage ?? ""}`.trim(),
|
|
2356
2592
|
});
|
|
2357
|
-
this._retryAttempt = 0;
|
|
2358
|
-
this._resolveRetry();
|
|
2359
2593
|
return false;
|
|
2360
2594
|
}
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2595
|
+
const delayMs = settings.baseDelayMs * 2 ** (attempt - 1);
|
|
2596
|
+
const previous = this._retry;
|
|
2597
|
+
this._retry = undefined;
|
|
2598
|
+
try {
|
|
2599
|
+
this._appendAutoRetryRecord({
|
|
2600
|
+
schemaVersion: 1,
|
|
2601
|
+
outcome: "scheduled",
|
|
2602
|
+
evidence,
|
|
2603
|
+
attempt: this._bounded(attempt),
|
|
2604
|
+
maxAttempts: this._bounded(settings.maxRetries),
|
|
2605
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2606
|
+
delayMs: this._bounded(delayMs),
|
|
2368
2607
|
});
|
|
2369
|
-
this._retryAttempt = 0;
|
|
2370
|
-
this._resolveRetry(); // Resolve so waitForRetry() completes
|
|
2371
|
-
return false;
|
|
2372
2608
|
}
|
|
2373
|
-
|
|
2609
|
+
catch (error) {
|
|
2610
|
+
if (previous) {
|
|
2611
|
+
this._emit({
|
|
2612
|
+
type: "auto_retry_end",
|
|
2613
|
+
success: false,
|
|
2614
|
+
attempt: previous.attempt,
|
|
2615
|
+
finalError: "Retry outcome persistence failed",
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
throw this._reportRetryFailure(error);
|
|
2619
|
+
}
|
|
2620
|
+
const controller = new AbortController();
|
|
2621
|
+
const backoff = { phase: "backoff", attempt, controller, run };
|
|
2622
|
+
this._retry = backoff;
|
|
2374
2623
|
this._emit({
|
|
2375
2624
|
type: "auto_retry_start",
|
|
2376
|
-
attempt
|
|
2625
|
+
attempt,
|
|
2377
2626
|
maxAttempts: settings.maxRetries,
|
|
2378
2627
|
delayMs,
|
|
2379
2628
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2380
2629
|
cumulativeErrors: this._runRetryCount,
|
|
2381
2630
|
});
|
|
2382
|
-
|
|
2631
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2632
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2633
|
+
}
|
|
2383
2634
|
const messages = this.agent.state.messages;
|
|
2384
2635
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2385
2636
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2386
2637
|
}
|
|
2387
|
-
// Wait with exponential backoff (abortable)
|
|
2388
|
-
this._retryAbortController = new AbortController();
|
|
2389
2638
|
try {
|
|
2390
|
-
await sleep(delayMs,
|
|
2639
|
+
await sleep(delayMs, controller.signal);
|
|
2640
|
+
await sleep(0, controller.signal);
|
|
2391
2641
|
}
|
|
2392
2642
|
catch {
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
this.
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2643
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2644
|
+
}
|
|
2645
|
+
if (controller.signal.aborted || this._disposed) {
|
|
2646
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2647
|
+
}
|
|
2648
|
+
// Hand the chain to the continuation before `continue()`; the run that eventually emits
|
|
2649
|
+
// `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
|
|
2650
|
+
const continuing = { phase: "continuing", attempt, chain: run.chain };
|
|
2651
|
+
this._retry = continuing;
|
|
2652
|
+
const continuation = this.agent.continue();
|
|
2653
|
+
this._settleAgentRun(run);
|
|
2654
|
+
continuation.catch((error) => {
|
|
2655
|
+
if (this._disposed || this._retry !== continuing)
|
|
2656
|
+
return;
|
|
2657
|
+
const continuationError = this._reportRetryFailure(error);
|
|
2658
|
+
let settlementError = continuationError;
|
|
2659
|
+
try {
|
|
2660
|
+
this._finishRetry({
|
|
2661
|
+
schemaVersion: 1,
|
|
2662
|
+
outcome: "failed",
|
|
2663
|
+
reason: "continuation_rejected",
|
|
2664
|
+
attempt: this._bounded(attempt),
|
|
2665
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2666
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: continuationError.message });
|
|
2667
|
+
}
|
|
2668
|
+
catch (persistenceError) {
|
|
2669
|
+
settlementError = new AggregateError([continuationError, persistenceError], "Retry continuation and outcome persistence both failed.", { cause: continuationError });
|
|
2670
|
+
}
|
|
2671
|
+
this._settleRetryChain(run.chain, settlementError);
|
|
2672
|
+
});
|
|
2413
2673
|
return true;
|
|
2414
2674
|
}
|
|
2675
|
+
_finishCancelledRetry(reason, expected) {
|
|
2676
|
+
if (this._disposed) {
|
|
2677
|
+
this._retry = undefined;
|
|
2678
|
+
throw new Error("AgentSession disposed before retry settlement completed.");
|
|
2679
|
+
}
|
|
2680
|
+
const retry = this._retry;
|
|
2681
|
+
if (expected && retry !== expected)
|
|
2682
|
+
return false;
|
|
2683
|
+
const attempt = retry?.attempt ?? 0;
|
|
2684
|
+
this._finishRetry({
|
|
2685
|
+
schemaVersion: 1,
|
|
2686
|
+
outcome: "cancelled",
|
|
2687
|
+
reason,
|
|
2688
|
+
attempt: this._bounded(attempt),
|
|
2689
|
+
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2690
|
+
}, { type: "auto_retry_end", success: false, attempt, finalError: "Retry cancelled" });
|
|
2691
|
+
return false;
|
|
2692
|
+
}
|
|
2415
2693
|
/**
|
|
2416
2694
|
* Cancel in-progress retry.
|
|
2417
2695
|
*/
|
|
2418
2696
|
abortRetry() {
|
|
2419
|
-
this.
|
|
2420
|
-
|
|
2421
|
-
this._resolveRetry();
|
|
2422
|
-
}
|
|
2423
|
-
/**
|
|
2424
|
-
* Wait for any in-progress retry to complete.
|
|
2425
|
-
* Returns immediately if no retry is in progress.
|
|
2426
|
-
*/
|
|
2427
|
-
async waitForRetry() {
|
|
2428
|
-
if (!this._retryPromise) {
|
|
2697
|
+
const retry = this._retry;
|
|
2698
|
+
if (!retry)
|
|
2429
2699
|
return;
|
|
2700
|
+
if (retry.phase === "backoff")
|
|
2701
|
+
retry.controller.abort();
|
|
2702
|
+
this.agent.abort();
|
|
2703
|
+
}
|
|
2704
|
+
/** Agent's own `activeRun` guard is false during backoff; the retry phase owns the idle Agent then. */
|
|
2705
|
+
_assertRetryNotOwningIdleAgent() {
|
|
2706
|
+
if (this._retry && !this.isStreaming) {
|
|
2707
|
+
throw new Error("Automatic retry is in progress. Cancel the retry before starting another prompt.");
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
async _runAgentPrompt(messages) {
|
|
2711
|
+
this._assertRetryNotOwningIdleAgent();
|
|
2712
|
+
const start = this._reserveAgentRunStart();
|
|
2713
|
+
this._runRetryCount = 0;
|
|
2714
|
+
let promptFailure;
|
|
2715
|
+
try {
|
|
2716
|
+
await this.agent.prompt(messages);
|
|
2430
2717
|
}
|
|
2431
|
-
|
|
2718
|
+
catch (error) {
|
|
2719
|
+
promptFailure = { error };
|
|
2720
|
+
}
|
|
2721
|
+
if (!start.run && this._pendingAgentRunStart === start) {
|
|
2722
|
+
const startError = promptFailure
|
|
2723
|
+
? promptFailure.error instanceof Error
|
|
2724
|
+
? promptFailure.error
|
|
2725
|
+
: new Error(String(promptFailure.error))
|
|
2726
|
+
: new Error("Agent prompt completed before agent_start.");
|
|
2727
|
+
this._rejectAgentRunStart(start, startError);
|
|
2728
|
+
if (promptFailure)
|
|
2729
|
+
throw promptFailure.error;
|
|
2730
|
+
}
|
|
2731
|
+
let settlementFailure;
|
|
2732
|
+
try {
|
|
2733
|
+
const settlement = await start.promise;
|
|
2734
|
+
await this.waitForRetry(settlement);
|
|
2735
|
+
}
|
|
2736
|
+
catch (error) {
|
|
2737
|
+
settlementFailure = { error };
|
|
2738
|
+
}
|
|
2739
|
+
if (promptFailure && settlementFailure) {
|
|
2740
|
+
throw new AggregateError([promptFailure.error, settlementFailure.error], "Agent prompt and retry settlement both failed.", { cause: promptFailure.error });
|
|
2741
|
+
}
|
|
2742
|
+
if (promptFailure)
|
|
2743
|
+
throw promptFailure.error;
|
|
2744
|
+
if (settlementFailure)
|
|
2745
|
+
throw settlementFailure.error;
|
|
2746
|
+
}
|
|
2747
|
+
async waitForRetry(settlement) {
|
|
2748
|
+
if (!settlement)
|
|
2749
|
+
return;
|
|
2750
|
+
await settlement.promise;
|
|
2751
|
+
await settlement.chain.promise;
|
|
2432
2752
|
await this.agent.waitForIdle();
|
|
2433
2753
|
}
|
|
2434
2754
|
/** Whether auto-retry is currently in progress */
|
|
2435
2755
|
get isRetrying() {
|
|
2436
|
-
return this.
|
|
2756
|
+
return this._retry !== undefined;
|
|
2437
2757
|
}
|
|
2438
2758
|
/** Whether auto-retry is enabled */
|
|
2439
2759
|
get autoRetryEnabled() {
|