@leanandmean/coding-agent 0.74.1-scramjet.67 → 0.74.1-scramjet.69
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/README.md +2 -0
- package/dist/core/agent-session.d.ts +6 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +247 -45
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +3 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +47 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/docs/compaction.md +13 -1
- package/docs/custom-provider.md +23 -3
- package/docs/extensions.md +11 -3
- package/docs/json.md +2 -0
- package/docs/sdk.md +9 -0
- package/docs/session-format.md +3 -0
- package/docs/settings.md +6 -2
- package/package.json +4 -4
|
@@ -17,14 +17,13 @@ import { basename, dirname, resolve } from "node:path";
|
|
|
17
17
|
// SCRAMJET-DIVERGENCE: provider-safe harness tool-call id generation for the persisted-settlement
|
|
18
18
|
// acknowledgement key (#341).
|
|
19
19
|
import { generateHarnessToolCallId } from "@leanandmean/agent";
|
|
20
|
-
import { clampThinkingLevel, cleanupSessionResources, flattenSystemPrompt, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@leanandmean/ai";
|
|
21
|
-
import { validateResponsesProviderFailure } from "@leanandmean/ai/openai-responses";
|
|
20
|
+
import { blocksFailureRecovery, clampThinkingLevel, cleanupSessionResources, flattenSystemPrompt, getSupportedThinkingLevels, inspectFailureEvidence, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@leanandmean/ai";
|
|
22
21
|
import { theme } from "../modes/interactive/theme/theme.js";
|
|
23
22
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
24
23
|
import { sleep } from "../utils/sleep.js";
|
|
25
24
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
26
25
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
27
|
-
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
26
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
28
27
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
29
28
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
30
29
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
@@ -87,6 +86,9 @@ function rejectDeferred(deferred, error) {
|
|
|
87
86
|
function settleDeferred(deferred, error) {
|
|
88
87
|
return error ? rejectDeferred(deferred, error) : resolveDeferred(deferred, undefined);
|
|
89
88
|
}
|
|
89
|
+
function validRecordedCost(value) {
|
|
90
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
91
|
+
}
|
|
90
92
|
export class AgentSession {
|
|
91
93
|
agent;
|
|
92
94
|
sessionManager;
|
|
@@ -97,6 +99,10 @@ export class AgentSession {
|
|
|
97
99
|
_unsubscribeAgent;
|
|
98
100
|
_eventListeners = [];
|
|
99
101
|
_agentEventQueue = Promise.resolve();
|
|
102
|
+
// SCRAMJET-DIVERGENCE: execution ownership survives final-event queue latency until append succeeds (#598).
|
|
103
|
+
_toolCostExecutions = new Map();
|
|
104
|
+
_pendingToolCosts = new Set();
|
|
105
|
+
_historicalCostDefinitions = new Map();
|
|
100
106
|
_outputThroughputTracker = new OutputThroughputTracker();
|
|
101
107
|
_outputThroughputHistory;
|
|
102
108
|
_outputThroughputGeneration;
|
|
@@ -115,6 +121,7 @@ export class AgentSession {
|
|
|
115
121
|
_inputSessionEntries = new WeakMap();
|
|
116
122
|
// Compaction state
|
|
117
123
|
_compactionAbortController = undefined;
|
|
124
|
+
_manualCompactionDrain;
|
|
118
125
|
_autoCompactionAbortController = undefined;
|
|
119
126
|
_overflowRecoveryAttempted = false;
|
|
120
127
|
// Branch summarization state
|
|
@@ -299,10 +306,13 @@ export class AgentSession {
|
|
|
299
306
|
}
|
|
300
307
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
301
308
|
_handleAgentEvent = (event) => {
|
|
309
|
+
if (this._disposed)
|
|
310
|
+
return;
|
|
302
311
|
this._captureOutputThroughput(event);
|
|
312
|
+
const toolCost = this._captureToolCost(event);
|
|
303
313
|
const runSettlement = this._captureAgentRunSettlement(event);
|
|
304
314
|
const harnessAck = this._harnessPersistenceAckForEvent(event);
|
|
305
|
-
const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck), () => this._processAgentEventTracked(event, runSettlement, harnessAck));
|
|
315
|
+
const processing = this._agentEventQueue.then(() => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost), () => this._processAgentEventTracked(event, runSettlement, harnessAck, toolCost));
|
|
306
316
|
this._agentEventQueue = processing;
|
|
307
317
|
if (harnessAck)
|
|
308
318
|
harnessAck.persistenceTail = processing;
|
|
@@ -313,6 +323,42 @@ export class AgentSession {
|
|
|
313
323
|
if (event.type === "turn_end")
|
|
314
324
|
return this._agentEventQueue;
|
|
315
325
|
};
|
|
326
|
+
// SCRAMJET-DIVERGENCE: scalar reports are captured before mutable events enter async hooks (#598).
|
|
327
|
+
_captureToolCost(event) {
|
|
328
|
+
if (event.type === "tool_execution_start") {
|
|
329
|
+
const execution = {};
|
|
330
|
+
this._toolCostExecutions.set(event.toolCallId, execution);
|
|
331
|
+
this._pendingToolCosts.add(execution);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
let toolCallId;
|
|
335
|
+
let reported;
|
|
336
|
+
const final = event.type === "message_end" && event.message.role === "toolResult";
|
|
337
|
+
if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
|
|
338
|
+
toolCallId = event.toolCallId;
|
|
339
|
+
reported = event.type === "tool_execution_update" ? event.partialResult?.cost : event.result?.cost;
|
|
340
|
+
}
|
|
341
|
+
else if (event.type === "message_end" && event.message.role === "toolResult") {
|
|
342
|
+
toolCallId = event.message.toolCallId;
|
|
343
|
+
reported = event.message.cost;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let execution = this._toolCostExecutions.get(toolCallId);
|
|
349
|
+
if (!execution && validRecordedCost(reported)) {
|
|
350
|
+
execution = {};
|
|
351
|
+
this._toolCostExecutions.set(toolCallId, execution);
|
|
352
|
+
this._pendingToolCosts.add(execution);
|
|
353
|
+
}
|
|
354
|
+
if (execution && validRecordedCost(reported))
|
|
355
|
+
execution.cost = reported;
|
|
356
|
+
if (final) {
|
|
357
|
+
this._toolCostExecutions.delete(toolCallId);
|
|
358
|
+
return execution;
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
316
362
|
// SCRAMJET-DIVERGENCE: Capture provider events synchronously and reset stale throughput lifecycles (#476).
|
|
317
363
|
_captureOutputThroughput(event) {
|
|
318
364
|
if (event.type === "message_start" && event.message.role === "assistant" && event.message.origin === "provider") {
|
|
@@ -391,16 +437,18 @@ export class AgentSession {
|
|
|
391
437
|
}
|
|
392
438
|
// SCRAMJET-DIVERGENCE: harness-tool persisted settlement (#341). Synchronous ownership keeps queued
|
|
393
439
|
// invocation events attributed after early failures; public settlement awaits execution and the final persistence tail.
|
|
394
|
-
async _processAgentEventTracked(event, run, harnessAck) {
|
|
440
|
+
async _processAgentEventTracked(event, run, harnessAck, toolCost) {
|
|
395
441
|
try {
|
|
396
|
-
await this._processAgentEvent(event, run, harnessAck !== undefined);
|
|
442
|
+
await this._processAgentEvent(event, run, harnessAck !== undefined, toolCost);
|
|
397
443
|
}
|
|
398
444
|
catch (err) {
|
|
399
445
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
446
|
+
if (this._manualCompactionDrain)
|
|
447
|
+
this._manualCompactionDrain.firstError ??= error;
|
|
400
448
|
if (harnessAck) {
|
|
401
449
|
harnessAck.firstPersistenceError ??= error;
|
|
402
450
|
}
|
|
403
|
-
else if (run) {
|
|
451
|
+
else if (run && !this._manualCompactionDrain) {
|
|
404
452
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
405
453
|
run.assistantPersistenceError = error;
|
|
406
454
|
}
|
|
@@ -437,7 +485,19 @@ export class AgentSession {
|
|
|
437
485
|
}
|
|
438
486
|
return toolCallId === undefined ? undefined : this._harnessPersistenceAcks.get(toolCallId);
|
|
439
487
|
}
|
|
440
|
-
async _processAgentEvent(event, run, fromHarnessInvocation = false) {
|
|
488
|
+
async _processAgentEvent(event, run, fromHarnessInvocation = false, toolCost) {
|
|
489
|
+
if (this._disposed)
|
|
490
|
+
return;
|
|
491
|
+
// SCRAMJET-DIVERGENCE: queued lifecycle events must respect manual compaction ownership (#598).
|
|
492
|
+
if (this._manualCompactionDrain && event.type !== "message_end" && event.type !== "agent_end")
|
|
493
|
+
return;
|
|
494
|
+
if (this._manualCompactionDrain && event.type === "agent_end") {
|
|
495
|
+
if (this._retry)
|
|
496
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
497
|
+
if (run)
|
|
498
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
441
501
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
442
502
|
// This ensures the UI sees the updated queue state
|
|
443
503
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -462,6 +522,18 @@ export class AgentSession {
|
|
|
462
522
|
}
|
|
463
523
|
// Emit to extensions first
|
|
464
524
|
await this._emitExtensionEvent(event);
|
|
525
|
+
if (event.type === "message_end" && event.message.role === "toolResult" && toolCost?.cost !== undefined) {
|
|
526
|
+
event.message.cost = toolCost.cost;
|
|
527
|
+
}
|
|
528
|
+
// An awaited hook may have acquired manual ownership while this event was in flight.
|
|
529
|
+
if (this._manualCompactionDrain && event.type !== "message_end") {
|
|
530
|
+
if (event.type === "agent_end" && run) {
|
|
531
|
+
if (this._retry)
|
|
532
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
533
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
465
537
|
// Notify all listeners
|
|
466
538
|
this._emit(event);
|
|
467
539
|
if (this._disposed)
|
|
@@ -482,6 +554,8 @@ export class AgentSession {
|
|
|
482
554
|
? JSON.parse(JSON.stringify(event.message))
|
|
483
555
|
: event.message;
|
|
484
556
|
const messageEntryId = this.sessionManager.appendMessage(persistedMessage);
|
|
557
|
+
if (toolCost)
|
|
558
|
+
this._pendingToolCosts.delete(toolCost);
|
|
485
559
|
if (persistedMessage.role === "assistant" && !fromHarnessInvocation && run) {
|
|
486
560
|
run.persistedAssistantSnapshot = persistedMessage;
|
|
487
561
|
}
|
|
@@ -514,7 +588,8 @@ export class AgentSession {
|
|
|
514
588
|
if (event.message.role === "assistant" &&
|
|
515
589
|
event.message.stopReason !== "error" &&
|
|
516
590
|
event.message.stopReason !== "aborted" &&
|
|
517
|
-
!fromHarnessInvocation
|
|
591
|
+
!fromHarnessInvocation &&
|
|
592
|
+
!this._manualCompactionDrain) {
|
|
518
593
|
this._overflowRecoveryAttempted = false;
|
|
519
594
|
if (this._retry)
|
|
520
595
|
this._retry = { ...this._retry, attempt: 0 };
|
|
@@ -523,6 +598,12 @@ export class AgentSession {
|
|
|
523
598
|
if (event.type === "agent_end") {
|
|
524
599
|
if (this._disposed || !run)
|
|
525
600
|
return;
|
|
601
|
+
if (this._manualCompactionDrain) {
|
|
602
|
+
if (this._retry)
|
|
603
|
+
this._finishCancelledRetry("cancelled_during_continuation");
|
|
604
|
+
this._completeAgentRun(run, this._manualCompactionDrain.firstError);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
526
607
|
if (run.assistantPersistenceError) {
|
|
527
608
|
const error = run.assistantPersistenceError;
|
|
528
609
|
run.assistantPersistenceError = undefined;
|
|
@@ -741,8 +822,8 @@ export class AgentSession {
|
|
|
741
822
|
* Preserves all existing listeners.
|
|
742
823
|
*/
|
|
743
824
|
_reconnectToAgent() {
|
|
744
|
-
if (this._unsubscribeAgent)
|
|
745
|
-
return; // Already connected
|
|
825
|
+
if (this._disposed || this._unsubscribeAgent)
|
|
826
|
+
return; // Already connected or disposed
|
|
746
827
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
747
828
|
}
|
|
748
829
|
/**
|
|
@@ -755,6 +836,8 @@ export class AgentSession {
|
|
|
755
836
|
if (this._disposed)
|
|
756
837
|
return;
|
|
757
838
|
this._disposed = true;
|
|
839
|
+
this._toolCostExecutions.clear();
|
|
840
|
+
this._pendingToolCosts.clear();
|
|
758
841
|
this._resetOutputThroughput();
|
|
759
842
|
const retryDisposeError = new Error("AgentSession disposed before retry settlement completed.");
|
|
760
843
|
const unsettledRuns = [...this._unsettledAgentRuns];
|
|
@@ -966,6 +1049,10 @@ export class AgentSession {
|
|
|
966
1049
|
if (this._disposed) {
|
|
967
1050
|
throw new Error(`Cannot invoke harness tool "${name}": the session has been disposed.`);
|
|
968
1051
|
}
|
|
1052
|
+
// SCRAMJET-DIVERGENCE: reject new session work before allocating invocation ownership (#598).
|
|
1053
|
+
if (this._compactionAbortController) {
|
|
1054
|
+
throw new Error(`Cannot invoke harness tool "${name}": manual compaction is in progress. Retry after it settles.`);
|
|
1055
|
+
}
|
|
969
1056
|
const tool = this._toolRegistry.get(name);
|
|
970
1057
|
if (!tool) {
|
|
971
1058
|
throw new Error(`Cannot invoke harness tool "${name}": no tool with that name is registered.`);
|
|
@@ -1231,7 +1318,7 @@ export class AgentSession {
|
|
|
1231
1318
|
// Check if we need to compact before sending (catches aborted responses)
|
|
1232
1319
|
const lastAssistant = this._findLastAssistantMessage();
|
|
1233
1320
|
if (lastAssistant) {
|
|
1234
|
-
await this._checkCompaction(lastAssistant,
|
|
1321
|
+
await this._checkCompaction(lastAssistant, "new_prompt");
|
|
1235
1322
|
}
|
|
1236
1323
|
// Build messages array (custom message if any, then user message)
|
|
1237
1324
|
messages = [];
|
|
@@ -1759,12 +1846,32 @@ export class AgentSession {
|
|
|
1759
1846
|
* @param customInstructions Optional instructions for the compaction summary
|
|
1760
1847
|
*/
|
|
1761
1848
|
async compact(customInstructions) {
|
|
1762
|
-
|
|
1763
|
-
this.
|
|
1764
|
-
|
|
1765
|
-
this.
|
|
1766
|
-
|
|
1849
|
+
// SCRAMJET-DIVERGENCE: acquire one owner before awaits and drain terminal persistence (#598).
|
|
1850
|
+
if (this._disposed)
|
|
1851
|
+
throw new Error("Cannot compact a disposed session.");
|
|
1852
|
+
if (this.isCompacting)
|
|
1853
|
+
throw new Error("Compaction or branch summarization is already in progress. Retry after it settles.");
|
|
1854
|
+
if (this.agent.hasActiveTransientHarnessTools) {
|
|
1855
|
+
throw new Error("A harness tool is still executing outside an Agent run. Retry compaction after its persisted settlement.");
|
|
1856
|
+
}
|
|
1857
|
+
const controller = new AbortController();
|
|
1858
|
+
this._compactionAbortController = controller;
|
|
1859
|
+
const drain = {};
|
|
1860
|
+
this._manualCompactionDrain = drain;
|
|
1861
|
+
const runs = [...this._unsettledAgentRuns];
|
|
1767
1862
|
try {
|
|
1863
|
+
this._resetOutputThroughput();
|
|
1864
|
+
this._disconnectFromAgent();
|
|
1865
|
+
this._unsubscribeAgent = this.agent.subscribe((event) => this._handleAgentEvent(event));
|
|
1866
|
+
this._emit({ type: "compaction_start", reason: "manual" });
|
|
1867
|
+
await this.abort();
|
|
1868
|
+
await this._agentEventQueue;
|
|
1869
|
+
if (drain.firstError)
|
|
1870
|
+
throw drain.firstError;
|
|
1871
|
+
if (this._disposed)
|
|
1872
|
+
throw new Error("Session disposed during compaction.");
|
|
1873
|
+
if (controller.signal.aborted)
|
|
1874
|
+
throw new Error("Compaction cancelled");
|
|
1768
1875
|
if (!this.model) {
|
|
1769
1876
|
throw new Error(formatNoModelSelectedMessage());
|
|
1770
1877
|
}
|
|
@@ -1788,7 +1895,7 @@ export class AgentSession {
|
|
|
1788
1895
|
preparation,
|
|
1789
1896
|
branchEntries: pathEntries,
|
|
1790
1897
|
customInstructions,
|
|
1791
|
-
signal:
|
|
1898
|
+
signal: controller.signal,
|
|
1792
1899
|
}));
|
|
1793
1900
|
if (result?.cancel) {
|
|
1794
1901
|
throw new Error("Compaction cancelled");
|
|
@@ -1811,13 +1918,15 @@ export class AgentSession {
|
|
|
1811
1918
|
}
|
|
1812
1919
|
else {
|
|
1813
1920
|
// Generate compaction result
|
|
1814
|
-
const result = await compact(preparation, this.model, apiKey, headers, customInstructions,
|
|
1921
|
+
const result = await compact(preparation, this.model, apiKey, headers, customInstructions, controller.signal, this.thinkingLevel);
|
|
1815
1922
|
summary = result.summary;
|
|
1816
1923
|
firstKeptEntryId = result.firstKeptEntryId;
|
|
1817
1924
|
tokensBefore = result.tokensBefore;
|
|
1818
1925
|
details = result.details;
|
|
1819
1926
|
}
|
|
1820
|
-
if (this.
|
|
1927
|
+
if (this._disposed)
|
|
1928
|
+
throw new Error("Session disposed during compaction.");
|
|
1929
|
+
if (controller.signal.aborted) {
|
|
1821
1930
|
throw new Error("Compaction cancelled");
|
|
1822
1931
|
}
|
|
1823
1932
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
|
@@ -1849,8 +1958,13 @@ export class AgentSession {
|
|
|
1849
1958
|
return compactionResult;
|
|
1850
1959
|
}
|
|
1851
1960
|
catch (error) {
|
|
1852
|
-
const
|
|
1853
|
-
const
|
|
1961
|
+
const failure = drain.firstError ?? error;
|
|
1962
|
+
const settlementError = failure instanceof Error ? failure : new Error(String(failure));
|
|
1963
|
+
this._releaseRetryForFailedRun(settlementError);
|
|
1964
|
+
for (const run of runs)
|
|
1965
|
+
this._completeAgentRun(run, settlementError);
|
|
1966
|
+
const message = settlementError.message;
|
|
1967
|
+
const aborted = message === "Compaction cancelled" || settlementError.name === "AbortError";
|
|
1854
1968
|
this._emit({
|
|
1855
1969
|
type: "compaction_end",
|
|
1856
1970
|
reason: "manual",
|
|
@@ -1859,9 +1973,11 @@ export class AgentSession {
|
|
|
1859
1973
|
willRetry: false,
|
|
1860
1974
|
errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
|
|
1861
1975
|
});
|
|
1862
|
-
throw
|
|
1976
|
+
throw failure;
|
|
1863
1977
|
}
|
|
1864
1978
|
finally {
|
|
1979
|
+
this._disconnectFromAgent();
|
|
1980
|
+
this._manualCompactionDrain = undefined;
|
|
1865
1981
|
this._compactionAbortController = undefined;
|
|
1866
1982
|
this._reconnectToAgent();
|
|
1867
1983
|
}
|
|
@@ -1888,15 +2004,43 @@ export class AgentSession {
|
|
|
1888
2004
|
* 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
1889
2005
|
*
|
|
1890
2006
|
* @param assistantMessage The assistant message to check
|
|
1891
|
-
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1892
2007
|
*/
|
|
1893
|
-
async _checkCompaction(assistantMessage,
|
|
2008
|
+
async _checkCompaction(assistantMessage, trigger = "settlement") {
|
|
2009
|
+
if (this._manualCompactionDrain)
|
|
2010
|
+
return;
|
|
1894
2011
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1895
2012
|
if (!settings.enabled)
|
|
1896
2013
|
return;
|
|
1897
|
-
//
|
|
1898
|
-
if (
|
|
2014
|
+
// SCRAMJET-DIVERGENCE: New prompts maintain context independently of failed-turn recovery.
|
|
2015
|
+
if (trigger === "settlement" && blocksFailureRecovery(assistantMessage))
|
|
1899
2016
|
return;
|
|
2017
|
+
if (trigger === "new_prompt") {
|
|
2018
|
+
const boundary = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
2019
|
+
const messages = this.agent.state.messages;
|
|
2020
|
+
const reverseIndex = [...messages]
|
|
2021
|
+
.reverse()
|
|
2022
|
+
.findIndex((message) => message.role === "assistant" &&
|
|
2023
|
+
message.origin !== "harness" &&
|
|
2024
|
+
message.stopReason !== "error" &&
|
|
2025
|
+
message.stopReason !== "aborted" &&
|
|
2026
|
+
message.provider === this.model?.provider &&
|
|
2027
|
+
message.model === this.model?.id &&
|
|
2028
|
+
Number.isFinite(calculateContextTokens(message.usage)) &&
|
|
2029
|
+
calculateContextTokens(message.usage) >= 0 &&
|
|
2030
|
+
(!boundary || message.timestamp > new Date(boundary.timestamp).getTime()) &&
|
|
2031
|
+
[message.usage.input, message.usage.output, message.usage.cacheRead, message.usage.cacheWrite].every((n) => Number.isFinite(n) && n >= 0));
|
|
2032
|
+
if (reverseIndex < 0)
|
|
2033
|
+
return;
|
|
2034
|
+
const sourceIndex = messages.length - reverseIndex - 1;
|
|
2035
|
+
const source = messages[sourceIndex];
|
|
2036
|
+
const tokens = calculateContextTokens(source.usage) +
|
|
2037
|
+
messages.slice(sourceIndex + 1).reduce((total, message) => total + estimateTokens(message), 0);
|
|
2038
|
+
const window = this.model?.contextWindow ?? 0;
|
|
2039
|
+
if (shouldCompact(tokens, window, settings) ||
|
|
2040
|
+
(window > settings.reserveTokens && tokens > (this.model?.maxInputTokens ?? Infinity)))
|
|
2041
|
+
await this._runAutoCompaction("threshold", false);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
1900
2044
|
// SCRAMJET-DIVERGENCE: Total context and independent input limits are separate constraints.
|
|
1901
2045
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
1902
2046
|
// Skip overflow check if the message came from a different model.
|
|
@@ -1969,9 +2113,10 @@ export class AgentSession {
|
|
|
1969
2113
|
*/
|
|
1970
2114
|
async _runAutoCompaction(reason, willRetry) {
|
|
1971
2115
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1972
|
-
|
|
2116
|
+
// SCRAMJET-DIVERGENCE: reentrant subscribers must observe compaction ownership (#598).
|
|
1973
2117
|
this._autoCompactionAbortController = new AbortController();
|
|
1974
2118
|
try {
|
|
2119
|
+
this._emit({ type: "compaction_start", reason });
|
|
1975
2120
|
if (!this.model) {
|
|
1976
2121
|
this._emit({
|
|
1977
2122
|
type: "compaction_end",
|
|
@@ -2316,7 +2461,13 @@ export class AgentSession {
|
|
|
2316
2461
|
definition,
|
|
2317
2462
|
sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
|
|
2318
2463
|
})),
|
|
2319
|
-
]
|
|
2464
|
+
];
|
|
2465
|
+
// SCRAMJET-DIVERGENCE: historical interpretation follows definition collision winners, not execution allowlists (#598).
|
|
2466
|
+
this._historicalCostDefinitions = new Map(this._baseToolDefinitions);
|
|
2467
|
+
for (const tool of allCustomTools) {
|
|
2468
|
+
this._historicalCostDefinitions.set(tool.definition.name, tool.definition);
|
|
2469
|
+
}
|
|
2470
|
+
const allowedCustomTools = allCustomTools.filter((tool) => isAllowedTool(tool.definition.name));
|
|
2320
2471
|
const definitionRegistry = new Map(Array.from(this._baseToolDefinitions.entries())
|
|
2321
2472
|
.filter(([name]) => isAllowedTool(name))
|
|
2322
2473
|
.map(([name, definition]) => [
|
|
@@ -2326,7 +2477,7 @@ export class AgentSession {
|
|
|
2326
2477
|
sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
|
|
2327
2478
|
},
|
|
2328
2479
|
]));
|
|
2329
|
-
for (const tool of
|
|
2480
|
+
for (const tool of allowedCustomTools) {
|
|
2330
2481
|
definitionRegistry.set(tool.definition.name, {
|
|
2331
2482
|
definition: tool.definition,
|
|
2332
2483
|
sourceInfo: tool.sourceInfo,
|
|
@@ -2346,7 +2497,7 @@ export class AgentSession {
|
|
|
2346
2497
|
})
|
|
2347
2498
|
.filter((entry) => entry !== undefined));
|
|
2348
2499
|
const runner = this._extensionRunner;
|
|
2349
|
-
const wrappedExtensionTools = wrapRegisteredTools(
|
|
2500
|
+
const wrappedExtensionTools = wrapRegisteredTools(allowedCustomTools, runner);
|
|
2350
2501
|
const wrappedBuiltInTools = wrapRegisteredTools(Array.from(this._baseToolDefinitions.values())
|
|
2351
2502
|
.filter((definition) => isAllowedTool(definition.name))
|
|
2352
2503
|
.map((definition) => ({
|
|
@@ -2451,20 +2602,30 @@ export class AgentSession {
|
|
|
2451
2602
|
// =========================================================================
|
|
2452
2603
|
// SCRAMJET-DIVERGENCE: persisted post-extension snapshots authoritatively classify and settle retries (#553).
|
|
2453
2604
|
_classifyRetry(message) {
|
|
2605
|
+
if ((message.origin === "harness" || message.stopReason === "aborted") && blocksFailureRecovery(message))
|
|
2606
|
+
return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
|
|
2454
2607
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2455
2608
|
if (isContextOverflow(message, Math.min(contextWindow, this.model?.maxInputTokens ?? Infinity))) {
|
|
2456
2609
|
return { kind: "context_overflow" };
|
|
2457
2610
|
}
|
|
2458
|
-
const structured =
|
|
2611
|
+
const structured = inspectFailureEvidence(message.diagnostics);
|
|
2459
2612
|
if (structured.status === "valid") {
|
|
2460
|
-
if (structured.
|
|
2461
|
-
return { kind: "
|
|
2613
|
+
if (structured.source !== "provider")
|
|
2614
|
+
return { kind: "do_not_retry", reason: "non_provider_failure", evidence: "none" };
|
|
2615
|
+
if (structured.suppression)
|
|
2616
|
+
return { kind: "do_not_retry", reason: structured.suppression, evidence: structured.family };
|
|
2617
|
+
if (structured.transient || ["missing_terminal_event", "missing_body"].includes(structured.category))
|
|
2618
|
+
return { kind: "retry", evidence: structured.family };
|
|
2462
2619
|
return {
|
|
2463
2620
|
kind: "do_not_retry",
|
|
2464
|
-
reason:
|
|
2465
|
-
|
|
2621
|
+
reason: ["unknown", "provider_error", "malformed_event"].includes(structured.category)
|
|
2622
|
+
? "structured_unknown"
|
|
2623
|
+
: "structured_non_transient",
|
|
2624
|
+
evidence: structured.family,
|
|
2466
2625
|
};
|
|
2467
2626
|
}
|
|
2627
|
+
if (structured.status === "conflicting")
|
|
2628
|
+
return { kind: "do_not_retry", reason: "invalid_failure_evidence", evidence: "none" };
|
|
2468
2629
|
if (structured.status === "malformed") {
|
|
2469
2630
|
return { kind: "do_not_retry", reason: "malformed_provider_diagnostic", evidence: "none" };
|
|
2470
2631
|
}
|
|
@@ -2498,7 +2659,7 @@ export class AgentSession {
|
|
|
2498
2659
|
this._appendAutoRetryRecord(record);
|
|
2499
2660
|
}
|
|
2500
2661
|
catch (error) {
|
|
2501
|
-
if (retry) {
|
|
2662
|
+
if (retry && !this._manualCompactionDrain) {
|
|
2502
2663
|
this._emit({
|
|
2503
2664
|
type: "auto_retry_end",
|
|
2504
2665
|
success: false,
|
|
@@ -2508,7 +2669,7 @@ export class AgentSession {
|
|
|
2508
2669
|
}
|
|
2509
2670
|
throw this._reportRetryFailure(error);
|
|
2510
2671
|
}
|
|
2511
|
-
if (retry && end)
|
|
2672
|
+
if (retry && end && !this._manualCompactionDrain)
|
|
2512
2673
|
this._emit(end);
|
|
2513
2674
|
}
|
|
2514
2675
|
_finishRetryWithoutSuccess(record, message) {
|
|
@@ -2561,9 +2722,8 @@ export class AgentSession {
|
|
|
2561
2722
|
async _handleRetryableError(message, evidence, run) {
|
|
2562
2723
|
const settings = this.settingsManager.getRetrySettings();
|
|
2563
2724
|
const attempt = (this._retry?.attempt ?? 0) + 1;
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
if (attempt > settings.maxRetries) {
|
|
2725
|
+
const cumulativeCap = Math.min(Number.MAX_SAFE_INTEGER, settings.maxRetries * 2);
|
|
2726
|
+
if ((this._retry?.attempt ?? 0) >= settings.maxRetries) {
|
|
2567
2727
|
const attemptsCompleted = attempt - 1;
|
|
2568
2728
|
this._finishRetry({
|
|
2569
2729
|
schemaVersion: 1,
|
|
@@ -2575,15 +2735,15 @@ export class AgentSession {
|
|
|
2575
2735
|
}, { type: "auto_retry_end", success: false, attempt: attemptsCompleted, finalError: message.errorMessage });
|
|
2576
2736
|
return false;
|
|
2577
2737
|
}
|
|
2578
|
-
if (this._runRetryCount
|
|
2579
|
-
const attemptsCompleted = this._runRetryCount
|
|
2738
|
+
if (this._runRetryCount >= cumulativeCap) {
|
|
2739
|
+
const attemptsCompleted = this._runRetryCount;
|
|
2580
2740
|
this._finishRetry({
|
|
2581
2741
|
schemaVersion: 1,
|
|
2582
2742
|
outcome: "exhausted",
|
|
2583
2743
|
reason: "cumulative_limit",
|
|
2584
2744
|
attemptsCompleted: this._bounded(attemptsCompleted),
|
|
2585
2745
|
maxAttempts: this._bounded(settings.maxRetries),
|
|
2586
|
-
cumulativeErrors: this._bounded(this._runRetryCount),
|
|
2746
|
+
cumulativeErrors: this._bounded(this._runRetryCount + 1),
|
|
2587
2747
|
}, {
|
|
2588
2748
|
type: "auto_retry_end",
|
|
2589
2749
|
success: false,
|
|
@@ -2592,7 +2752,10 @@ export class AgentSession {
|
|
|
2592
2752
|
});
|
|
2593
2753
|
return false;
|
|
2594
2754
|
}
|
|
2595
|
-
|
|
2755
|
+
this._runRetryCount++;
|
|
2756
|
+
const delayMs = settings.baseDelayMs === 0
|
|
2757
|
+
? 0
|
|
2758
|
+
: Math.min(2_147_483_647, settings.baseDelayMs * 2 ** Math.min(31, attempt - 1));
|
|
2596
2759
|
const previous = this._retry;
|
|
2597
2760
|
this._retry = undefined;
|
|
2598
2761
|
try {
|
|
@@ -2645,6 +2808,8 @@ export class AgentSession {
|
|
|
2645
2808
|
if (controller.signal.aborted || this._disposed) {
|
|
2646
2809
|
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2647
2810
|
}
|
|
2811
|
+
if (this._manualCompactionDrain)
|
|
2812
|
+
return this._finishCancelledRetry("cancelled_during_backoff", backoff);
|
|
2648
2813
|
// Hand the chain to the continuation before `continue()`; the run that eventually emits
|
|
2649
2814
|
// `agent_start` adopts it (see `_captureAgentRunSettlement`), however long Agent defers that start.
|
|
2650
2815
|
const continuing = { phase: "continuing", attempt, chain: run.chain };
|
|
@@ -3051,6 +3216,43 @@ export class AgentSession {
|
|
|
3051
3216
|
}
|
|
3052
3217
|
return result;
|
|
3053
3218
|
}
|
|
3219
|
+
// SCRAMJET-DIVERGENCE: whole-journal parent/tool costs plus reports awaiting successful append (#598).
|
|
3220
|
+
getRecordedSessionCost() {
|
|
3221
|
+
let total = 0;
|
|
3222
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
3223
|
+
if (entry.type !== "message")
|
|
3224
|
+
continue;
|
|
3225
|
+
const message = entry.message;
|
|
3226
|
+
if (message.role === "assistant") {
|
|
3227
|
+
if (validRecordedCost(message.usage?.cost?.total))
|
|
3228
|
+
total += message.usage.cost.total;
|
|
3229
|
+
}
|
|
3230
|
+
else if (message.role === "toolResult") {
|
|
3231
|
+
if (validRecordedCost(message.cost)) {
|
|
3232
|
+
total += message.cost;
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
const definition = this._historicalCostDefinitions.get(message.toolName);
|
|
3236
|
+
if (!definition?.getHistoricalCost)
|
|
3237
|
+
continue;
|
|
3238
|
+
try {
|
|
3239
|
+
const cost = definition.getHistoricalCost(message.details);
|
|
3240
|
+
if (validRecordedCost(cost))
|
|
3241
|
+
total += cost;
|
|
3242
|
+
}
|
|
3243
|
+
catch (error) {
|
|
3244
|
+
this._extensionRunner.emitError({
|
|
3245
|
+
extensionPath: `tool:${message.toolName}`,
|
|
3246
|
+
event: "historical_cost",
|
|
3247
|
+
error: `Failed to interpret historical tool cost: ${error instanceof Error ? error.message : String(error)}`,
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
for (const execution of this._pendingToolCosts)
|
|
3253
|
+
total += execution.cost ?? 0;
|
|
3254
|
+
return total;
|
|
3255
|
+
}
|
|
3054
3256
|
/**
|
|
3055
3257
|
* Get session statistics.
|
|
3056
3258
|
*/
|