@animalabs/agent-framework 0.6.7 → 0.6.8

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.
Files changed (36) hide show
  1. package/dist/src/agent.d.ts +1 -34
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +55 -137
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +0 -56
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +45 -1733
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/types/agent.d.ts +0 -12
  10. package/dist/src/types/agent.d.ts.map +1 -1
  11. package/dist/test/helpers/mock-membrane.js +1 -1
  12. package/dist/test/helpers/mock-membrane.js.map +1 -1
  13. package/dist/test/mcpl-awareness-barrier.test.js +1 -228
  14. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  15. package/dist/test/refusal-single-call.test.d.ts +2 -0
  16. package/dist/test/refusal-single-call.test.d.ts.map +1 -0
  17. package/dist/test/refusal-single-call.test.js +177 -0
  18. package/dist/test/refusal-single-call.test.js.map +1 -0
  19. package/dist/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +2 -2
  21. package/src/agent.ts +60 -226
  22. package/src/framework.ts +72 -2387
  23. package/src/types/agent.ts +0 -12
  24. package/dist/src/canonical-json.d.ts +0 -2
  25. package/dist/src/canonical-json.d.ts.map +0 -1
  26. package/dist/src/canonical-json.js +0 -23
  27. package/dist/src/canonical-json.js.map +0 -1
  28. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +0 -2
  29. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +0 -1
  30. package/dist/test/primary-summary-refusal-fallback-integration.test.js +0 -1955
  31. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +0 -1
  32. package/dist/test/primary-summary-refusal-fallback.test.d.ts +0 -2
  33. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +0 -1
  34. package/dist/test/primary-summary-refusal-fallback.test.js +0 -753
  35. package/dist/test/primary-summary-refusal-fallback.test.js.map +0 -1
  36. package/src/canonical-json.ts +0 -22
package/src/framework.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  import { join } from 'node:path';
2
2
  import { appendFileSync, mkdirSync } from 'node:fs';
3
- import { createHash } from 'node:crypto';
4
3
  import { JsStore } from '@animalabs/chronicle';
5
- import type { Membrane, ContentBlock, NormalizedRequest, NormalizedResponse, YieldingStream, ToolResult as MembraneToolResult, ToolResultContentBlock } from '@animalabs/membrane';
4
+ import type { Membrane, ContentBlock, NormalizedRequest, YieldingStream, ToolResult as MembraneToolResult, ToolResultContentBlock } from '@animalabs/membrane';
6
5
  import { MembraneError } from '@animalabs/membrane';
7
6
  import { ContextManager, PassthroughStrategy } from '@animalabs/context-manager';
8
7
  import type {
9
- BranchGenerationInfo,
10
8
  MessageId,
11
9
  MessageMetadata,
12
10
  MessageQuery,
13
11
  MessageQueryResult,
14
- PrimarySummaryContract,
15
- PrimarySummaryIdentity,
16
- PrimarySummaryProjection,
17
12
  StoredMessage,
18
13
  } from '@animalabs/context-manager';
19
14
  import type {
@@ -56,7 +51,7 @@ import type {
56
51
  SameRoundThinkTextPolicy,
57
52
  } from './types/index.js';
58
53
  import { ProcessQueueImpl } from './queue.js';
59
- import { Agent, type ActivationCompileArtifacts } from './agent.js';
54
+ import { Agent } from './agent.js';
60
55
  import { ModuleRegistry, isStateExistsError } from './module-registry.js';
61
56
  import { McplServerRegistry } from './mcpl/server-registry.js';
62
57
  import { FeatureSetManager } from './mcpl/feature-set-manager.js';
@@ -70,7 +65,6 @@ import { safeSlice } from './safe-slice.js';
70
65
  import type { WorkspaceModule } from './modules/workspace/index.js';
71
66
  import { toolResultDataToHistoryString } from './tool-result-history.js';
72
67
  import { splitProseSegments } from './prose-segments.js';
73
- import { canonicalJsonStringify } from './canonical-json.js';
74
68
 
75
69
  /**
76
70
  * Tools whose presence suppresses auto-routing of the surrounding prose,
@@ -127,12 +121,6 @@ const FRAMEWORK_STATE_ID = 'framework/state';
127
121
  const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
128
122
  const INFERENCE_LOG_ID = 'framework/inference-log';
129
123
  const PROCESS_LOG_ID = 'framework/process-log';
130
- const PRIMARY_SUMMARY_FALLBACK_STATE_ID = 'framework/primary-summary-fallback';
131
- const PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY = 'primarySummarySettlement';
132
- const PRIMARY_SUMMARY_SETTLEMENT_VERSION = 1;
133
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH = 1024;
134
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH = 256;
135
- const PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH = 256;
136
124
  const TURN_CHECKPOINTS_ID = 'framework/turn-checkpoints'; // legacy single-map layout, read-only fallback
137
125
  const TURN_CHECKPOINTS_TREE_ID = 'framework/turn-checkpoints/tree';
138
126
 
@@ -187,128 +175,6 @@ interface InferenceToolSnapshot {
187
175
  sameRoundThinkTextPolicy: SameRoundThinkTextPolicy;
188
176
  }
189
177
 
190
- type PrimarySummaryFallbackStatus =
191
- | 'success'
192
- | 'refusal'
193
- | 'held'
194
- | 'pending'
195
- | 'error'
196
- | 'timeout_unknown';
197
-
198
- interface PrimarySummaryFallbackRetryDispatch {
199
- originalRequestId: string;
200
- baselineRequestId: string;
201
- intentId: string;
202
- branch: { id: string; generation: number };
203
- candidateSummaries: PrimarySummaryIdentity[];
204
- originalArtifacts: ActivationCompileArtifacts;
205
- originalProjectionKeys: string[];
206
- expectedRetryRequestHash: string;
207
- requestBudgetTokens: number;
208
- originalRequestInputBoundTokens: number;
209
- originalProviderInputTokens?: number;
210
- }
211
-
212
- interface PrimarySummaryFallbackIntentRecord {
213
- intentId: string;
214
- createdAt: number;
215
- baselineRequestId: string;
216
- candidateSummaries: PrimarySummaryIdentity[];
217
- retryRequestHash: string;
218
- retryCompleteBoundTokens: number;
219
- requestBudgetTokens: number;
220
- retryRequestId?: string;
221
- dispatchedAt?: number;
222
- quarantinePersistedAt?: number;
223
- outputs?: PrimarySummaryFallbackOutputRecord[];
224
- toolDispatches?: PrimarySummaryFallbackToolDispatchRecord[];
225
- finalizedAt?: number;
226
- }
227
-
228
- interface PrimarySummaryFallbackOutputRecord {
229
- phase: string;
230
- contentHash: string;
231
- messageId: string;
232
- blockTypes: string[];
233
- persistedAt: number;
234
- }
235
-
236
- type PrimarySummarySettlementOutcome = 'success' | 'held';
237
- type PrimarySummarySettlementRole = 'assistant' | 'user';
238
- type PrimarySummarySettlementKind = 'assistant_output' | 'generated_tool_result';
239
-
240
- interface PrimarySummarySettlementMetadata {
241
- version: number;
242
- requestId: string;
243
- settlementId: string;
244
- agentName: string;
245
- dispatchKind: PrimarySummaryRequestRecord['dispatchKind'];
246
- outcome: PrimarySummarySettlementOutcome;
247
- stopReason?: string;
248
- holdReason?: string;
249
- providerInputTokens?: number;
250
- visibleAssistantOutput: boolean;
251
- executedToolCalls: number;
252
- branch: { id: string; name: string; generation: number };
253
- role: PrimarySummarySettlementRole;
254
- kind: PrimarySummarySettlementKind;
255
- entryIndex: number;
256
- entryCount: number;
257
- }
258
-
259
- interface PrimarySummaryLogicalSettlementEntry {
260
- messages: StoredMessage[];
261
- settlement: PrimarySummarySettlementMetadata;
262
- participant: string;
263
- content: ContentBlock[];
264
- bodyGroupId?: string;
265
- }
266
-
267
- interface PrimarySummaryFallbackToolDispatchRecord {
268
- callId: string;
269
- inputHash: string;
270
- recordedAt: number;
271
- }
272
-
273
- interface PrimarySummaryRequestRecord {
274
- requestId: string;
275
- agentName: string;
276
- namespace: string;
277
- timestamp: number;
278
- dispatchKind: 'primary' | 'primary_summary_fallback_retry';
279
- branch: { id: string; name: string; generation: number };
280
- projection: PrimarySummaryProjection | null;
281
- requestInputBoundTokens: number;
282
- requestCompleteBoundTokens: number;
283
- providerInputTokens?: number;
284
- systemHash: string;
285
- modelConfigHash: string;
286
- toolContractHash: string;
287
- stopReason?: string;
288
- visibleAssistantOutput: boolean;
289
- executedToolCalls: number;
290
- finalStatus: PrimarySummaryFallbackStatus;
291
- fallbackIntent?: PrimarySummaryFallbackIntentRecord;
292
- fallbackStatus?: PrimarySummaryFallbackStatus;
293
- fallbackHeldReason?: string;
294
- }
295
-
296
- interface PrimarySummaryFallbackState {
297
- requests: PrimarySummaryRequestRecord[];
298
- }
299
-
300
- interface ContextBudgetRestartDispatch {
301
- requestId: string;
302
- dispatchKind: 'primary' | 'primary_summary_fallback_retry';
303
- originalRequestId?: string;
304
- intentId?: string;
305
- }
306
-
307
- interface PendingInferenceRequest extends InferenceRequest {
308
- primarySummaryFallbackRetry?: PrimarySummaryFallbackRetryDispatch;
309
- contextBudgetRestart?: ContextBudgetRestartDispatch;
310
- }
311
-
312
178
  /**
313
179
  * Default inference policy - infer if any request exists for the agent.
314
180
  */
@@ -456,13 +322,6 @@ interface EphemeralRun {
456
322
  toolCallsCount: number;
457
323
  }
458
324
 
459
- interface ActiveStreamContext {
460
- requestId: string;
461
- trigger?: PendingInferenceRequest;
462
- compiledRequest?: NormalizedRequest;
463
- artifacts?: ActivationCompileArtifacts;
464
- }
465
-
466
325
  /** Cap an API error message for inline use in a rewind marker: keep enough to
467
326
  * identify the failure class without pasting a wall of provider JSON into the
468
327
  * agent's context. */
@@ -479,7 +338,7 @@ export class AgentFramework {
479
338
  private moduleRegistry: ModuleRegistry;
480
339
  private inferencePolicy: InferencePolicy;
481
340
  private errorPolicy: ErrorPolicy;
482
- private pendingRequests: PendingInferenceRequest[] = [];
341
+ private pendingRequests: InferenceRequest[] = [];
483
342
  /**
484
343
  * Per-agent channel that triggered the agent's CURRENT inference turn, if any
485
344
  * (item-3 redux). Read by the ChannelRegistry's `activeChannelResolver` to
@@ -508,8 +367,6 @@ export class AgentFramework {
508
367
  private processLoggingPersist: boolean;
509
368
  private processLoggingBroadcast: boolean;
510
369
  private activeStreams: Map<string, Promise<void>> = new Map();
511
- private activeStreamContexts: Map<string, ActiveStreamContext> = new Map();
512
- private primarySummaryStateLock: Promise<void> = Promise.resolve();
513
370
 
514
371
  /** Per-agent output locus pinned for the CURRENT logical turn (see
515
372
  * resolveTurnLocus in driveStream). Lives here — not in driveStream
@@ -765,12 +622,6 @@ export class AgentFramework {
765
622
  }
766
623
  }
767
624
 
768
- try {
769
- store.registerState({ id: PRIMARY_SUMMARY_FALLBACK_STATE_ID, strategy: 'snapshot' });
770
- } catch {
771
- // Already registered
772
- }
773
-
774
625
  const discordAwarenessOutboxPath = config.discordAwarenessOutboxPath
775
626
  ?? (config.storePath ? defaultDiscordAwarenessOutboxPath(config.storePath) : undefined);
776
627
  const discordAwarenessOutbox = discordAwarenessOutboxPath
@@ -815,15 +666,14 @@ export class AgentFramework {
815
666
  }
816
667
  }
817
668
 
669
+ // Restore persisted usage data (if any) from prior session
670
+ framework.restoreUsageState();
671
+
818
672
  // Create agents
819
673
  for (const agentConfig of config.agents) {
820
674
  await framework.createAgent(agentConfig);
821
675
  }
822
676
 
823
- // Restore persisted usage data (if any) from prior session
824
- framework.restoreUsageState();
825
- await framework.reconcilePrimarySummaryFallbackStateOnStartup();
826
-
827
677
  // Finish any branch-local suppression interrupted after Chronicle switched
828
678
  // branches. This runs before modules, MCPL connections, or inbound traffic.
829
679
  await framework.resumePreparedDiscordSuppressions();
@@ -1080,7 +930,7 @@ export class AgentFramework {
1080
930
  const queued = [...this.agents.values()].flatMap((agent) => {
1081
931
  const cm = agent.getContextManager();
1082
932
  const tools = this.getToolsForAgent(agent.name).filter((tool) => agent.canUseTool(tool.name));
1083
- cm.setToolDefinitions?.(tools);
933
+ cm.setToolDefinitions(tools);
1084
934
  if (cm.isReady()) return [];
1085
935
  const pending = cm.getPendingWork()?.description;
1086
936
  const progress = this.contextProgress(cm);
@@ -1723,1338 +1573,22 @@ export class AgentFramework {
1723
1573
 
1724
1574
  const agent = new Agent(config, contextManager, this.membrane);
1725
1575
 
1726
- const cleanup = () => {
1727
- // Don't close the store — it's shared. Just release the CM.
1728
- // Data persists in the store under the namespace for investigation.
1729
- };
1730
-
1731
- return { agent, contextManager, cleanup };
1732
- }
1733
-
1734
- private createDeferred<T>(): Deferred<T> {
1735
- let resolve!: (value: T) => void;
1736
- let reject!: (reason?: unknown) => void;
1737
- const promise = new Promise<T>((res, rej) => {
1738
- resolve = res;
1739
- reject = rej;
1740
- });
1741
- return { promise, resolve, reject };
1742
- }
1743
-
1744
- private hashJson(value: unknown): string {
1745
- return createHash('sha256').update(canonicalJsonStringify(value), 'utf8').digest('hex');
1746
- }
1747
-
1748
- private estimateStringTokens(text: string, overhead = 0): number {
1749
- return Math.ceil(Buffer.byteLength(text, 'utf8') / 4) + overhead;
1750
- }
1751
-
1752
- private estimateUnknownTokens(value: unknown, overhead = 0): number {
1753
- return this.estimateStringTokens(JSON.stringify(value) ?? 'null', overhead);
1754
- }
1755
-
1756
- private estimateRequestInputBoundTokens(request: NormalizedRequest): number {
1757
- let total = 96;
1758
- total += this.estimateStringTokens(request.system ?? '', 48);
1759
- for (const message of request.messages) {
1760
- total += 24;
1761
- total += this.estimateStringTokens(message.participant, 8);
1762
- if ((message as { cacheBreakpoint?: boolean }).cacheBreakpoint) total += 8;
1763
- for (const block of message.content) {
1764
- switch (block.type) {
1765
- case 'text':
1766
- total += this.estimateStringTokens(block.text, 24);
1767
- break;
1768
- case 'thinking':
1769
- total += this.estimateStringTokens(block.thinking, 32);
1770
- total += block.signature ? this.estimateStringTokens(block.signature, 8) : 0;
1771
- break;
1772
- case 'redacted_thinking':
1773
- total += this.estimateUnknownTokens(block, 32);
1774
- break;
1775
- default:
1776
- total += this.estimateUnknownTokens(block, 32);
1777
- break;
1778
- }
1779
- }
1780
- }
1781
- if (request.tools && request.tools.length > 0) {
1782
- total += 32;
1783
- for (const tool of request.tools) {
1784
- total += this.estimateUnknownTokens({
1785
- name: tool.name,
1786
- description: tool.description,
1787
- inputSchema: tool.inputSchema,
1788
- }, 48);
1789
- }
1790
- }
1791
- total += this.estimateUnknownTokens(request.config, 48);
1792
- if (request.providerParams !== undefined) {
1793
- total += this.estimateUnknownTokens(request.providerParams, 32);
1794
- }
1795
- total += request.promptCaching ? 16 : 0;
1796
- total += request.cacheTtl ? this.estimateStringTokens(request.cacheTtl, 8) : 0;
1797
- total += request.assistantParticipant
1798
- ? this.estimateStringTokens(request.assistantParticipant, 8)
1799
- : 0;
1800
- return total;
1801
- }
1802
-
1803
- private estimateRequestCompleteBoundTokens(request: NormalizedRequest): number {
1804
- const reserve = typeof request.config.maxTokens === 'number' && Number.isFinite(request.config.maxTokens)
1805
- ? request.config.maxTokens
1806
- : 0;
1807
- return this.estimateRequestInputBoundTokens(request) + reserve + 96;
1808
- }
1809
-
1810
- private primarySummaryAdmissionBoundTokens(
1811
- originalRequestInputTokens: number,
1812
- expandedRequest: NormalizedRequest,
1813
- originalProviderInputTokens: number | undefined,
1814
- ): number {
1815
- const expandedInput = this.estimateRequestInputBoundTokens(expandedRequest);
1816
- const expandedComplete = this.estimateRequestCompleteBoundTokens(expandedRequest);
1817
- if (originalProviderInputTokens === undefined) return expandedComplete;
1818
- const reserve = typeof expandedRequest.config.maxTokens === 'number' && Number.isFinite(expandedRequest.config.maxTokens)
1819
- ? expandedRequest.config.maxTokens
1820
- : 0;
1821
- const positiveDelta = Math.max(0, expandedInput - originalRequestInputTokens);
1822
- return Math.max(
1823
- expandedComplete,
1824
- originalProviderInputTokens + positiveDelta + reserve + 96,
1825
- );
1826
- }
1827
-
1828
- private readPrimarySummaryFallbackState(): PrimarySummaryFallbackState {
1829
- const raw = (this.store as { getStateJson?: (id: string) => unknown } | undefined)
1830
- ?.getStateJson?.(PRIMARY_SUMMARY_FALLBACK_STATE_ID);
1831
- if (!raw || typeof raw !== 'object') return { requests: [] };
1832
- const requests = Array.isArray((raw as { requests?: unknown[] }).requests)
1833
- ? ((raw as { requests: PrimarySummaryRequestRecord[] }).requests)
1834
- : [];
1835
- return { requests };
1836
- }
1837
-
1838
- private writePrimarySummaryFallbackState(state: PrimarySummaryFallbackState): void {
1839
- this.store.setStateJson(PRIMARY_SUMMARY_FALLBACK_STATE_ID, state);
1840
- }
1841
-
1842
- private async withPrimarySummaryStateLock<T>(work: () => Promise<T> | T): Promise<T> {
1843
- const previous = this.primarySummaryStateLock;
1844
- let release!: () => void;
1845
- this.primarySummaryStateLock = new Promise<void>((resolve) => { release = resolve; });
1846
- await previous;
1847
- try {
1848
- return await work();
1849
- } finally {
1850
- release();
1851
- }
1852
- }
1853
-
1854
- private async reconcilePrimarySummaryFallbackStateOnStartup(): Promise<void> {
1855
- const alerts: Array<{ agentName: string; requestId: string; reason: string }> = [];
1856
- const unsettledRequests = (await this.withPrimarySummaryStateLock(() =>
1857
- this.readPrimarySummaryFallbackState().requests
1858
- .filter((record) =>
1859
- record.finalStatus === 'pending'
1860
- || record.finalStatus === 'refusal'
1861
- || record.fallbackStatus === 'pending',
1862
- )
1863
- .map((record) => structuredClone(record)),
1864
- )) as PrimarySummaryRequestRecord[];
1865
-
1866
- const resolvedRequestIds = new Set<string>();
1867
- for (const record of unsettledRequests) {
1868
- const result = await this.finalizePrimarySummarySettlementOnStartup(record);
1869
- if (result === 'finalized') {
1870
- resolvedRequestIds.add(record.requestId);
1871
- continue;
1872
- }
1873
- if (result && typeof result === 'object' && 'invalidReason' in result) {
1874
- resolvedRequestIds.add(record.requestId);
1875
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason: result.invalidReason });
1876
- await this.markPrimarySummaryRequestRestartFailure(record, result.invalidReason);
1877
- }
1878
- }
1879
-
1880
- await this.withPrimarySummaryStateLock(() => {
1881
- const state = this.readPrimarySummaryFallbackState();
1882
- let changed = false;
1883
- state.requests = state.requests.map((record) => {
1884
- if (resolvedRequestIds.has(record.requestId)) return record;
1885
- if (record.dispatchKind === 'primary_summary_fallback_retry' && (
1886
- record.finalStatus === 'pending' || record.finalStatus === 'refusal'
1887
- )) {
1888
- changed = true;
1889
- const reason = 'primary_summary_fallback_unresolved_on_restart';
1890
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1891
- return {
1892
- ...record,
1893
- finalStatus: 'held',
1894
- fallbackHeldReason: reason,
1895
- };
1896
- }
1897
- if (record.fallbackIntent && record.fallbackStatus === 'pending') {
1898
- changed = true;
1899
- const reason = 'primary_summary_fallback_unresolved_on_restart';
1900
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1901
- return {
1902
- ...record,
1903
- fallbackStatus: 'held',
1904
- fallbackHeldReason: reason,
1905
- };
1906
- }
1907
- if (record.finalStatus === 'refusal') {
1908
- changed = true;
1909
- const reason = 'primary_summary_request_unresolved_on_restart';
1910
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1911
- return {
1912
- ...record,
1913
- finalStatus: 'held',
1914
- fallbackStatus: 'held',
1915
- fallbackHeldReason: reason,
1916
- };
1917
- }
1918
- if (record.finalStatus === 'pending') {
1919
- changed = true;
1920
- const reason = 'primary_summary_request_unresolved_on_restart';
1921
- alerts.push({ agentName: record.agentName, requestId: record.requestId, reason });
1922
- return {
1923
- ...record,
1924
- finalStatus: 'timeout_unknown',
1925
- fallbackHeldReason: reason,
1926
- };
1927
- }
1928
- return record;
1929
- });
1930
- if (changed) this.writePrimarySummaryFallbackState({ requests: state.requests.slice(-256) });
1931
- });
1932
- for (const alert of alerts) {
1933
- this.opsAlert('refusal-held', alert.agentName, alert.reason, {
1934
- data: { requestId: alert.requestId },
1935
- });
1936
- }
1937
- }
1938
-
1939
- private primarySummaryIdentityKey(identity: PrimarySummaryIdentity): string {
1940
- return [
1941
- identity.id,
1942
- identity.contentHash,
1943
- identity.carrierHash,
1944
- identity.sourceLeafHash,
1945
- ].join('\u0000');
1946
- }
1947
-
1948
- private primarySummaryProjectionKeys(projection: PrimarySummaryProjection | null): string[] {
1949
- return projection?.selectedSummaries.map((selection) =>
1950
- this.primarySummaryIdentityKey(selection.identity),
1951
- ) ?? [];
1952
- }
1953
-
1954
- private primarySummaryFallbackConfig(agent: Agent): {
1955
- enabled: boolean;
1956
- maxNewSummaries: number;
1957
- requestBudgetTokens: number;
1958
- } {
1959
- const raw = (agent.refusalHandling as {
1960
- primarySummaryFallback?: {
1961
- enabled?: boolean;
1962
- maxNewSummaries?: number;
1963
- requestBudgetTokens?: number;
1964
- };
1965
- } | undefined)?.primarySummaryFallback;
1966
- const fallbackBudgetBase = agent.contextBudgetTokens ?? 100_000;
1967
- return {
1968
- enabled: raw?.enabled === true,
1969
- maxNewSummaries:
1970
- typeof raw?.maxNewSummaries === 'number' && Number.isSafeInteger(raw.maxNewSummaries)
1971
- ? Math.max(0, raw.maxNewSummaries)
1972
- : 4,
1973
- requestBudgetTokens:
1974
- typeof raw?.requestBudgetTokens === 'number' && Number.isFinite(raw.requestBudgetTokens) && raw.requestBudgetTokens > 0
1975
- ? raw.requestBudgetTokens
1976
- : fallbackBudgetBase + agent.maxTokens,
1977
- };
1978
- }
1979
-
1980
- private latestCompatibleHealthyPrimaryRequest(
1981
- current: PrimarySummaryRequestRecord,
1982
- state: PrimarySummaryFallbackState,
1983
- ): PrimarySummaryRequestRecord | null {
1984
- for (let i = state.requests.length - 1; i >= 0; i--) {
1985
- const candidate = state.requests[i]!;
1986
- if (candidate.requestId === current.requestId) continue;
1987
- if (candidate.agentName !== current.agentName) continue;
1988
- if (candidate.namespace !== current.namespace) continue;
1989
- if (candidate.branch.id !== current.branch.id) continue;
1990
- if (candidate.branch.generation !== current.branch.generation) continue;
1991
- if (candidate.systemHash !== current.systemHash) continue;
1992
- if (candidate.modelConfigHash !== current.modelConfigHash) continue;
1993
- if (candidate.toolContractHash !== current.toolContractHash) continue;
1994
- if (candidate.dispatchKind !== 'primary') continue;
1995
- if (candidate.fallbackIntent || candidate.fallbackStatus) continue;
1996
- if (candidate.finalStatus !== 'success') continue;
1997
- return candidate;
1998
- }
1999
- return null;
2000
- }
2001
-
2002
- private async persistPrimarySummaryRequestRecord(record: PrimarySummaryRequestRecord): Promise<void> {
2003
- await this.withPrimarySummaryStateLock(() => {
2004
- const state = this.readPrimarySummaryFallbackState();
2005
- const requests = state.requests.filter((entry) => entry.requestId !== record.requestId);
2006
- requests.push(record);
2007
- this.writePrimarySummaryFallbackState({ requests: requests.slice(-256) });
2008
- });
2009
- }
2010
-
2011
- private async updatePrimarySummaryRequestRecord(
2012
- requestId: string,
2013
- update: (record: PrimarySummaryRequestRecord) => PrimarySummaryRequestRecord,
2014
- ): Promise<PrimarySummaryRequestRecord | null> {
2015
- return this.withPrimarySummaryStateLock(() => {
2016
- const state = this.readPrimarySummaryFallbackState();
2017
- const index = state.requests.findIndex((entry) => entry.requestId === requestId);
2018
- if (index < 0) return null;
2019
- state.requests[index] = update(state.requests[index]!);
2020
- this.writePrimarySummaryFallbackState({ requests: state.requests.slice(-256) });
2021
- return state.requests[index]!;
2022
- });
2023
- }
2024
-
2025
- private async getPrimarySummaryRequestRecord(requestId: string): Promise<PrimarySummaryRequestRecord | null> {
2026
- return this.withPrimarySummaryStateLock(() => {
2027
- const state = this.readPrimarySummaryFallbackState();
2028
- return state.requests.find((entry) => entry.requestId === requestId) ?? null;
2029
- });
2030
- }
2031
-
2032
- private primarySummaryContractsMatch(
2033
- left: PrimarySummaryContract & { systemHash: string },
2034
- right: PrimarySummaryContract & { systemHash: string },
2035
- ): boolean {
2036
- return left.systemHash === right.systemHash
2037
- && left.modelConfigHash === right.modelConfigHash
2038
- && left.toolContractHash === right.toolContractHash;
2039
- }
2040
-
2041
- private primarySummaryFallbackOutputPhaseHash(blocks: ReadonlyArray<ContentBlock>): string {
2042
- return this.hashJson(blocks);
2043
- }
2044
-
2045
- private primarySummaryFallbackBlockTypes(blocks: ReadonlyArray<ContentBlock>): string[] {
2046
- return blocks.map((block) => block.type);
2047
- }
2048
-
2049
- private static readonly NON_EXECUTED_TOOL_RESULT_TEXT = '[tool not executed]';
2050
-
2051
- private buildNonExecutedToolResults(
2052
- blocks: ReadonlyArray<ContentBlock>,
2053
- ): ContentBlock[] {
2054
- const seenToolUseIds = new Set<string>();
2055
- return blocks
2056
- .filter((block): block is ContentBlock & { type: 'tool_use'; id: string } => block.type === 'tool_use')
2057
- .filter((block) => {
2058
- if (seenToolUseIds.has(block.id)) return false;
2059
- seenToolUseIds.add(block.id);
2060
- return true;
2061
- })
2062
- .map((block) => ({
2063
- type: 'tool_result' as const,
2064
- toolUseId: block.id,
2065
- content: AgentFramework.NON_EXECUTED_TOOL_RESULT_TEXT,
2066
- isError: true,
2067
- }));
2068
- }
2069
-
2070
- private buildPrimarySummarySettlementId(
2071
- record: Pick<PrimarySummaryRequestRecord, 'dispatchKind'>,
2072
- outcome: PrimarySummarySettlementOutcome,
2073
- ): string {
2074
- return `${record.dispatchKind}:${outcome}:terminal:v${PRIMARY_SUMMARY_SETTLEMENT_VERSION}`;
2075
- }
2076
-
2077
- private buildPrimarySummarySettlementMetadata(
2078
- record: PrimarySummaryRequestRecord,
2079
- options: {
2080
- outcome: PrimarySummarySettlementOutcome;
2081
- settlementId?: string;
2082
- stopReason?: string;
2083
- holdReason?: string;
2084
- providerInputTokens?: number;
2085
- visibleAssistantOutput: boolean;
2086
- executedToolCalls: number;
2087
- entryIndex: number;
2088
- entryCount: number;
2089
- role: PrimarySummarySettlementRole;
2090
- kind: PrimarySummarySettlementKind;
2091
- },
2092
- ): MessageMetadata {
2093
- const settlementId = options.settlementId ?? this.buildPrimarySummarySettlementId(record, options.outcome);
2094
- return {
2095
- [PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY]: {
2096
- version: PRIMARY_SUMMARY_SETTLEMENT_VERSION,
2097
- requestId: record.requestId,
2098
- settlementId,
2099
- agentName: record.agentName,
2100
- dispatchKind: record.dispatchKind,
2101
- outcome: options.outcome,
2102
- ...(options.stopReason ? { stopReason: options.stopReason } : {}),
2103
- ...(options.holdReason ? { holdReason: options.holdReason } : {}),
2104
- ...(options.providerInputTokens !== undefined
2105
- ? { providerInputTokens: options.providerInputTokens }
2106
- : {}),
2107
- visibleAssistantOutput: options.visibleAssistantOutput,
2108
- executedToolCalls: options.executedToolCalls,
2109
- branch: {
2110
- id: record.branch.id,
2111
- name: record.branch.name,
2112
- generation: record.branch.generation,
2113
- },
2114
- role: options.role,
2115
- kind: options.kind,
2116
- entryIndex: options.entryIndex,
2117
- entryCount: options.entryCount,
2118
- } satisfies PrimarySummarySettlementMetadata,
2119
- };
2120
- }
2121
-
2122
- private isBoundedNonEmptyString(value: unknown, maxLength: number): value is string {
2123
- return typeof value === 'string' && value.length > 0 && value.length <= maxLength;
2124
- }
2125
-
2126
- private isSafeNonNegativeInteger(value: unknown): value is number {
2127
- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
2128
- }
2129
-
2130
- private primarySummarySettlementRequestId(message: StoredMessage): string | null {
2131
- const raw = message.metadata?.[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY];
2132
- if (!raw || typeof raw !== 'object') return null;
2133
- const requestId = (raw as { requestId?: unknown }).requestId;
2134
- return typeof requestId === 'string' ? requestId : null;
2135
- }
2136
-
2137
- private parsePrimarySummarySettlementMetadata(
2138
- message: StoredMessage,
2139
- ): PrimarySummarySettlementMetadata | { invalidReason: string } | null {
2140
- const raw = message.metadata?.[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY];
2141
- if (!raw || typeof raw !== 'object') return null;
2142
- const candidate = raw as Partial<PrimarySummarySettlementMetadata>;
2143
- if (
2144
- candidate.version !== PRIMARY_SUMMARY_SETTLEMENT_VERSION ||
2145
- !this.isBoundedNonEmptyString(candidate.requestId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
2146
- !this.isBoundedNonEmptyString(candidate.settlementId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
2147
- !this.isBoundedNonEmptyString(candidate.agentName, PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH) ||
2148
- (candidate.dispatchKind !== 'primary' && candidate.dispatchKind !== 'primary_summary_fallback_retry') ||
2149
- (candidate.outcome !== 'success' && candidate.outcome !== 'held') ||
2150
- typeof candidate.visibleAssistantOutput !== 'boolean' ||
2151
- !this.isSafeNonNegativeInteger(candidate.executedToolCalls) ||
2152
- !this.isSafeNonNegativeInteger(candidate.entryIndex) ||
2153
- !this.isSafeNonNegativeInteger(candidate.entryCount) ||
2154
- (candidate.role !== 'assistant' && candidate.role !== 'user') ||
2155
- (candidate.kind !== 'assistant_output' && candidate.kind !== 'generated_tool_result') ||
2156
- !candidate.branch ||
2157
- !this.isBoundedNonEmptyString(candidate.branch.id, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH) ||
2158
- !this.isBoundedNonEmptyString(candidate.branch.name, PRIMARY_SUMMARY_SETTLEMENT_MAX_NAME_LENGTH) ||
2159
- !this.isSafeNonNegativeInteger(candidate.branch.generation) ||
2160
- candidate.entryCount < 1 ||
2161
- candidate.entryIndex >= candidate.entryCount ||
2162
- (candidate.stopReason !== undefined
2163
- && !this.isBoundedNonEmptyString(candidate.stopReason, PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH)) ||
2164
- (candidate.holdReason !== undefined
2165
- && !this.isBoundedNonEmptyString(candidate.holdReason, PRIMARY_SUMMARY_SETTLEMENT_MAX_REASON_LENGTH)) ||
2166
- (candidate.providerInputTokens !== undefined
2167
- && !this.isSafeNonNegativeInteger(candidate.providerInputTokens)) ||
2168
- (candidate.outcome === 'held' && candidate.holdReason === undefined) ||
2169
- (candidate.outcome === 'success' && candidate.holdReason !== undefined)
2170
- ) {
2171
- return { invalidReason: 'primary_summary_settlement_metadata_invalid_on_restart' };
2172
- }
2173
- return candidate as PrimarySummarySettlementMetadata;
2174
- }
2175
-
2176
- private primarySummarySettlementMetadataConsistent(
2177
- left: PrimarySummarySettlementMetadata,
2178
- right: PrimarySummarySettlementMetadata,
2179
- ): boolean {
2180
- return left.version === right.version
2181
- && left.requestId === right.requestId
2182
- && left.settlementId === right.settlementId
2183
- && left.agentName === right.agentName
2184
- && left.dispatchKind === right.dispatchKind
2185
- && left.outcome === right.outcome
2186
- && left.stopReason === right.stopReason
2187
- && left.holdReason === right.holdReason
2188
- && left.providerInputTokens === right.providerInputTokens
2189
- && left.visibleAssistantOutput === right.visibleAssistantOutput
2190
- && left.executedToolCalls === right.executedToolCalls
2191
- && left.branch.id === right.branch.id
2192
- && left.branch.name === right.branch.name
2193
- && left.branch.generation === right.branch.generation
2194
- && left.entryCount === right.entryCount;
2195
- }
2196
-
2197
- private primarySummaryShardEnvelopeConsistent(
2198
- participant: string,
2199
- left: PrimarySummarySettlementMetadata,
2200
- candidateParticipant: string,
2201
- right: PrimarySummarySettlementMetadata,
2202
- ): boolean {
2203
- return participant === candidateParticipant
2204
- && this.primarySummarySettlementMetadataConsistent(left, right)
2205
- && left.role === right.role
2206
- && left.kind === right.kind
2207
- && left.entryIndex === right.entryIndex;
2208
- }
2209
-
2210
- private primarySummaryLogicalSettlementEntries(
2211
- agent: Agent,
2212
- requestId: string,
2213
- ): PrimarySummaryLogicalSettlementEntry[] | { invalidReason: string } {
2214
- const allMessages = agent.getContextManager().getAllMessages()
2215
- .slice()
2216
- .sort((left, right) => left.sequence - right.sequence);
2217
- const entries: PrimarySummaryLogicalSettlementEntry[] = [];
2218
- for (let index = 0; index < allMessages.length;) {
2219
- const message = allMessages[index]!;
2220
- if (this.primarySummarySettlementRequestId(message) !== requestId) {
2221
- index++;
2222
- continue;
2223
- }
2224
- const parsed = this.parsePrimarySummarySettlementMetadata(message);
2225
- if (!parsed) {
2226
- index++;
2227
- continue;
2228
- }
2229
- if ('invalidReason' in parsed) return parsed;
2230
-
2231
- if (message.bodyGroupId === undefined) {
2232
- if (message.shardIndex !== undefined) {
2233
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2234
- }
2235
- entries.push({
2236
- messages: [message],
2237
- settlement: parsed,
2238
- participant: message.participant,
2239
- content: message.content,
2240
- });
2241
- index++;
2242
- continue;
2243
- }
2244
-
2245
- const groupId = message.bodyGroupId;
2246
- if (!this.isBoundedNonEmptyString(groupId, PRIMARY_SUMMARY_SETTLEMENT_MAX_ID_LENGTH)) {
2247
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2248
- }
2249
- if (index > 0 && allMessages[index - 1]?.bodyGroupId === groupId) {
2250
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2251
- }
2252
-
2253
- const groupMessages: StoredMessage[] = [];
2254
- while (index < allMessages.length && allMessages[index]!.bodyGroupId === groupId) {
2255
- groupMessages.push(allMessages[index]!);
2256
- index++;
2257
- }
2258
- if (allMessages.slice(index).some((candidate) => candidate.bodyGroupId === groupId)) {
2259
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2260
- }
2261
-
2262
- const orderedGroupMessages: StoredMessage[] = [];
2263
- let expectedShardIndex = 0;
2264
- for (const groupMessage of groupMessages) {
2265
- if (this.primarySummarySettlementRequestId(groupMessage) !== requestId) {
2266
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2267
- }
2268
- const groupParsed = this.parsePrimarySummarySettlementMetadata(groupMessage);
2269
- if (!groupParsed) {
2270
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2271
- }
2272
- if ('invalidReason' in groupParsed) return groupParsed;
2273
- if (!this.primarySummaryShardEnvelopeConsistent(
2274
- message.participant,
2275
- parsed,
2276
- groupMessage.participant,
2277
- groupParsed,
2278
- )) {
2279
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2280
- }
2281
- if (groupMessage.shardIndex !== expectedShardIndex) {
2282
- return { invalidReason: 'primary_summary_settlement_shard_group_invalid_on_restart' };
2283
- }
2284
- orderedGroupMessages.push(groupMessage);
2285
- expectedShardIndex++;
2286
- }
2287
-
2288
- entries.push({
2289
- messages: orderedGroupMessages,
2290
- settlement: parsed,
2291
- participant: message.participant,
2292
- content: orderedGroupMessages.flatMap((groupMessage) => groupMessage.content),
2293
- bodyGroupId: groupId,
2294
- });
2295
- }
2296
- return entries;
2297
- }
2298
-
2299
- private primarySummaryContentMatches(
2300
- left: ReadonlyArray<ContentBlock>,
2301
- right: ReadonlyArray<ContentBlock>,
2302
- ): boolean {
2303
- return this.primarySummaryFallbackOutputPhaseHash(left) === this.primarySummaryFallbackOutputPhaseHash(right);
2304
- }
2305
-
2306
- private async persistPrimarySummaryChronicleEntries(
2307
- agent: Agent,
2308
- entries: Array<{ participant: string; content: ContentBlock[]; metadata?: MessageMetadata }>,
2309
- ): Promise<string[]> {
2310
- const messageIds: string[] = [];
2311
- for (const entry of entries) {
2312
- if (entry.content.length === 0) continue;
2313
- messageIds.push(agent.getContextManager().addMessage(entry.participant, entry.content, entry.metadata));
2314
- }
2315
- return messageIds;
2316
- }
2317
-
2318
- private async markPrimarySummaryRequestRestartFailure(
2319
- record: PrimarySummaryRequestRecord,
2320
- reason: string,
2321
- ): Promise<void> {
2322
- if (record.dispatchKind === 'primary_summary_fallback_retry') {
2323
- const parent = await this.findPrimarySummaryRetryParent(record.requestId);
2324
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2325
- ...entry,
2326
- finalStatus: 'held',
2327
- fallbackHeldReason: reason,
2328
- }));
2329
- if (parent) {
2330
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => ({
2331
- ...entry,
2332
- finalStatus: 'held',
2333
- fallbackStatus: 'held',
2334
- fallbackHeldReason: reason,
2335
- }));
2336
- }
2337
- return;
2338
- }
2339
- await this.updatePrimarySummaryRequestRecord(record.requestId, (current) => ({
2340
- ...current,
2341
- finalStatus: 'held',
2342
- fallbackStatus: 'held',
2343
- fallbackHeldReason: reason,
2344
- }));
2345
- }
2346
-
2347
- private async persistPrimarySummaryHeldOutput(
2348
- agent: Agent,
2349
- requestId: string,
2350
- holdReason: string,
2351
- options: {
2352
- stopReason?: string;
2353
- providerInputTokens?: number;
2354
- visibleAssistantOutput: boolean;
2355
- executedToolCalls: number;
2356
- assistantBlocks: ContentBlock[];
2357
- retryDispatch?: PrimarySummaryFallbackRetryDispatch;
2358
- retryPhase?: string;
2359
- holdData?: Record<string, unknown>;
2360
- },
2361
- ): Promise<void> {
2362
- const record = await this.getPrimarySummaryRequestRecord(requestId);
2363
- if (!record) {
2364
- throw new Error(`Primary summary request '${requestId}' disappeared before held-output persistence`);
2365
- }
2366
- const placeholderBlocks = this.buildNonExecutedToolResults(options.assistantBlocks);
2367
- const entryCount = placeholderBlocks.length > 0 ? 2 : 1;
2368
- const assistantMetadata = this.buildPrimarySummarySettlementMetadata(record, {
2369
- outcome: 'held',
2370
- stopReason: options.stopReason,
2371
- holdReason,
2372
- providerInputTokens: options.providerInputTokens,
2373
- visibleAssistantOutput: options.visibleAssistantOutput,
2374
- executedToolCalls: options.executedToolCalls,
2375
- entryIndex: 0,
2376
- entryCount,
2377
- role: 'assistant',
2378
- kind: 'assistant_output',
2379
- });
2380
- const settlementId =
2381
- (assistantMetadata[PRIMARY_SUMMARY_SETTLEMENT_METADATA_KEY] as PrimarySummarySettlementMetadata).settlementId;
2382
- const placeholderMetadata = placeholderBlocks.length > 0
2383
- ? this.buildPrimarySummarySettlementMetadata(record, {
2384
- outcome: 'held',
2385
- settlementId,
2386
- stopReason: options.stopReason,
2387
- holdReason,
2388
- providerInputTokens: options.providerInputTokens,
2389
- visibleAssistantOutput: options.visibleAssistantOutput,
2390
- executedToolCalls: options.executedToolCalls,
2391
- entryIndex: 1,
2392
- entryCount,
2393
- role: 'user',
2394
- kind: 'generated_tool_result',
2395
- })
2396
- : undefined;
2397
- const messageIds = await this.persistPrimarySummaryChronicleEntries(agent, [
2398
- { participant: agent.name, content: options.assistantBlocks, metadata: assistantMetadata },
2399
- ...(placeholderBlocks.length > 0 && placeholderMetadata
2400
- ? [{ participant: 'user', content: placeholderBlocks, metadata: placeholderMetadata }]
2401
- : []),
2402
- ]);
2403
- if (options.retryDispatch && options.retryPhase && options.assistantBlocks.length > 0) {
2404
- await this.recordPrimarySummaryRetryOutput(
2405
- options.retryDispatch.originalRequestId,
2406
- options.retryDispatch.intentId,
2407
- {
2408
- phase: options.retryPhase,
2409
- contentHash: this.primarySummaryFallbackOutputPhaseHash(options.assistantBlocks),
2410
- messageId: messageIds[0] ?? '',
2411
- blockTypes: this.primarySummaryFallbackBlockTypes(options.assistantBlocks),
2412
- persistedAt: Date.now(),
2413
- },
2414
- );
2415
- }
2416
- if (options.retryDispatch) {
2417
- await this.holdPrimarySummaryRetryFamily(
2418
- agent,
2419
- requestId,
2420
- options.retryDispatch,
2421
- holdReason,
2422
- options.holdData,
2423
- );
2424
- return;
2425
- }
2426
- await this.holdPrimarySummaryRequest(agent, requestId, holdReason, options.holdData);
2427
- }
2428
-
2429
- private async findPrimarySummaryRetryParent(
2430
- retryRequestId: string,
2431
- ): Promise<{ originalRequestId: string; intentId: string } | null> {
2432
- return this.withPrimarySummaryStateLock(() => {
2433
- const state = this.readPrimarySummaryFallbackState();
2434
- for (const record of state.requests) {
2435
- const intent = record.fallbackIntent;
2436
- if (intent?.retryRequestId === retryRequestId) {
2437
- return { originalRequestId: record.requestId, intentId: intent.intentId };
2438
- }
2439
- }
2440
- return null;
2441
- });
2442
- }
2443
-
2444
- private async finalizePrimarySummarySettlementOnStartup(
2445
- record: PrimarySummaryRequestRecord,
2446
- ): Promise<'finalized' | { invalidReason: string } | false> {
2447
- const agent = this.agents.get(record.agentName);
2448
- if (!agent) return false;
2449
- const settlementEntries = this.primarySummaryLogicalSettlementEntries(agent, record.requestId);
2450
- if ('invalidReason' in settlementEntries) return settlementEntries;
2451
- if (settlementEntries.length === 0) {
2452
- return false;
2453
- }
2454
- const currentBranch = agent.getCurrentBranchGeneration();
2455
- const assistantEntry = settlementEntries[0]!;
2456
- const settlement = assistantEntry.settlement;
2457
- if (
2458
- assistantEntry.participant !== record.agentName ||
2459
- settlement.agentName !== record.agentName ||
2460
- settlement.dispatchKind !== record.dispatchKind ||
2461
- settlement.entryIndex !== 0 ||
2462
- settlement.role !== 'assistant' ||
2463
- settlement.kind !== 'assistant_output'
2464
- ) {
2465
- return { invalidReason: 'primary_summary_settlement_invalid_assistant_marker_on_restart' };
2466
- }
2467
- if (!currentBranch) {
2468
- return { invalidReason: 'primary_summary_settlement_branch_unknown_on_restart' };
2469
- }
2470
- if (
2471
- settlement.branch.id !== record.branch.id ||
2472
- settlement.branch.name !== record.branch.name ||
2473
- settlement.branch.generation !== record.branch.generation ||
2474
- settlement.branch.id !== currentBranch.id ||
2475
- settlement.branch.name !== currentBranch.name ||
2476
- settlement.branch.generation !== currentBranch.generation
2477
- ) {
2478
- return { invalidReason: 'primary_summary_settlement_branch_mismatch_on_restart' };
2479
- }
2480
- if (settlement.entryCount < 1 || settlement.entryCount > 2) {
2481
- return { invalidReason: 'primary_summary_settlement_entry_count_invalid_on_restart' };
2482
- }
2483
- const entriesByIndex = new Map<number, PrimarySummaryLogicalSettlementEntry>();
2484
- for (const entry of settlementEntries) {
2485
- if (!this.primarySummarySettlementMetadataConsistent(settlement, entry.settlement)) {
2486
- return { invalidReason: 'primary_summary_settlement_marker_conflict_on_restart' };
2487
- }
2488
- if (entriesByIndex.has(entry.settlement.entryIndex)) {
2489
- return { invalidReason: 'primary_summary_settlement_duplicate_entry_on_restart' };
2490
- }
2491
- entriesByIndex.set(entry.settlement.entryIndex, entry);
2492
- }
2493
- const placeholderEntry = entriesByIndex.get(1);
2494
- if (
2495
- placeholderEntry
2496
- && (
2497
- placeholderEntry.participant !== 'user'
2498
- || placeholderEntry.settlement.role !== 'user'
2499
- || placeholderEntry.settlement.kind !== 'generated_tool_result'
2500
- )
2501
- ) {
2502
- return { invalidReason: 'primary_summary_settlement_placeholder_marker_invalid_on_restart' };
2503
- }
2504
- if ([...entriesByIndex.keys()].some((index) => index !== 0 && index !== 1)) {
2505
- return { invalidReason: 'primary_summary_settlement_out_of_order_entries_on_restart' };
2506
- }
2507
- const expectedPlaceholder = settlement.outcome === 'held'
2508
- ? this.buildNonExecutedToolResults(assistantEntry.content)
2509
- : [];
2510
- if (settlement.outcome === 'success') {
2511
- if (settlement.entryCount !== 1 || placeholderEntry) {
2512
- return { invalidReason: 'primary_summary_success_settlement_shape_invalid_on_restart' };
2513
- }
2514
- } else {
2515
- const expectedEntryCount = expectedPlaceholder.length > 0 ? 2 : 1;
2516
- if (settlement.entryCount !== expectedEntryCount) {
2517
- return { invalidReason: 'primary_summary_held_settlement_shape_invalid_on_restart' };
2518
- }
2519
- if (placeholderEntry) {
2520
- if (!this.primarySummaryContentMatches(placeholderEntry.content, expectedPlaceholder)) {
2521
- return { invalidReason: 'primary_summary_settlement_placeholder_content_mismatch_on_restart' };
2522
- }
2523
- } else if (expectedPlaceholder.length > 0) {
2524
- const placeholderMetadata = this.buildPrimarySummarySettlementMetadata(record, {
2525
- outcome: 'held',
2526
- settlementId: settlement.settlementId,
2527
- stopReason: settlement.stopReason,
2528
- holdReason: settlement.holdReason,
2529
- providerInputTokens: settlement.providerInputTokens,
2530
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2531
- executedToolCalls: settlement.executedToolCalls,
2532
- entryIndex: 1,
2533
- entryCount: settlement.entryCount,
2534
- role: 'user',
2535
- kind: 'generated_tool_result',
2536
- });
2537
- await this.persistPrimarySummaryChronicleEntries(agent, [
2538
- { participant: 'user', content: expectedPlaceholder, metadata: placeholderMetadata },
2539
- ]);
2540
- }
2541
- }
2542
-
2543
- if (record.dispatchKind === 'primary_summary_fallback_retry') {
2544
- const parent = await this.findPrimarySummaryRetryParent(record.requestId);
2545
- if (!parent) return { invalidReason: 'primary_summary_fallback_parent_missing_on_restart' };
2546
- if (settlement.outcome === 'success') {
2547
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2548
- ...entry,
2549
- stopReason: settlement.stopReason ?? entry.stopReason,
2550
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
2551
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2552
- executedToolCalls: settlement.executedToolCalls,
2553
- finalStatus: 'success',
2554
- fallbackHeldReason: undefined,
2555
- }));
2556
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => {
2557
- const intent = entry.fallbackIntent;
2558
- return {
2559
- ...entry,
2560
- finalStatus: 'success',
2561
- fallbackStatus: 'success',
2562
- fallbackHeldReason: undefined,
2563
- fallbackIntent: intent && intent.intentId === parent.intentId
2564
- ? { ...intent, finalizedAt: intent.finalizedAt ?? Date.now() }
2565
- : intent,
2566
- };
2567
- });
2568
- return 'finalized';
2569
- }
2570
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2571
- ...entry,
2572
- stopReason: settlement.stopReason ?? entry.stopReason,
2573
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
2574
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2575
- executedToolCalls: settlement.executedToolCalls,
2576
- finalStatus: 'held',
2577
- fallbackHeldReason: settlement.holdReason,
2578
- }));
2579
- await this.updatePrimarySummaryRequestRecord(parent.originalRequestId, (entry) => ({
2580
- ...entry,
2581
- finalStatus: 'held',
2582
- fallbackStatus: 'held',
2583
- fallbackHeldReason: settlement.holdReason,
2584
- }));
2585
- return 'finalized';
2586
- }
2587
-
2588
- await this.updatePrimarySummaryRequestRecord(record.requestId, (entry) => ({
2589
- ...entry,
2590
- stopReason: settlement.stopReason ?? entry.stopReason,
2591
- providerInputTokens: settlement.providerInputTokens ?? entry.providerInputTokens,
2592
- visibleAssistantOutput: settlement.visibleAssistantOutput,
2593
- executedToolCalls: settlement.executedToolCalls,
2594
- finalStatus: settlement.outcome === 'success' ? 'success' : 'held',
2595
- fallbackStatus: settlement.outcome === 'success' ? entry.fallbackStatus : 'held',
2596
- fallbackHeldReason: settlement.outcome === 'held' ? settlement.holdReason : undefined,
2597
- }));
2598
- return 'finalized';
2599
- }
2600
-
2601
- private summarizePrimarySummaryRetryRequest(request: NormalizedRequest): Record<string, unknown> {
2602
- const blockCounts = new Map<string, number>();
2603
- for (const message of request.messages) {
2604
- for (const block of message.content) {
2605
- blockCounts.set(block.type, (blockCounts.get(block.type) ?? 0) + 1);
2606
- }
2607
- }
2608
- return {
2609
- kind: 'primary_summary_fallback_retry',
2610
- requestHash: this.hashJson(request),
2611
- systemHash: this.hashJson(request.system ?? ''),
2612
- messageCount: request.messages.length,
2613
- toolCount: request.tools?.length ?? 0,
2614
- participantCount: [...new Set(request.messages.map((message) => message.participant))].length,
2615
- blockCounts: Object.fromEntries([...blockCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
2616
- promptCaching: request.promptCaching === true,
2617
- cacheTtl: request.cacheTtl,
2618
- assistantParticipant: request.assistantParticipant,
2619
- requestCompleteBoundTokens: this.estimateRequestCompleteBoundTokens(request),
2620
- };
2621
- }
2622
-
2623
- private summarizePrimarySummaryRetryResponse(response: NormalizedResponse): Record<string, unknown> {
2624
- const blockCounts = new Map<string, number>();
2625
- for (const block of response.content) {
2626
- blockCounts.set(block.type, (blockCounts.get(block.type) ?? 0) + 1);
2627
- }
2628
- return {
2629
- kind: 'primary_summary_fallback_retry',
2630
- stopReason: response.stopReason,
2631
- rawAssistantTextHash: response.rawAssistantText
2632
- ? this.hashJson(response.rawAssistantText)
2633
- : undefined,
2634
- blockCounts: Object.fromEntries([...blockCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
2635
- toolCallIds: response.toolCalls.map((call: { id: string }) => call.id),
2636
- usage: response.usage
2637
- ? {
2638
- inputTokens: response.usage.inputTokens,
2639
- outputTokens: response.usage.outputTokens,
2640
- }
2641
- : undefined,
2642
- };
2643
- }
2644
-
2645
- private async claimPrimarySummaryRetryIntent(
2646
- originalRequestId: string,
2647
- intentId: string,
2648
- retryRequestId: string,
2649
- ): Promise<'claimed' | 'duplicate' | 'missing'> {
2650
- let outcome: 'claimed' | 'duplicate' | 'missing' = 'missing';
2651
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2652
- const intent = record.fallbackIntent;
2653
- if (!intent || intent.intentId !== intentId || record.fallbackStatus !== 'pending') {
2654
- return record;
2655
- }
2656
- if (intent.retryRequestId && intent.retryRequestId !== retryRequestId) {
2657
- outcome = 'duplicate';
2658
- return record;
2659
- }
2660
- outcome = 'claimed';
2661
- return {
2662
- ...record,
2663
- fallbackIntent: {
2664
- ...intent,
2665
- retryRequestId,
2666
- dispatchedAt: intent.dispatchedAt ?? Date.now(),
2667
- },
2668
- };
2669
- });
2670
- return outcome;
2671
- }
2672
-
2673
- private async validatePrimarySummaryRetryMutation(
2674
- agent: Agent,
2675
- requestId: string,
2676
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2677
- compiledRequest: NormalizedRequest,
2678
- artifacts: ActivationCompileArtifacts,
2679
- phase: string,
2680
- ): Promise<{ ok: true } | { ok: false; reason: string; data: Record<string, unknown> }> {
2681
- const branch = agent.getCurrentBranchGeneration();
2682
- if (!branch || branch.id !== retryDispatch.branch.id || branch.generation !== retryDispatch.branch.generation) {
2683
- return {
2684
- ok: false,
2685
- reason: `primary_summary_fallback_branch_changed_${phase}`,
2686
- data: {
2687
- expectedBranchId: retryDispatch.branch.id,
2688
- expectedGeneration: retryDispatch.branch.generation,
2689
- actualBranchId: branch?.id,
2690
- actualGeneration: branch?.generation,
2691
- },
2692
- };
2693
- }
2694
- const requestHash = this.hashJson(compiledRequest);
2695
- if (requestHash !== retryDispatch.expectedRetryRequestHash) {
2696
- return {
2697
- ok: false,
2698
- reason: `primary_summary_fallback_request_changed_${phase}`,
2699
- data: {
2700
- expectedRetryRequestHash: retryDispatch.expectedRetryRequestHash,
2701
- actualRetryRequestHash: requestHash,
2702
- },
2703
- };
2704
- }
2705
- if (
2706
- this.primarySummaryProjectionKeys(artifacts.compileResult.primarySummaryProjection ?? null).join('\u0000') !==
2707
- retryDispatch.originalProjectionKeys.join('\u0000')
2708
- ) {
2709
- return {
2710
- ok: false,
2711
- reason: `primary_summary_fallback_projection_changed_${phase}`,
2712
- data: {
2713
- expectedProjectionKeys: retryDispatch.originalProjectionKeys,
2714
- actualProjectionKeys: this.primarySummaryProjectionKeys(
2715
- artifacts.compileResult.primarySummaryProjection ?? null,
2716
- ),
2717
- },
2718
- };
2719
- }
2720
- if (!this.primarySummaryContractsMatch(artifacts.contract, retryDispatch.originalArtifacts.contract)) {
2721
- return {
2722
- ok: false,
2723
- reason: `primary_summary_fallback_contract_changed_${phase}`,
2724
- data: {
2725
- expectedContract: retryDispatch.originalArtifacts.contract,
2726
- actualContract: artifacts.contract,
2727
- },
2728
- };
2729
- }
2730
- const original = await this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId);
2731
- if (
2732
- !original?.fallbackIntent ||
2733
- original.fallbackIntent.intentId !== retryDispatch.intentId ||
2734
- original.fallbackIntent.retryRequestId !== requestId ||
2735
- original.fallbackStatus !== 'pending'
2736
- ) {
2737
- return {
2738
- ok: false,
2739
- reason: `primary_summary_fallback_intent_changed_${phase}`,
2740
- data: {
2741
- expectedIntentId: retryDispatch.intentId,
2742
- actualIntentId: original?.fallbackIntent?.intentId,
2743
- retryRequestId: original?.fallbackIntent?.retryRequestId,
2744
- fallbackStatus: original?.fallbackStatus,
2745
- },
2746
- };
2747
- }
2748
- return { ok: true };
2749
- }
2750
-
2751
- private async recordPrimarySummaryRetryQuarantine(
2752
- originalRequestId: string,
2753
- intentId: string,
2754
- ): Promise<void> {
2755
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2756
- const intent = record.fallbackIntent;
2757
- if (!intent || intent.intentId !== intentId || intent.quarantinePersistedAt) return record;
2758
- return {
2759
- ...record,
2760
- fallbackIntent: {
2761
- ...intent,
2762
- quarantinePersistedAt: Date.now(),
2763
- },
2764
- };
2765
- });
2766
- }
2767
-
2768
- private async recordPrimarySummaryRetryOutput(
2769
- originalRequestId: string,
2770
- intentId: string,
2771
- output: PrimarySummaryFallbackOutputRecord,
2772
- ): Promise<void> {
2773
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2774
- const intent = record.fallbackIntent;
2775
- if (!intent || intent.intentId !== intentId) return record;
2776
- const existing = intent.outputs?.find((entry) => entry.phase === output.phase);
2777
- if (
2778
- existing &&
2779
- existing.contentHash === output.contentHash &&
2780
- existing.messageId === output.messageId
2781
- ) {
2782
- return record;
2783
- }
2784
- if (existing) {
2785
- throw new Error(`Primary summary fallback output phase '${output.phase}' changed after persistence`);
2786
- }
2787
- return {
2788
- ...record,
2789
- fallbackIntent: {
2790
- ...intent,
2791
- outputs: [...(intent.outputs ?? []), output],
2792
- },
2793
- };
2794
- });
2795
- }
2796
-
2797
- private async recordPrimarySummaryRetryToolDispatch(
2798
- originalRequestId: string,
2799
- intentId: string,
2800
- call: ToolCall,
2801
- ): Promise<void> {
2802
- await this.updatePrimarySummaryRequestRecord(originalRequestId, (record) => {
2803
- const intent = record.fallbackIntent;
2804
- if (!intent || intent.intentId !== intentId) return record;
2805
- const inputHash = this.hashJson(call.input);
2806
- const existing = intent.toolDispatches?.find((entry) => entry.callId === call.id);
2807
- if (existing?.inputHash === inputHash) return record;
2808
- if (existing) {
2809
- throw new Error(`Primary summary fallback tool dispatch '${call.id}' changed after persistence`);
2810
- }
2811
- return {
2812
- ...record,
2813
- fallbackIntent: {
2814
- ...intent,
2815
- toolDispatches: [
2816
- ...(intent.toolDispatches ?? []),
2817
- {
2818
- callId: call.id,
2819
- inputHash,
2820
- recordedAt: Date.now(),
2821
- },
2822
- ],
2823
- },
2824
- };
2825
- });
2826
- }
2827
-
2828
- private async resolveContextBudgetRestartDispatch(
2829
- requestId: string,
2830
- retryDispatch?: PrimarySummaryFallbackRetryDispatch,
2831
- ): Promise<ContextBudgetRestartDispatch> {
2832
- const retryIdentity = retryDispatch
2833
- ? {
2834
- requestId,
2835
- dispatchKind: 'primary_summary_fallback_retry' as const,
2836
- originalRequestId: retryDispatch.originalRequestId,
2837
- intentId: retryDispatch.intentId,
2838
- }
2839
- : null;
2840
- const record = await this.getPrimarySummaryRequestRecord(requestId);
2841
- if (record?.dispatchKind === 'primary_summary_fallback_retry' && retryIdentity) {
2842
- return retryIdentity;
2843
- }
2844
- if (retryIdentity) {
2845
- const original = await this.getPrimarySummaryRequestRecord(retryIdentity.originalRequestId);
2846
- if (
2847
- original?.fallbackIntent?.intentId === retryIdentity.intentId
2848
- && original.fallbackIntent.retryRequestId === requestId
2849
- ) {
2850
- return retryIdentity;
2851
- }
2852
- }
2853
- return {
2854
- requestId,
2855
- dispatchKind: 'primary',
2856
- };
2857
- }
2858
-
2859
- private contextBudgetRestartMatchesIdentity(
2860
- request: PendingInferenceRequest,
2861
- identity: ContextBudgetRestartDispatch,
2862
- ): boolean {
2863
- if (request.reason !== 'context_budget_restart') return false;
2864
- const restart = request.contextBudgetRestart;
2865
- if (!restart) return false;
2866
- if (restart.dispatchKind !== identity.dispatchKind) return false;
2867
- if (restart.requestId !== identity.requestId) return false;
2868
- if (identity.dispatchKind !== 'primary_summary_fallback_retry') return true;
2869
- return restart.originalRequestId === identity.originalRequestId
2870
- && restart.intentId === identity.intentId;
2871
- }
2872
-
2873
- private clearQueuedContextBudgetRestart(identity: ContextBudgetRestartDispatch): number {
2874
- const before = this.pendingRequests.length;
2875
- this.pendingRequests = this.pendingRequests.filter(
2876
- (request) => !this.contextBudgetRestartMatchesIdentity(request, identity),
2877
- );
2878
- return before - this.pendingRequests.length;
2879
- }
2880
-
2881
- private async markPrimarySummaryRetrySuccess(
2882
- requestId: string,
2883
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2884
- stopReason: string | undefined,
2885
- providerInputTokens: number | undefined,
2886
- visibleAssistantOutput: boolean,
2887
- executedToolCalls: number,
2888
- ): Promise<void> {
2889
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2890
- ...record,
2891
- stopReason,
2892
- providerInputTokens,
2893
- visibleAssistantOutput,
2894
- executedToolCalls,
2895
- finalStatus: 'success',
2896
- fallbackHeldReason: undefined,
2897
- }));
2898
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => {
2899
- const intent = record.fallbackIntent;
2900
- return {
2901
- ...record,
2902
- finalStatus: 'success',
2903
- fallbackStatus: 'success',
2904
- fallbackHeldReason: undefined,
2905
- fallbackIntent: intent && intent.intentId === retryDispatch.intentId
2906
- ? { ...intent, finalizedAt: Date.now() }
2907
- : intent,
2908
- };
2909
- });
2910
- }
2911
-
2912
- private async holdPrimarySummaryRetryFamily(
2913
- agent: Agent,
2914
- requestId: string,
2915
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2916
- reason: string,
2917
- data?: Record<string, unknown>,
2918
- options?: { emitChronicleMarker?: boolean; stopReason?: string },
2919
- ): Promise<void> {
2920
- const clearedQueuedContextBudgetRestarts = this.clearQueuedContextBudgetRestart({
2921
- requestId,
2922
- dispatchKind: 'primary_summary_fallback_retry',
2923
- originalRequestId: retryDispatch.originalRequestId,
2924
- intentId: retryDispatch.intentId,
2925
- });
2926
- const holdData = clearedQueuedContextBudgetRestarts > 0
2927
- ? { ...(data ?? {}), clearedQueuedContextBudgetRestarts }
2928
- : data;
2929
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2930
- ...record,
2931
- ...(options?.stopReason ? { stopReason: options.stopReason } : {}),
2932
- finalStatus: 'held',
2933
- fallbackHeldReason: reason,
2934
- }));
2935
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => ({
2936
- ...record,
2937
- finalStatus: 'held',
2938
- fallbackStatus: 'held',
2939
- fallbackHeldReason: reason,
2940
- }));
2941
- this.opsAlert('refusal-held', agent.name, reason, { data: holdData });
2942
- if (options?.emitChronicleMarker === false) return;
2943
- try {
2944
- agent.getContextManager().addMessage(
2945
- 'user',
2946
- [{
2947
- type: 'text',
2948
- text:
2949
- `[refusal-held] Your previous turn was refused by the provider and ` +
2950
- `is being held for operator review rather than silently retried. ` +
2951
- `Automatic raw-expansion recovery was either unsafe or unsuccessful.`,
2952
- }],
2953
- { system: true, kind: 'refusal-held', reason },
2954
- );
2955
- } catch (error) {
2956
- console.error('[refusal-held] could not record chronicle marker:', error);
2957
- }
2958
- }
2959
-
2960
- private async primarySummaryRetryAlreadySucceeded(
2961
- requestId: string,
2962
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2963
- ): Promise<boolean> {
2964
- const [retryRecord, originalRecord] = await Promise.all([
2965
- this.getPrimarySummaryRequestRecord(requestId),
2966
- this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId),
2967
- ]);
2968
- return retryRecord?.finalStatus === 'success'
2969
- || originalRecord?.fallbackStatus === 'success'
2970
- || originalRecord?.finalStatus === 'success';
2971
- }
2972
-
2973
- private async primarySummaryRetryFamilyAlreadySettled(
2974
- requestId: string,
2975
- retryDispatch: PrimarySummaryFallbackRetryDispatch,
2976
- ): Promise<boolean> {
2977
- const [retryRecord, originalRecord] = await Promise.all([
2978
- this.getPrimarySummaryRequestRecord(requestId),
2979
- this.getPrimarySummaryRequestRecord(retryDispatch.originalRequestId),
2980
- ]);
2981
- return retryRecord?.finalStatus === 'held'
2982
- || retryRecord?.finalStatus === 'success'
2983
- || originalRecord?.fallbackStatus === 'held'
2984
- || originalRecord?.fallbackStatus === 'success'
2985
- || originalRecord?.finalStatus === 'held'
2986
- || originalRecord?.finalStatus === 'success';
2987
- }
2988
-
2989
- private async holdPrimarySummaryRequest(
2990
- agent: Agent,
2991
- requestId: string,
2992
- reason: string,
2993
- data?: Record<string, unknown>,
2994
- ): Promise<void> {
2995
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
2996
- ...record,
2997
- finalStatus: 'held',
2998
- fallbackStatus: 'held',
2999
- fallbackHeldReason: reason,
3000
- }));
3001
- this.opsAlert('refusal-held', agent.name, reason, { data });
3002
- try {
3003
- agent.getContextManager().addMessage(
3004
- 'user',
3005
- [{
3006
- type: 'text',
3007
- text:
3008
- `[refusal-held] Your previous turn was refused by the provider and ` +
3009
- `is being held for operator review rather than silently retried. ` +
3010
- `Automatic raw-expansion recovery was either unsafe or unsuccessful.`,
3011
- }],
3012
- { system: true, kind: 'refusal-held', reason },
3013
- );
3014
- } catch (error) {
3015
- console.error('[refusal-held] could not record chronicle marker:', error);
3016
- }
3017
- }
1576
+ const cleanup = () => {
1577
+ // Don't close the store — it's shared. Just release the CM.
1578
+ // Data persists in the store under the namespace for investigation.
1579
+ };
3018
1580
 
3019
- private hasVisibleAssistantOutput(blocks: ReadonlyArray<ContentBlock>): boolean {
3020
- return blocks.some((block) => block.type !== 'thinking' && block.type !== 'redacted_thinking');
1581
+ return { agent, contextManager, cleanup };
3021
1582
  }
3022
1583
 
3023
- private buildPrimarySummaryRequestRecord(
3024
- agent: Agent,
3025
- requestId: string,
3026
- dispatchKind: PrimarySummaryRequestRecord['dispatchKind'],
3027
- request: NormalizedRequest,
3028
- artifacts: ActivationCompileArtifacts | undefined,
3029
- branch: BranchGenerationInfo | null,
3030
- stopReason: string | undefined,
3031
- visibleAssistantOutput: boolean,
3032
- executedToolCalls: number,
3033
- providerInputTokens: number | undefined,
3034
- finalStatus: PrimarySummaryFallbackStatus,
3035
- ): PrimarySummaryRequestRecord {
3036
- const projection = artifacts?.compileResult.primarySummaryProjection ?? null;
3037
- return {
3038
- requestId,
3039
- agentName: agent.name,
3040
- namespace: projection?.namespace ?? `agents/${agent.name}`,
3041
- timestamp: Date.now(),
3042
- dispatchKind,
3043
- branch: branch
3044
- ? { id: branch.id, name: branch.name, generation: branch.generation }
3045
- : { id: 'unknown', name: 'unknown', generation: -1 },
3046
- projection,
3047
- requestInputBoundTokens: this.estimateRequestInputBoundTokens(request),
3048
- requestCompleteBoundTokens: this.estimateRequestCompleteBoundTokens(request),
3049
- providerInputTokens,
3050
- systemHash: artifacts?.contract.systemHash ?? this.hashJson(request.system ?? ''),
3051
- modelConfigHash: artifacts?.contract.modelConfigHash ?? this.hashJson({ request: request.config }),
3052
- toolContractHash: artifacts?.contract.toolContractHash ?? this.hashJson(request.tools ?? []),
3053
- stopReason,
3054
- visibleAssistantOutput,
3055
- executedToolCalls,
3056
- finalStatus,
3057
- };
1584
+ private createDeferred<T>(): Deferred<T> {
1585
+ let resolve!: (value: T) => void;
1586
+ let reject!: (reason?: unknown) => void;
1587
+ const promise = new Promise<T>((res, rej) => {
1588
+ resolve = res;
1589
+ reject = rej;
1590
+ });
1591
+ return { promise, resolve, reject };
3058
1592
  }
3059
1593
 
3060
1594
  /** Liveness ping for an active ephemeral run (no-op otherwise). */
@@ -4603,7 +3137,6 @@ export class AgentFramework {
4603
3137
  // Cast to AgentState to bypass TypeScript's control flow narrowing
4604
3138
  const currentState = agent.state as AgentState;
4605
3139
  if (currentState.status === 'ready') {
4606
- const activeStreamContext = this.activeStreamContexts.get(agent.name);
4607
3140
  // Flush pending assistant blocks (tool_use + preamble text) to context
4608
3141
  const pendingBlocks = this.pendingAssistantBlocks.get(agent.name);
4609
3142
  if (pendingBlocks) {
@@ -4725,59 +3258,6 @@ export class AgentFramework {
4725
3258
  // a terminal failure — rejecting an ephemeral's promise mid-run
4726
3259
  // and emitting a spurious inference:exhausted (same race shape as
4727
3260
  // endTurn above).
4728
- const requestId = activeStreamContext?.requestId;
4729
- const primarySummaryRetry = activeStreamContext?.trigger?.primarySummaryFallbackRetry;
4730
- const compiledRequest = activeStreamContext?.compiledRequest;
4731
- const artifacts = activeStreamContext?.artifacts;
4732
- const restartDispatch = await this.resolveContextBudgetRestartDispatch(
4733
- requestId ?? `${agent.name}:context_budget_restart`,
4734
- primarySummaryRetry,
4735
- );
4736
- if (
4737
- restartDispatch.dispatchKind === 'primary_summary_fallback_retry'
4738
- && requestId
4739
- && primarySummaryRetry
4740
- && compiledRequest
4741
- && artifacts
4742
- ) {
4743
- const validation = await this.validatePrimarySummaryRetryMutation(
4744
- agent,
4745
- requestId,
4746
- primarySummaryRetry,
4747
- compiledRequest,
4748
- artifacts,
4749
- 'before_context_budget_restart_hold',
4750
- );
4751
- if (!validation.ok) {
4752
- await this.holdPrimarySummaryRetryFamily(
4753
- agent,
4754
- requestId,
4755
- primarySummaryRetry,
4756
- validation.reason,
4757
- validation.data,
4758
- { emitChronicleMarker: false, stopReason: 'abort' },
4759
- );
4760
- } else {
4761
- await this.holdPrimarySummaryRetryFamily(
4762
- agent,
4763
- requestId,
4764
- primarySummaryRetry,
4765
- 'primary_summary_fallback_context_budget_restart_required',
4766
- {
4767
- inputTokens: agent.lastStreamInputTokens,
4768
- budget: agent.maxStreamTokens,
4769
- originalRequestId: primarySummaryRetry.originalRequestId,
4770
- intentId: primarySummaryRetry.intentId,
4771
- },
4772
- { emitChronicleMarker: false, stopReason: 'abort' },
4773
- );
4774
- }
4775
- this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
4776
- currentState.stream?.cancel();
4777
- agent.reset();
4778
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
4779
- return;
4780
- }
4781
3261
  if (currentState.stream) {
4782
3262
  this.frameworkCancelledStreams.set(`${agent.name}:${agent.streamId}`, 'budget_restart');
4783
3263
  }
@@ -4794,8 +3274,6 @@ export class AgentFramework {
4794
3274
  reason: 'context_budget_restart',
4795
3275
  source: 'framework',
4796
3276
  timestamp: Date.now(),
4797
- channelId: activeStreamContext?.trigger?.channelId,
4798
- contextBudgetRestart: restartDispatch,
4799
3277
  });
4800
3278
  } else if (currentState.stream) {
4801
3279
  // Streaming path: convert results and resume the stream.
@@ -5382,7 +3860,7 @@ export class AgentFramework {
5382
3860
  const state = this.createFrameworkState();
5383
3861
 
5384
3862
  // Group requests by agent
5385
- const requestsByAgent = new Map<string, PendingInferenceRequest[]>();
3863
+ const requestsByAgent = new Map<string, InferenceRequest[]>();
5386
3864
  for (const req of this.pendingRequests) {
5387
3865
  const existing = requestsByAgent.get(req.agentName) ?? [];
5388
3866
  existing.push(req);
@@ -5393,8 +3871,7 @@ export class AgentFramework {
5393
3871
  this.pendingRequests = [];
5394
3872
 
5395
3873
  // Check each agent
5396
- for (const [agentName, queuedRequests] of requestsByAgent) {
5397
- let requests = queuedRequests;
3874
+ for (const [agentName, requests] of requestsByAgent) {
5398
3875
  const agent = this.agents.get(agentName);
5399
3876
  if (!agent) {
5400
3877
  // Agent not found — request is orphaned. Emit warning and drop.
@@ -5439,19 +3916,6 @@ export class AgentFramework {
5439
3916
  continue;
5440
3917
  }
5441
3918
 
5442
- if (this.discordAwarenessBarrier) {
5443
- this.pendingRequests.push(...requests);
5444
- continue;
5445
- }
5446
-
5447
- requests = requests.filter((request) =>
5448
- request.reason !== 'context_budget_restart'
5449
- || request.contextBudgetRestart?.dispatchKind !== 'primary_summary_fallback_retry'
5450
- );
5451
- if (requests.length === 0) {
5452
- continue;
5453
- }
5454
-
5455
3919
  // Check policy
5456
3920
  if (!this.inferencePolicy.shouldInfer(agentName, requests, state)) {
5457
3921
  // Loud drop: a queued request that dies here is otherwise invisible —
@@ -5465,12 +3929,7 @@ export class AgentFramework {
5465
3929
  }
5466
3930
 
5467
3931
  // Start streaming inference (non-blocking — driveStream runs in background)
5468
- const trigger = requests.find((request) => request.primarySummaryFallbackRetry) ?? requests[0]!;
5469
- if (trigger.primarySummaryFallbackRetry) {
5470
- for (const request of requests) {
5471
- if (request !== trigger) this.pendingRequests.push(request);
5472
- }
5473
- }
3932
+ const trigger = requests[0];
5474
3933
  // Route this turn's auto-published speech to the channel that triggered
5475
3934
  // it (item-3 redux). A batched wake may carry several triggering channels
5476
3935
  // (messages arrived in >1 channel while the agent was busy/idle) — reply
@@ -5487,9 +3946,9 @@ export class AgentFramework {
5487
3946
  }
5488
3947
  }
5489
3948
 
5490
- private async startAgentStream(agent: Agent, trigger?: PendingInferenceRequest, attempt = 0): Promise<void> {
3949
+ private async startAgentStream(agent: Agent, trigger?: InferenceRequest, attempt = 0): Promise<void> {
5491
3950
  // Record turn checkpoint before inference (only on first attempt, not retries)
5492
- if (attempt === 0 && trigger?.reason !== 'primary-summary-fallback-retry') {
3951
+ if (attempt === 0) {
5493
3952
  this.recordTurnCheckpoint(agent.name);
5494
3953
  this.redoStacks.delete(agent.name); // new work invalidates redo
5495
3954
  }
@@ -5510,276 +3969,62 @@ export class AgentFramework {
5510
3969
  this.emitTrace({ type: 'inference:started', agentName: agent.name });
5511
3970
  this.eventGate?.onInferenceStarted(agent.name);
5512
3971
  this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), startedAt: Date.now() });
5513
- const requestId = `${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5514
3972
 
5515
3973
  try {
5516
3974
  const requestSnapshot = this.captureInferenceToolSnapshot(agent);
5517
3975
  const allTools = this.getToolsForAgent(agent.name, requestSnapshot);
5518
3976
  const tools = allTools.filter((t) => agent.canUseTool(t.name));
5519
- const fallbackConfig = this.primarySummaryFallbackConfig(agent);
5520
- const retryDispatch = trigger?.primarySummaryFallbackRetry;
5521
- let compiledRequest: NormalizedRequest;
5522
- let artifacts: ActivationCompileArtifacts;
5523
- let branch = agent.getCurrentBranchGeneration();
5524
-
5525
- if (retryDispatch) {
5526
- if (!fallbackConfig.enabled) {
5527
- await this.holdPrimarySummaryRequest(
5528
- agent,
5529
- retryDispatch.originalRequestId,
5530
- 'primary_summary_fallback_disabled_before_retry',
5531
- );
5532
- agent.reset();
5533
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5534
- this.eventGate?.onInferenceEnded(agent.name);
5535
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5536
- return;
5537
- }
5538
-
5539
- const claim = await this.claimPrimarySummaryRetryIntent(
5540
- retryDispatch.originalRequestId,
5541
- retryDispatch.intentId,
5542
- requestId,
5543
- );
5544
- if (claim !== 'claimed') {
5545
- await this.holdPrimarySummaryRequest(
5546
- agent,
5547
- retryDispatch.originalRequestId,
5548
- claim === 'duplicate'
5549
- ? 'primary_summary_fallback_duplicate_retry_blocked'
5550
- : 'primary_summary_fallback_retry_intent_missing',
5551
- { intentId: retryDispatch.intentId, retryRequestId: requestId },
5552
- );
5553
- agent.reset();
5554
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5555
- this.eventGate?.onInferenceEnded(agent.name);
5556
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5557
- return;
5558
- }
5559
3977
 
5560
- if (
5561
- !branch ||
5562
- branch.id !== retryDispatch.branch.id ||
5563
- branch.generation !== retryDispatch.branch.generation
5564
- ) {
5565
- await this.holdPrimarySummaryRequest(
5566
- agent,
5567
- retryDispatch.originalRequestId,
5568
- 'primary_summary_fallback_branch_changed_before_retry',
5569
- {
5570
- expectedBranchId: retryDispatch.branch.id,
5571
- expectedGeneration: retryDispatch.branch.generation,
5572
- actualBranchId: branch?.id,
5573
- actualGeneration: branch?.generation,
5574
- },
5575
- );
5576
- agent.reset();
5577
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5578
- this.eventGate?.onInferenceEnded(agent.name);
5579
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5580
- return;
5581
- }
3978
+ // Gather context from modules (pull-based) and MCPL hooks (push-based)
3979
+ // Both produce ContextInjection[] that get merged before inference.
3980
+ let injections: ContextInjection[] | undefined;
5582
3981
 
5583
- const rebuilt = agent.buildPrimarySummaryRawExpansionRequest(
5584
- retryDispatch.originalArtifacts,
5585
- retryDispatch.candidateSummaries,
5586
- tools,
3982
+ // Module gatherContext (fail-open, per-module timeout — the module's
3983
+ // contextTimeoutMs, else the registry default). Injections are
3984
+ // channel-scoped via scopeInjectionsForAgent, matching the MCPL hook
3985
+ // injections below, so conversation forks don't see cross-channel
3986
+ // content.
3987
+ try {
3988
+ const moduleInjections = this.scopeInjectionsForAgent(
3989
+ agent.name,
3990
+ await this.moduleRegistry.gatherContext(agent.name),
5587
3991
  );
5588
- compiledRequest = rebuilt.request;
5589
- artifacts = rebuilt.artifacts;
5590
-
5591
- const rebuiltHash = this.hashJson(compiledRequest);
5592
- if (
5593
- rebuiltHash !== retryDispatch.expectedRetryRequestHash ||
5594
- this.primarySummaryProjectionKeys(artifacts.compileResult.primarySummaryProjection ?? null).join('\u0000') !==
5595
- retryDispatch.originalProjectionKeys.join('\u0000')
5596
- ) {
5597
- await this.holdPrimarySummaryRequest(
5598
- agent,
5599
- retryDispatch.originalRequestId,
5600
- 'primary_summary_fallback_revalidation_failed_before_retry',
5601
- {
5602
- expectedRetryRequestHash: retryDispatch.expectedRetryRequestHash,
5603
- actualRetryRequestHash: rebuiltHash,
5604
- },
5605
- );
5606
- agent.reset();
5607
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5608
- this.eventGate?.onInferenceEnded(agent.name);
5609
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5610
- return;
3992
+ if (moduleInjections.length > 0) {
3993
+ injections = moduleInjections;
5611
3994
  }
3995
+ } catch (error) {
3996
+ console.error('Module gatherContext error:', error);
3997
+ }
5612
3998
 
5613
- const retryRecord = this.buildPrimarySummaryRequestRecord(
5614
- agent,
5615
- requestId,
5616
- 'primary_summary_fallback_retry',
5617
- compiledRequest,
5618
- artifacts,
5619
- branch,
5620
- undefined,
5621
- false,
5622
- 0,
5623
- undefined,
5624
- 'pending',
5625
- );
5626
- await this.persistPrimarySummaryRequestRecord(retryRecord);
5627
-
5628
- const admittedTokens = this.primarySummaryAdmissionBoundTokens(
5629
- retryDispatch.originalRequestInputBoundTokens,
5630
- compiledRequest,
5631
- retryDispatch.originalProviderInputTokens,
5632
- );
5633
- if (admittedTokens > retryDispatch.requestBudgetTokens) {
5634
- await this.holdPrimarySummaryRequest(
5635
- agent,
5636
- requestId,
5637
- 'primary_summary_fallback_retry_over_budget',
5638
- {
5639
- admittedTokens,
5640
- requestBudgetTokens: retryDispatch.requestBudgetTokens,
5641
- retryRequestHash: rebuiltHash,
5642
- },
5643
- );
5644
- await this.updatePrimarySummaryRequestRecord(retryDispatch.originalRequestId, (record) => ({
5645
- ...record,
5646
- fallbackStatus: 'held',
5647
- fallbackHeldReason: 'primary_summary_fallback_retry_over_budget',
5648
- }));
5649
- agent.reset();
5650
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5651
- this.eventGate?.onInferenceEnded(agent.name);
5652
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5653
- return;
5654
- }
5655
- } else {
5656
- // Gather context from modules (pull-based) and MCPL hooks (push-based)
5657
- // Both produce ContextInjection[] that get merged before inference.
5658
- let injections: ContextInjection[] | undefined;
5659
-
5660
- // Module gatherContext (fail-open, per-module timeout — the module's
5661
- // contextTimeoutMs, else the registry default). Injections are
5662
- // channel-scoped via scopeInjectionsForAgent, matching the MCPL hook
5663
- // injections below, so conversation forks don't see cross-channel
5664
- // content.
3999
+ // MCPL beforeInference hooks (fail-open)
4000
+ if (this.hookOrchestrator) {
5665
4001
  try {
5666
- const moduleInjections = this.scopeInjectionsForAgent(
4002
+ const hookParams = this.buildBeforeInferenceParams(agent, trigger);
4003
+ const hookInjections = this.scopeInjectionsForAgent(
5667
4004
  agent.name,
5668
- await this.moduleRegistry.gatherContext(agent.name),
4005
+ await this.hookOrchestrator.beforeInference(hookParams),
5669
4006
  );
5670
- if (moduleInjections.length > 0) {
5671
- injections = moduleInjections;
4007
+ if (hookInjections.length > 0) {
4008
+ injections = injections ? [...injections, ...hookInjections] : hookInjections;
5672
4009
  }
5673
4010
  } catch (error) {
5674
- console.error('Module gatherContext error:', error);
5675
- }
5676
-
5677
- // MCPL beforeInference hooks (fail-open)
5678
- if (this.hookOrchestrator) {
5679
- try {
5680
- const hookParams = this.buildBeforeInferenceParams(agent, trigger);
5681
- const hookInjections = this.scopeInjectionsForAgent(
5682
- agent.name,
5683
- await this.hookOrchestrator.beforeInference(hookParams),
5684
- );
5685
- if (hookInjections.length > 0) {
5686
- injections = injections ? [...injections, ...hookInjections] : hookInjections;
5687
- }
5688
- } catch (error) {
5689
- console.error('beforeInference hook error:', error);
5690
- }
5691
- }
5692
-
5693
- const prepared = await agent.prepareActivationRequest(tools, injections);
5694
- compiledRequest = prepared.request;
5695
- artifacts = prepared.artifacts;
5696
- branch = agent.getCurrentBranchGeneration();
5697
-
5698
- if (fallbackConfig.enabled) {
5699
- const record = this.buildPrimarySummaryRequestRecord(
5700
- agent,
5701
- requestId,
5702
- 'primary',
5703
- compiledRequest,
5704
- artifacts,
5705
- branch,
5706
- undefined,
5707
- false,
5708
- 0,
5709
- undefined,
5710
- 'pending',
5711
- );
5712
- await this.persistPrimarySummaryRequestRecord(record);
5713
- const expandedByQuarantine =
5714
- artifacts.compileResult.primarySummaryProjection?.selectedSummaries.some(
5715
- (selection) => selection.renderedAs === 'raw_expansion',
5716
- ) ?? false;
5717
- if (expandedByQuarantine) {
5718
- const admittedTokens = this.estimateRequestCompleteBoundTokens(compiledRequest);
5719
- if (admittedTokens > fallbackConfig.requestBudgetTokens) {
5720
- await this.holdPrimarySummaryRequest(
5721
- agent,
5722
- requestId,
5723
- 'primary_summary_quarantine_over_budget',
5724
- {
5725
- admittedTokens,
5726
- requestBudgetTokens: fallbackConfig.requestBudgetTokens,
5727
- },
5728
- );
5729
- agent.reset();
5730
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5731
- this.eventGate?.onInferenceEnded(agent.name);
5732
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5733
- return;
5734
- }
5735
- }
4011
+ console.error('beforeInference hook error:', error);
5736
4012
  }
5737
4013
  }
5738
4014
 
5739
- const { stream } = agent.startStreamFromRequest(compiledRequest, artifacts);
5740
- this.activeStreamContexts.set(agent.name, {
5741
- requestId,
5742
- trigger,
5743
- compiledRequest,
5744
- artifacts,
5745
- });
4015
+ const { stream, request: compiledRequest } = await agent.startStreamWithInjections(tools, injections);
4016
+
5746
4017
  const handle = this.driveStream(
5747
4018
  agent,
5748
4019
  stream,
5749
4020
  requestSnapshot,
5750
- requestId,
5751
4021
  trigger,
5752
4022
  attempt,
5753
4023
  compiledRequest,
5754
- artifacts,
5755
4024
  );
5756
4025
  this.activeStreams.set(agent.name, handle);
5757
4026
  } catch (error) {
5758
4027
  const err = error instanceof Error ? error : new Error(String(error));
5759
- if (trigger?.primarySummaryFallbackRetry) {
5760
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
5761
- ...record,
5762
- finalStatus: 'held',
5763
- fallbackHeldReason: err.message,
5764
- }));
5765
- await this.updatePrimarySummaryRequestRecord(trigger.primarySummaryFallbackRetry.originalRequestId, (record) => ({
5766
- ...record,
5767
- finalStatus: 'held',
5768
- fallbackStatus: 'held',
5769
- fallbackHeldReason: err.message,
5770
- }));
5771
- this.opsAlert('refusal-held', agent.name, err.message, {
5772
- data: { requestId, originalRequestId: trigger.primarySummaryFallbackRetry.originalRequestId },
5773
- });
5774
- agent.reset();
5775
- this.settleAgent(agent.name, {
5776
- stopReason: 'completed',
5777
- speech: '',
5778
- });
5779
- this.eventGate?.onInferenceEnded(agent.name);
5780
- this.lastInferenceAt.set(agent.name, { ...this.lastInferenceAt.get(agent.name), endedAt: Date.now() });
5781
- return;
5782
- }
5783
4028
  this.emitTrace({
5784
4029
  type: 'inference:failed',
5785
4030
  agentName: agent.name,
@@ -5823,23 +4068,14 @@ export class AgentFramework {
5823
4068
  agent: Agent,
5824
4069
  stream: YieldingStream,
5825
4070
  requestSnapshot: InferenceToolSnapshot,
5826
- requestId: string,
5827
- trigger?: PendingInferenceRequest,
4071
+ trigger?: InferenceRequest,
5828
4072
  attempt = 0,
5829
- compiledRequest?: NormalizedRequest,
5830
- artifacts?: ActivationCompileArtifacts,
4073
+ compiledRequest?: NormalizedRequest
5831
4074
  ): Promise<void> {
5832
4075
  const startTime = Date.now();
4076
+ const requestId = `${agent.name}-${startTime}-${Math.random().toString(36).slice(2, 8)}`;
5833
4077
  const myStreamId = agent.streamId;
5834
4078
  let hadToolCalls = false;
5835
- let executedToolCalls = 0;
5836
- const primarySummaryConfig = this.primarySummaryFallbackConfig(agent);
5837
- const primarySummaryRetry = trigger?.primarySummaryFallbackRetry;
5838
- let primarySummaryQuarantinePersisted = false;
5839
- const fallbackRetryRequestForLog =
5840
- primarySummaryRetry && compiledRequest
5841
- ? this.summarizePrimarySummaryRetryRequest(compiledRequest)
5842
- : compiledRequest ?? { note: 'streaming request' };
5843
4079
 
5844
4080
  // ---- Present-while-acting turn state ---------------------------------
5845
4081
  // Output locus for the current CONVERSATIONAL ROUND: resolved lazily and
@@ -5848,7 +4084,7 @@ export class AgentFramework {
5848
4084
  // single provider inference can now contain several conversations. The
5849
4085
  // pin lives in `turnLocusPins` so it survives a context-budget restart. A
5850
4086
  // null resolution is not pinned.
5851
- if (trigger?.reason !== 'context_budget_restart' && trigger?.reason !== 'primary-summary-fallback-retry') {
4087
+ if (trigger?.reason !== 'context_budget_restart') {
5852
4088
  this.turnLocusPins.delete(agent.name);
5853
4089
  this.midTurnRoutingResets.delete(agent.name);
5854
4090
  }
@@ -5920,104 +4156,6 @@ export class AgentFramework {
5920
4156
  }
5921
4157
  };
5922
4158
 
5923
- const persistPrimarySummaryRetryQuarantine = async (): Promise<boolean> => {
5924
- if (!primarySummaryRetry || primarySummaryQuarantinePersisted || !artifacts) return true;
5925
- const validation = await this.validatePrimarySummaryRetryMutation(
5926
- agent,
5927
- requestId,
5928
- primarySummaryRetry,
5929
- compiledRequest!,
5930
- artifacts,
5931
- 'before_quarantine',
5932
- );
5933
- if (!validation.ok) {
5934
- await this.holdPrimarySummaryRetryFamily(
5935
- agent,
5936
- requestId,
5937
- primarySummaryRetry,
5938
- validation.reason,
5939
- validation.data,
5940
- { emitChronicleMarker: false },
5941
- );
5942
- stream.cancel();
5943
- agent.reset();
5944
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5945
- return false;
5946
- }
5947
- try {
5948
- const quarantinePrimarySummaryForPrimaryLane =
5949
- agent.getContextManager().quarantinePrimarySummaryForPrimaryLane;
5950
- if (typeof quarantinePrimarySummaryForPrimaryLane !== 'function') {
5951
- throw new Error('Primary summary quarantine is unavailable in the active context strategy');
5952
- }
5953
- await quarantinePrimarySummaryForPrimaryLane.call(
5954
- agent.getContextManager(),
5955
- artifacts.contract,
5956
- primarySummaryRetry.candidateSummaries,
5957
- );
5958
- primarySummaryQuarantinePersisted = true;
5959
- await this.recordPrimarySummaryRetryQuarantine(
5960
- primarySummaryRetry.originalRequestId,
5961
- primarySummaryRetry.intentId,
5962
- );
5963
- return true;
5964
- } catch (error) {
5965
- const reason = 'primary_summary_fallback_quarantine_failed';
5966
- await this.holdPrimarySummaryRetryFamily(agent, requestId, primarySummaryRetry, reason, {
5967
- error: error instanceof Error ? error.message : String(error),
5968
- });
5969
- stream.cancel();
5970
- agent.reset();
5971
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
5972
- return false;
5973
- }
5974
- };
5975
-
5976
- const persistPrimarySummaryRetryOutput = async (
5977
- phase: string,
5978
- blocks: ContentBlock[],
5979
- participant = agent.name,
5980
- metadata?: MessageMetadata,
5981
- ): Promise<boolean> => {
5982
- if (!primarySummaryRetry || !artifacts) return true;
5983
- if (blocks.length === 0) return true;
5984
- const validation = await this.validatePrimarySummaryRetryMutation(
5985
- agent,
5986
- requestId,
5987
- primarySummaryRetry,
5988
- compiledRequest!,
5989
- artifacts,
5990
- `before_${phase}`,
5991
- );
5992
- if (!validation.ok) {
5993
- await this.holdPrimarySummaryRetryFamily(
5994
- agent,
5995
- requestId,
5996
- primarySummaryRetry,
5997
- validation.reason,
5998
- validation.data,
5999
- { emitChronicleMarker: false },
6000
- );
6001
- stream.cancel();
6002
- agent.reset();
6003
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
6004
- return false;
6005
- }
6006
- const messageId = agent.getContextManager().addMessage(participant, blocks, metadata);
6007
- await this.recordPrimarySummaryRetryOutput(
6008
- primarySummaryRetry.originalRequestId,
6009
- primarySummaryRetry.intentId,
6010
- {
6011
- phase,
6012
- contentHash: this.primarySummaryFallbackOutputPhaseHash(blocks),
6013
- messageId,
6014
- blockTypes: this.primarySummaryFallbackBlockTypes(blocks),
6015
- persistedAt: Date.now(),
6016
- },
6017
- );
6018
- return true;
6019
- };
6020
-
6021
4159
  try {
6022
4160
  for await (const event of stream) {
6023
4161
  this.touchEphemeralRun(agent.name, true);
@@ -6047,6 +4185,7 @@ export class AgentFramework {
6047
4185
  case 'tool-calls': {
6048
4186
  adoptInjectedRound();
6049
4187
  hadToolCalls = true;
4188
+ this.recordEphemeralToolCalls(agent.name, event.calls.length);
6050
4189
  this.emitTrace({
6051
4190
  type: 'inference:tool_calls_yielded',
6052
4191
  agentName: agent.name,
@@ -6085,51 +4224,7 @@ export class AgentFramework {
6085
4224
 
6086
4225
  agent.enterWaitingForTools(event.calls, stream);
6087
4226
 
6088
- if (!(await persistPrimarySummaryRetryQuarantine())) {
6089
- return;
6090
- }
6091
-
6092
- if (primarySummaryRetry) {
6093
- const phase = `tool_round_${event.context.depth ?? 0}`;
6094
- if (!(await persistPrimarySummaryRetryOutput(phase, assistantBlocks))) {
6095
- return;
6096
- }
6097
- this.pendingAssistantBlocks.delete(agent.name);
6098
- }
6099
-
6100
- executedToolCalls += event.calls.length;
6101
- this.recordEphemeralToolCalls(agent.name, event.calls.length);
6102
-
6103
4227
  for (const call of event.calls) {
6104
- if (primarySummaryRetry) {
6105
- const validation = await this.validatePrimarySummaryRetryMutation(
6106
- agent,
6107
- requestId,
6108
- primarySummaryRetry,
6109
- compiledRequest!,
6110
- artifacts!,
6111
- `before_tool_dispatch_${call.id}`,
6112
- );
6113
- if (!validation.ok) {
6114
- await this.holdPrimarySummaryRetryFamily(
6115
- agent,
6116
- requestId,
6117
- primarySummaryRetry,
6118
- validation.reason,
6119
- validation.data,
6120
- { emitChronicleMarker: false },
6121
- );
6122
- stream.cancel();
6123
- agent.reset();
6124
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
6125
- return;
6126
- }
6127
- await this.recordPrimarySummaryRetryToolDispatch(
6128
- primarySummaryRetry.originalRequestId,
6129
- primarySummaryRetry.intentId,
6130
- call,
6131
- );
6132
- }
6133
4228
  this.dispatchToolCall(agent.name, call);
6134
4229
  }
6135
4230
 
@@ -6222,213 +4317,6 @@ export class AgentFramework {
6222
4317
  }
6223
4318
  }
6224
4319
 
6225
- const lastToolIdx = response.content.reduce(
6226
- (last: number, b: ContentBlock, i: number) =>
6227
- b.type === 'tool_use' || b.type === 'tool_result' ? i : last,
6228
- -1
6229
- );
6230
- const terminalContent = lastToolIdx >= 0
6231
- ? response.content.slice(lastToolIdx + 1)
6232
- : response.content;
6233
- const heldAssistantContent = response.content;
6234
- const visibleAssistantOutput = this.hasVisibleAssistantOutput(response.content);
6235
- const providerInputTokens =
6236
- response.usage?.inputTokens ??
6237
- response.details?.usage?.inputTokens;
6238
-
6239
- if (primarySummaryConfig.enabled && compiledRequest && artifacts && response.stopReason === 'refusal') {
6240
- const stopDetails = (response.raw?.response as {
6241
- stop_details?: { category?: string; explanation?: string };
6242
- } | undefined)?.stop_details;
6243
- const category = stopDetails?.category ?? 'unknown';
6244
- console.error(
6245
- `[inference-refusal] agent=${agent.name} category=${category}` +
6246
- (stopDetails?.explanation ? ` explanation=${stopDetails.explanation}` : ''),
6247
- );
6248
- this.noteRefusal(agent.name, category, response.details?.usage
6249
- ? {
6250
- input: response.details.usage.inputTokens,
6251
- output: response.details.usage.outputTokens,
6252
- }
6253
- : undefined);
6254
-
6255
- const currentRecord = await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6256
- ...record,
6257
- stopReason: response.stopReason,
6258
- providerInputTokens,
6259
- visibleAssistantOutput,
6260
- executedToolCalls,
6261
- finalStatus: 'refusal',
6262
- }));
6263
-
6264
- const holdReasonData = {
6265
- category,
6266
- visibleAssistantOutput,
6267
- executedToolCalls,
6268
- candidateLimit: primarySummaryConfig.maxNewSummaries,
6269
- };
6270
-
6271
- if (primarySummaryRetry) {
6272
- const reason = visibleAssistantOutput
6273
- ? 'primary_summary_fallback_retry_partial_output'
6274
- : executedToolCalls > 0
6275
- ? 'primary_summary_fallback_retry_tool_already_executed'
6276
- : 'primary_summary_fallback_retry_refusal';
6277
- if (visibleAssistantOutput || executedToolCalls > 0) {
6278
- await this.persistPrimarySummaryHeldOutput(agent, requestId, reason, {
6279
- stopReason: response.stopReason,
6280
- providerInputTokens,
6281
- visibleAssistantOutput,
6282
- executedToolCalls,
6283
- assistantBlocks: heldAssistantContent,
6284
- retryDispatch: primarySummaryRetry,
6285
- retryPhase: 'refusal_visible_output',
6286
- holdData: holdReasonData,
6287
- });
6288
- } else {
6289
- await this.holdPrimarySummaryRetryFamily(
6290
- agent,
6291
- requestId,
6292
- primarySummaryRetry,
6293
- reason,
6294
- holdReasonData,
6295
- );
6296
- }
6297
- } else if (visibleAssistantOutput || executedToolCalls > 0 || !currentRecord?.projection) {
6298
- const reason = visibleAssistantOutput
6299
- ? 'primary_summary_refusal_partial_output'
6300
- : executedToolCalls > 0
6301
- ? 'primary_summary_refusal_tool_already_executed'
6302
- : 'primary_summary_refusal_no_projection';
6303
- if (visibleAssistantOutput || executedToolCalls > 0) {
6304
- await this.persistPrimarySummaryHeldOutput(agent, requestId, reason, {
6305
- stopReason: response.stopReason,
6306
- providerInputTokens,
6307
- visibleAssistantOutput,
6308
- executedToolCalls,
6309
- assistantBlocks: heldAssistantContent,
6310
- holdData: holdReasonData,
6311
- });
6312
- } else {
6313
- await this.holdPrimarySummaryRequest(agent, requestId, reason, holdReasonData);
6314
- }
6315
- } else {
6316
- const state = this.readPrimarySummaryFallbackState();
6317
- const baseline = this.latestCompatibleHealthyPrimaryRequest(currentRecord, state);
6318
- if (!baseline?.projection) {
6319
- await this.holdPrimarySummaryRequest(
6320
- agent,
6321
- requestId,
6322
- 'primary_summary_refusal_no_compatible_healthy_baseline',
6323
- holdReasonData,
6324
- );
6325
- } else {
6326
- const baselineKeys = new Set(this.primarySummaryProjectionKeys(baseline.projection));
6327
- const candidates = currentRecord.projection.selectedSummaries
6328
- .filter((selection) => !baselineKeys.has(this.primarySummaryIdentityKey(selection.identity)))
6329
- .map((selection) => selection.identity);
6330
- if (candidates.length === 0) {
6331
- await this.holdPrimarySummaryRequest(
6332
- agent,
6333
- requestId,
6334
- 'primary_summary_refusal_no_newly_admitted_candidates',
6335
- holdReasonData,
6336
- );
6337
- } else if (candidates.length > primarySummaryConfig.maxNewSummaries) {
6338
- await this.holdPrimarySummaryRequest(
6339
- agent,
6340
- requestId,
6341
- 'primary_summary_refusal_too_many_candidates',
6342
- { ...holdReasonData, candidateCount: candidates.length },
6343
- );
6344
- } else {
6345
- try {
6346
- const retry = agent.buildPrimarySummaryRawExpansionRequest(
6347
- artifacts,
6348
- candidates,
6349
- compiledRequest.tools ?? [],
6350
- );
6351
- const retryRequestHash = this.hashJson(retry.request);
6352
- const admittedTokens = this.primarySummaryAdmissionBoundTokens(
6353
- currentRecord.requestInputBoundTokens,
6354
- retry.request,
6355
- providerInputTokens,
6356
- );
6357
- if (admittedTokens > primarySummaryConfig.requestBudgetTokens) {
6358
- await this.holdPrimarySummaryRequest(
6359
- agent,
6360
- requestId,
6361
- 'primary_summary_refusal_retry_over_budget',
6362
- {
6363
- ...holdReasonData,
6364
- admittedTokens,
6365
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
6366
- },
6367
- );
6368
- } else {
6369
- const intentId = `${requestId}:fallback:${Math.random().toString(36).slice(2, 8)}`;
6370
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6371
- ...record,
6372
- fallbackIntent: {
6373
- intentId,
6374
- createdAt: Date.now(),
6375
- baselineRequestId: baseline.requestId,
6376
- candidateSummaries: candidates,
6377
- retryRequestHash,
6378
- retryCompleteBoundTokens: admittedTokens,
6379
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
6380
- },
6381
- fallbackStatus: 'pending',
6382
- }));
6383
- this.pendingRequests.push({
6384
- agentName: agent.name,
6385
- reason: 'primary-summary-fallback-retry',
6386
- source: 'framework',
6387
- timestamp: Date.now(),
6388
- channelId: trigger?.channelId,
6389
- primarySummaryFallbackRetry: {
6390
- originalRequestId: requestId,
6391
- baselineRequestId: baseline.requestId,
6392
- intentId,
6393
- branch: {
6394
- id: currentRecord.branch.id,
6395
- generation: currentRecord.branch.generation,
6396
- },
6397
- candidateSummaries: candidates,
6398
- originalArtifacts: artifacts,
6399
- originalProjectionKeys: this.primarySummaryProjectionKeys(currentRecord.projection),
6400
- expectedRetryRequestHash: retryRequestHash,
6401
- requestBudgetTokens: primarySummaryConfig.requestBudgetTokens,
6402
- originalRequestInputBoundTokens: currentRecord.requestInputBoundTokens,
6403
- originalProviderInputTokens: providerInputTokens,
6404
- },
6405
- });
6406
- }
6407
- } catch (error) {
6408
- await this.holdPrimarySummaryRequest(
6409
- agent,
6410
- requestId,
6411
- 'primary_summary_refusal_retry_build_failed',
6412
- {
6413
- ...holdReasonData,
6414
- error: error instanceof Error ? error.message : String(error),
6415
- },
6416
- );
6417
- }
6418
- }
6419
- }
6420
- }
6421
-
6422
- agent.reset();
6423
- this.eventGate?.onInferenceEnded(agent.name);
6424
- this.settleAgent(agent.name, { stopReason: 'completed', speech: '' });
6425
- continue;
6426
- }
6427
-
6428
- if (!(await persistPrimarySummaryRetryQuarantine())) {
6429
- return;
6430
- }
6431
-
6432
4320
  // Add assistant response to context.
6433
4321
  // If we had tool calls, each round's blocks (thinking + text +
6434
4322
  // tool_use) were already stored as pendingAssistantBlocks and
@@ -6437,52 +4325,20 @@ export class AgentFramework {
6437
4325
  // double-store earlier rounds' text/thinking AND detach signed
6438
4326
  // thinking blocks from their tool_use turn (the API requires
6439
4327
  // thinking to precede tool_use in the same assistant turn).
4328
+ const lastToolIdx = response.content.reduce(
4329
+ (last: number, b: ContentBlock, i: number) =>
4330
+ b.type === 'tool_use' || b.type === 'tool_result' ? i : last,
4331
+ -1
4332
+ );
4333
+ const terminalContent = lastToolIdx >= 0
4334
+ ? response.content.slice(lastToolIdx + 1)
4335
+ : response.content;
6440
4336
  if (lastToolIdx >= 0) {
6441
4337
  if (terminalContent.length > 0) {
6442
- if (primarySummaryRetry) {
6443
- const retryRecord = await this.getPrimarySummaryRequestRecord(requestId);
6444
- const terminalMetadata = retryRecord
6445
- ? this.buildPrimarySummarySettlementMetadata(retryRecord, {
6446
- outcome: 'success',
6447
- stopReason: response.stopReason,
6448
- providerInputTokens,
6449
- visibleAssistantOutput,
6450
- executedToolCalls,
6451
- entryIndex: 0,
6452
- entryCount: 1,
6453
- role: 'assistant',
6454
- kind: 'assistant_output',
6455
- })
6456
- : undefined;
6457
- if (!(await persistPrimarySummaryRetryOutput('terminal', terminalContent, agent.name, terminalMetadata))) {
6458
- return;
6459
- }
6460
- } else {
6461
- agent.addAssistantResponse(terminalContent);
6462
- }
6463
- }
6464
- } else {
6465
- if (primarySummaryRetry) {
6466
- const retryRecord = await this.getPrimarySummaryRequestRecord(requestId);
6467
- const terminalMetadata = retryRecord && terminalContent.length > 0
6468
- ? this.buildPrimarySummarySettlementMetadata(retryRecord, {
6469
- outcome: 'success',
6470
- stopReason: response.stopReason,
6471
- providerInputTokens,
6472
- visibleAssistantOutput,
6473
- executedToolCalls,
6474
- entryIndex: 0,
6475
- entryCount: 1,
6476
- role: 'assistant',
6477
- kind: 'assistant_output',
6478
- })
6479
- : undefined;
6480
- if (!(await persistPrimarySummaryRetryOutput('terminal', terminalContent, agent.name, terminalMetadata))) {
6481
- return;
6482
- }
6483
- } else {
6484
4338
  agent.addAssistantResponse(terminalContent);
6485
4339
  }
4340
+ } else {
4341
+ agent.addAssistantResponse(terminalContent);
6486
4342
  }
6487
4343
 
6488
4344
  // Run afterInference hooks (no-op if no MCPL servers)
@@ -6544,50 +4400,6 @@ export class AgentFramework {
6544
4400
  }
6545
4401
  : undefined;
6546
4402
 
6547
- if (primarySummaryRetry && artifacts) {
6548
- const validation = await this.validatePrimarySummaryRetryMutation(
6549
- agent,
6550
- requestId,
6551
- primarySummaryRetry,
6552
- compiledRequest!,
6553
- artifacts,
6554
- 'before_success_markers',
6555
- );
6556
- if (!validation.ok) {
6557
- await this.holdPrimarySummaryRetryFamily(
6558
- agent,
6559
- requestId,
6560
- primarySummaryRetry,
6561
- validation.reason,
6562
- validation.data,
6563
- { emitChronicleMarker: false },
6564
- );
6565
- return;
6566
- }
6567
- }
6568
-
6569
- if (primarySummaryConfig.enabled) {
6570
- if (primarySummaryRetry) {
6571
- await this.markPrimarySummaryRetrySuccess(
6572
- requestId,
6573
- primarySummaryRetry,
6574
- response.stopReason,
6575
- providerInputTokens,
6576
- visibleAssistantOutput,
6577
- executedToolCalls,
6578
- );
6579
- } else {
6580
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6581
- ...record,
6582
- stopReason: response.stopReason,
6583
- providerInputTokens,
6584
- visibleAssistantOutput,
6585
- executedToolCalls,
6586
- finalStatus: 'success',
6587
- }));
6588
- }
6589
- }
6590
-
6591
4403
  // Reset agent state before emitting inference:completed. Traces are
6592
4404
  // observability-only, but external synchronous listeners should
6593
4405
  // still see the terminal state at the terminal trace boundary.
@@ -6626,10 +4438,8 @@ export class AgentFramework {
6626
4438
  agentName: agent.name,
6627
4439
  requestId,
6628
4440
  success: true,
6629
- request: fallbackRetryRequestForLog,
6630
- response: primarySummaryRetry
6631
- ? this.summarizePrimarySummaryRetryResponse(response)
6632
- : response.raw ?? { note: 'streaming response' },
4441
+ request: compiledRequest ?? { note: 'streaming request' },
4442
+ response: response.raw ?? { note: 'streaming response' },
6633
4443
  durationMs,
6634
4444
  tokenUsage,
6635
4445
  stopReason: response.stopReason,
@@ -6851,30 +4661,13 @@ export class AgentFramework {
6851
4661
  requestId,
6852
4662
  success: false,
6853
4663
  error: err.message,
6854
- request: fallbackRetryRequestForLog,
4664
+ request: compiledRequest ?? { note: 'streaming request failed' },
6855
4665
  durationMs,
6856
4666
  });
6857
4667
 
6858
4668
  // Only reset + retry if this is still the active stream
6859
4669
  if (agent.streamId !== myStreamId) break;
6860
4670
 
6861
- if (primarySummaryRetry) {
6862
- await this.holdPrimarySummaryRetryFamily(
6863
- agent,
6864
- requestId,
6865
- primarySummaryRetry,
6866
- 'primary_summary_fallback_retry_stream_error',
6867
- { error: err.message },
6868
- );
6869
- agent.reset();
6870
- this.eventGate?.onInferenceEnded(agent.name);
6871
- this.settleAgent(agent.name, {
6872
- stopReason: 'completed',
6873
- speech: '',
6874
- });
6875
- return;
6876
- }
6877
-
6878
4671
  agent.reset();
6879
4672
 
6880
4673
  const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
@@ -6914,17 +4707,6 @@ export class AgentFramework {
6914
4707
  // reject an ephemeral's promise mid-run and bump the failure
6915
4708
  // streak). Gate release + stream teardown happen in `finally`.
6916
4709
  if (this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`)) {
6917
- if (primarySummaryRetry) {
6918
- if (!(await this.primarySummaryRetryFamilyAlreadySettled(requestId, primarySummaryRetry))) {
6919
- await this.holdPrimarySummaryRetryFamily(
6920
- agent,
6921
- requestId,
6922
- primarySummaryRetry,
6923
- 'primary_summary_fallback_retry_cancelled_before_success',
6924
- { frameworkCancelled: true },
6925
- );
6926
- }
6927
- }
6928
4710
  this.eventGate?.onInferenceEnded(agent.name);
6929
4711
  return;
6930
4712
  }
@@ -6933,40 +4715,6 @@ export class AgentFramework {
6933
4715
  // may have already started a new stream, bumping streamId)
6934
4716
  if (agent.streamId === myStreamId) {
6935
4717
  const durationMs = Date.now() - startTime;
6936
- if (primarySummaryRetry) {
6937
- await this.holdPrimarySummaryRetryFamily(
6938
- agent,
6939
- requestId,
6940
- primarySummaryRetry,
6941
- `stream_aborted:${reason}`,
6942
- { reason },
6943
- { stopReason: 'abort' },
6944
- );
6945
- agent.reset();
6946
- this.eventGate?.onInferenceEnded(agent.name);
6947
- this.logInference({
6948
- timestamp: startTime,
6949
- agentName: agent.name,
6950
- requestId,
6951
- success: false,
6952
- error: `Stream aborted: ${reason}`,
6953
- request: fallbackRetryRequestForLog,
6954
- durationMs,
6955
- });
6956
- this.settleAgent(agent.name, {
6957
- stopReason: 'completed',
6958
- speech: '',
6959
- });
6960
- return;
6961
- }
6962
- if (primarySummaryConfig.enabled) {
6963
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
6964
- ...record,
6965
- stopReason: 'abort',
6966
- finalStatus: 'error',
6967
- fallbackHeldReason: `stream_aborted:${reason}`,
6968
- }));
6969
- }
6970
4718
  agent.reset();
6971
4719
  this.settleAgent(agent.name, {
6972
4720
  stopReason: 'exhausted',
@@ -6990,7 +4738,7 @@ export class AgentFramework {
6990
4738
  requestId,
6991
4739
  success: false,
6992
4740
  error: `Stream aborted: ${reason}`,
6993
- request: fallbackRetryRequestForLog,
4741
+ request: compiledRequest ?? { note: 'streaming request aborted' },
6994
4742
  durationMs,
6995
4743
  });
6996
4744
  this.eventGate?.onInferenceEnded(agent.name);
@@ -7042,63 +4790,12 @@ export class AgentFramework {
7042
4790
  // inference:exhausted so ephemeral agent promises can settle.
7043
4791
  const err = error instanceof Error ? error : new Error(String(error));
7044
4792
  const durationMs = Date.now() - startTime;
7045
- if (primarySummaryRetry) {
7046
- if (await this.primarySummaryRetryAlreadySucceeded(requestId, primarySummaryRetry)) {
7047
- this.logInference({
7048
- timestamp: startTime,
7049
- agentName: agent.name,
7050
- requestId,
7051
- success: true,
7052
- request: fallbackRetryRequestForLog,
7053
- response: { kind: 'primary_summary_fallback_retry', postSuccessError: err.message },
7054
- durationMs,
7055
- });
7056
- agent.reset();
7057
- this.eventGate?.onInferenceEnded(agent.name);
7058
- this.settleAgent(agent.name, {
7059
- stopReason: 'completed',
7060
- speech: '',
7061
- });
7062
- return;
7063
- }
7064
- await this.holdPrimarySummaryRetryFamily(
7065
- agent,
7066
- requestId,
7067
- primarySummaryRetry,
7068
- 'primary_summary_fallback_retry_stream_threw',
7069
- { error: err.message },
7070
- );
7071
- } else if (primarySummaryConfig.enabled) {
7072
- await this.updatePrimarySummaryRequestRecord(requestId, (record) => ({
7073
- ...record,
7074
- finalStatus: 'error',
7075
- fallbackHeldReason: err.message,
7076
- }));
7077
- }
7078
4793
  this.emitTrace({
7079
4794
  type: 'inference:failed',
7080
4795
  agentName: agent.name,
7081
4796
  error: err.message,
7082
4797
  stack: err.stack,
7083
4798
  });
7084
- if (primarySummaryRetry) {
7085
- this.logInference({
7086
- timestamp: startTime,
7087
- agentName: agent.name,
7088
- requestId,
7089
- success: false,
7090
- error: `Stream threw: ${err.message}`,
7091
- request: fallbackRetryRequestForLog,
7092
- durationMs,
7093
- });
7094
- agent.reset();
7095
- this.eventGate?.onInferenceEnded(agent.name);
7096
- this.settleAgent(agent.name, {
7097
- stopReason: 'completed',
7098
- speech: '',
7099
- });
7100
- return;
7101
- }
7102
4799
  this.settleAgent(agent.name, {
7103
4800
  stopReason: 'exhausted',
7104
4801
  speech: '',
@@ -7119,7 +4816,7 @@ export class AgentFramework {
7119
4816
  requestId,
7120
4817
  success: false,
7121
4818
  error: `Stream threw: ${err.message}`,
7122
- request: fallbackRetryRequestForLog,
4819
+ request: compiledRequest ?? { note: 'streaming request threw' },
7123
4820
  durationMs,
7124
4821
  });
7125
4822
  agent.reset();
@@ -7140,7 +4837,6 @@ export class AgentFramework {
7140
4837
  this.channelRegistry?.stopTyping();
7141
4838
  this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
7142
4839
  this.activeStreams.delete(agent.name);
7143
- this.activeStreamContexts.delete(agent.name);
7144
4840
  this.pendingAssistantBlocks.delete(agent.name);
7145
4841
 
7146
4842
  // A conversation fork whose TTL closure turn just finished is done for
@@ -7678,23 +5374,12 @@ export class AgentFramework {
7678
5374
  * work, per-agent status + last inference activity. Cheap and read-only.
7679
5375
  */
7680
5376
  healthSnapshot(): Record<string, unknown> {
7681
- const primarySummary = this.readPrimarySummaryFallbackState();
7682
5377
  return {
7683
5378
  at: new Date().toISOString(),
7684
5379
  uptimeSec: Math.round(process.uptime()),
7685
5380
  gate: this.eventGate?.inferenceDiagnostics() ?? null,
7686
5381
  pendingRequests: this.pendingRequests.length,
7687
5382
  activeStreams: [...this.activeStreams.keys()],
7688
- primarySummaryFallback: {
7689
- requests: primarySummary.requests.length,
7690
- pendingDispatches: primarySummary.requests.filter((record) => record.finalStatus === 'pending').length,
7691
- unresolvedIntents: primarySummary.requests.filter((record) => record.fallbackStatus === 'pending').length,
7692
- held: primarySummary.requests.filter((record) =>
7693
- record.finalStatus === 'held' ||
7694
- record.finalStatus === 'timeout_unknown' ||
7695
- record.fallbackStatus === 'held' ||
7696
- record.fallbackStatus === 'timeout_unknown').length,
7697
- },
7698
5383
  agents: [...this.agents.entries()].map(([name, agent]) => ({
7699
5384
  name,
7700
5385
  status: agent.state.status,