@modelprofile.com/flexharness-agent 6.0.0 → 7.0.0

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 (40) hide show
  1. package/dist_ts_agent/classes.sessionengine.d.ts +93 -0
  2. package/dist_ts_agent/classes.sessionengine.js +2140 -0
  3. package/dist_ts_agent/index.d.ts +3 -2
  4. package/dist_ts_agent/index.js +3 -2
  5. package/dist_ts_agent/persistence.file.d.ts +24 -0
  6. package/dist_ts_agent/persistence.file.js +199 -0
  7. package/dist_ts_agent/plugins.d.ts +0 -7
  8. package/dist_ts_agent/plugins.js +1 -9
  9. package/dist_ts_agent/plugins.node.d.ts +7 -0
  10. package/dist_ts_agent/plugins.node.js +9 -0
  11. package/dist_ts_agent/plugins.portable.d.ts +4 -0
  12. package/dist_ts_agent/plugins.portable.js +5 -0
  13. package/dist_ts_agent/runner.d.ts +4 -0
  14. package/dist_ts_agent/runner.js +34 -0
  15. package/dist_ts_agent/runtime.interfaces.d.ts +21 -0
  16. package/dist_ts_agent/runtime.interfaces.js +2 -0
  17. package/dist_ts_agent/runtime.run.d.ts +3 -0
  18. package/dist_ts_agent/runtime.run.js +94 -0
  19. package/dist_ts_agent/smartagent.classes.agent.js +4 -93
  20. package/dist_ts_agent/smartagent.classes.session.d.ts +5 -88
  21. package/dist_ts_agent/smartagent.classes.session.js +19 -2134
  22. package/dist_ts_agent/smartagent.events.js +2 -2
  23. package/dist_ts_agent/smartagent.persistence.d.ts +2 -22
  24. package/dist_ts_agent/smartagent.persistence.js +3 -198
  25. package/package.json +9 -3
  26. package/readme.md +9 -3
  27. package/ts_agent/classes.sessionengine.ts +2591 -0
  28. package/ts_agent/index.ts +2 -2
  29. package/ts_agent/persistence.file.ts +232 -0
  30. package/ts_agent/plugins.node.ts +10 -0
  31. package/ts_agent/plugins.portable.ts +5 -0
  32. package/ts_agent/plugins.ts +0 -10
  33. package/ts_agent/readme.md +9 -3
  34. package/ts_agent/runner.ts +40 -0
  35. package/ts_agent/runtime.interfaces.ts +21 -0
  36. package/ts_agent/runtime.run.ts +107 -0
  37. package/ts_agent/smartagent.classes.agent.ts +4 -101
  38. package/ts_agent/smartagent.classes.session.ts +24 -2582
  39. package/ts_agent/smartagent.events.ts +1 -1
  40. package/ts_agent/smartagent.persistence.ts +2 -224
@@ -1,2588 +1,30 @@
1
- import * as plugins from './plugins.js';
2
- import { buildModelMessages } from './smartagent.context.js';
3
- import {
4
- createAgentEvent,
5
- createAgentEventId,
6
- modelMessagesToAgentEvents,
7
- type IArchivedAgentGenerationTransaction,
8
- type IAssistantMessageEvent,
9
- type IContextCompactionEvent,
10
- type IGenerationBegunEvent,
11
- type IGenerationExecutionCompletedEvent,
12
- type IGenerationExecutionStartedEvent,
13
- type IGenerationOutcomeEvent,
14
- type IRuntimeEvent,
15
- type IToolCallEvent,
16
- type IToolExecutionIntentEvent,
17
- type IToolExecutionReconciliationEvent,
18
- type IToolResultEvent,
19
- type IUserMessageEvent,
20
- type TAgentEvent,
21
- type TAgentGenerationOutcome,
22
- type TAgentToolResultOutput,
23
- } from './smartagent.events.js';
24
- import {
25
- AgentGenerationLeaseCleanupError,
26
- ContextOverflowError,
27
- type IAgentGenerateOptions,
28
- type IAgentGenerateResult,
29
- type IAgentGenerationHandle,
30
- type IAgentGenerationLease,
31
- type IAgentCompactOptions,
32
- type IAgentBeginGenerationOptions,
33
- type IAgentSession,
34
- type IAgentSessionAbortOptions,
35
- type IAgentSessionChange,
36
- type IAgentSessionOptions,
37
- type IAgentScheduleGenerateOptions,
38
- type IAgentToolCallRecord,
39
- type IAgentToolCallStartEvent,
40
- type IAgentToolCallUpdateEvent,
41
- type TAgentSessionChangeListener,
42
- type TAgentCacheSetting,
43
- type TAgentGenerationPrepare,
44
- type TAgentPrompt,
45
- type TAgentToolCallFinishEvent,
46
- type TAgentToolExecutionReconciliationOptions,
47
- } from './smartagent.interfaces.js';
48
- import {
49
- isAgentEventStoreV2,
50
- validateAgentEventSnapshotV2,
51
- } from './smartagent.persistence.js';
52
- import type {
53
- IAgentEventArchive,
54
- IAgentEventArchiveV2,
55
- TAgentEventArchive,
56
- TAgentEventSnapshot,
57
- } from './smartagent.persistence.js';
58
- import {
59
- filterModelVisibleAgentEvents,
60
- getAgentGenerationTransactions,
61
- getOpenAgentGenerationTransactions,
62
- getUncertainToolExecutionIntents,
63
- type IAgentGenerationTransaction,
64
- } from './smartagent.transactions.js';
65
-
66
- const RETRY_INITIAL_DELAY = 2000;
67
- const RETRY_BACKOFF_FACTOR = 2;
68
- const RETRY_MAX_DELAY = 30_000;
69
- const MAX_RETRY_ATTEMPTS = 8;
70
-
71
- const isTransactionalControlEvent = (event: TAgentEvent): boolean =>
72
- event.type === 'generation-begun'
73
- || event.type === 'generation-execution-started'
74
- || event.type === 'generation-execution-completed'
75
- || event.type === 'generation-outcome'
76
- || event.type === 'tool-execution-intent'
77
- || event.type === 'tool-execution-reconciliation';
78
-
79
- interface IToolCallPreparationLedgerEntry {
80
- signature: string;
81
- promise: Promise<void>;
82
- }
83
-
84
- interface IToolExecutionLedgerEntry {
85
- signature: string;
86
- promise: Promise<unknown>;
87
- }
88
-
89
- interface ICombinedAbortSignal {
90
- signal: AbortSignal;
91
- cleanup: () => void;
92
- }
93
-
94
- interface IScheduledGeneration {
95
- state: 'debouncing' | 'queued';
96
- debounceMs: number;
97
- model?: plugins.TSmartAiLanguageModel;
98
- system?: string;
99
- tools?: plugins.ToolSet;
100
- providerOptions?: plugins.ProviderOptions;
101
- cache?: TAgentCacheSetting;
102
- maxSteps?: number;
103
- abort?: AbortSignal;
104
- transaction?: IAgentGenerationHandle;
105
- prepare?: TAgentGenerationPrepare;
106
- timer: NodeJS.Timeout;
107
- controller: AbortController;
108
- removeAbortListener: () => void;
109
- promise: Promise<IAgentGenerateResult>;
110
- resolve: (result: IAgentGenerateResult) => void;
111
- reject: (error: unknown) => void;
112
- }
113
-
114
- interface IEffectiveGenerationRuntime {
115
- model: plugins.TSmartAiLanguageModel;
116
- system?: string;
117
- tools: plugins.ToolSet;
118
- providerOptions?: plugins.ProviderOptions;
119
- maxSteps: number;
120
- }
121
-
122
- interface IAgentSessionListenerState {
123
- listener: TAgentSessionChangeListener;
124
- queue: IAgentSessionChange[];
125
- deliveryPromise?: Promise<void>;
126
- executing: boolean;
127
- subscribed: boolean;
128
- }
129
-
130
- interface IGenerationCallbackState {
131
- active: boolean;
132
- }
133
-
134
- interface IGenerationLeaseCleanupOwnership {
135
- lease: IAgentGenerationLease;
136
- settled: boolean;
137
- }
138
-
139
- const sessionHydrations = new WeakMap<IAgentSessionOptions, TAgentEventSnapshot | undefined>();
140
-
141
- const retryDelay = (attempt: number, headers?: Record<string, string>): number => {
142
- if (headers) {
143
- const milliseconds = headers['retry-after-ms'];
144
- if (milliseconds) {
145
- const parsed = Number.parseFloat(milliseconds);
146
- if (!Number.isNaN(parsed)) return parsed;
147
- }
148
- const retryAfter = headers['retry-after'];
149
- if (retryAfter) {
150
- const seconds = Number.parseFloat(retryAfter);
151
- if (!Number.isNaN(seconds)) return Math.ceil(seconds * 1000);
152
- const dateDelay = Date.parse(retryAfter) - Date.now();
153
- if (!Number.isNaN(dateDelay) && dateDelay > 0) return Math.ceil(dateDelay);
154
- }
155
- }
156
- return Math.min(
157
- RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1),
158
- RETRY_MAX_DELAY,
159
- );
160
- };
161
-
162
- const sleep = async (milliseconds: number, signal?: AbortSignal): Promise<void> =>
163
- new Promise((resolve, reject) => {
164
- if (signal?.aborted) {
165
- reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
166
- return;
167
- }
168
- let settled = false;
169
- const cleanup = () => signal?.removeEventListener('abort', onAbort);
170
- const onAbort = () => {
171
- if (settled) return;
172
- settled = true;
173
- clearTimeout(timer);
174
- cleanup();
175
- reject(signal?.reason ?? new DOMException('Aborted', 'AbortError'));
176
- };
177
- const timer = setTimeout(() => {
178
- if (settled) return;
179
- settled = true;
180
- cleanup();
181
- resolve();
182
- }, milliseconds);
183
- signal?.addEventListener('abort', onAbort, { once: true });
184
- });
185
-
186
- const combineAbortSignals = (signals: Array<AbortSignal | undefined>): ICombinedAbortSignal => {
187
- const controller = new AbortController();
188
- const activeSignals = signals.filter((signal): signal is AbortSignal => !!signal);
189
- const listeners = new Map<AbortSignal, () => void>();
190
- const cleanup = () => {
191
- for (const [signal, listener] of listeners) signal.removeEventListener('abort', listener);
192
- listeners.clear();
193
- };
194
- for (const signal of activeSignals) {
195
- const listener = () => {
196
- if (!controller.signal.aborted) {
197
- controller.abort(signal.reason ?? new DOMException('Aborted', 'AbortError'));
198
- }
199
- cleanup();
200
- };
201
- if (signal.aborted) {
202
- listener();
203
- break;
204
- }
205
- listeners.set(signal, listener);
206
- signal.addEventListener('abort', listener, { once: true });
207
- }
208
- return { signal: controller.signal, cleanup };
209
- };
210
-
211
- const waitWithAbort = <TValue>(
212
- promise: Promise<TValue>,
213
- signals: Array<AbortSignal | undefined>,
214
- ): Promise<TValue> => {
215
- const activeSignals = signals.filter((signal): signal is AbortSignal => !!signal);
216
- if (activeSignals.length === 0) return promise;
217
- return new Promise<TValue>((resolve, reject) => {
218
- let settled = false;
219
- const listeners = new Map<AbortSignal, () => void>();
220
- const cleanup = () => {
221
- for (const [signal, listener] of listeners) signal.removeEventListener('abort', listener);
222
- listeners.clear();
223
- };
224
- const settle = (handler: () => void) => {
225
- if (settled) return;
226
- settled = true;
227
- cleanup();
228
- handler();
229
- };
230
- for (const signal of activeSignals) {
231
- const listener = () => settle(() => reject(
232
- signal.reason ?? new DOMException('Aborted', 'AbortError'),
233
- ));
234
- if (signal.aborted) {
235
- listener();
236
- return;
237
- }
238
- listeners.set(signal, listener);
239
- signal.addEventListener('abort', listener, { once: true });
240
- }
241
- promise.then(
242
- (value) => settle(() => resolve(value)),
243
- (error) => settle(() => reject(error)),
244
- );
245
- });
246
- };
247
-
248
- const isRetryableError = (error: unknown): boolean => {
249
- const candidate = error as { status?: number; statusCode?: number };
250
- const status = candidate?.status ?? candidate?.statusCode;
251
- if (status === 429 || status === 529 || status === 503) return true;
252
- if (!(error instanceof Error)) return false;
253
- const message = error.message.toLowerCase();
254
- return message.includes('rate limit')
255
- || message.includes('overloaded')
256
- || message.includes('too many requests');
257
- };
258
-
259
- const isContextOverflow = (error: unknown): boolean => {
260
- if (!(error instanceof Error)) return false;
261
- const message = error.message.toLowerCase();
262
- return message.includes('context_length_exceeded')
263
- || message.includes('context window')
264
- || message.includes('maximum context length')
265
- || message.includes('too many tokens')
266
- || message.includes('input is too long')
267
- || error.name === 'AI_ContextWindowExceededError';
268
- };
269
-
270
- const parseToolInput = (input: unknown): unknown => {
271
- if (typeof input !== 'string') return input;
272
- try {
273
- return JSON.parse(input);
274
- } catch {
275
- return input;
276
- }
277
- };
278
-
279
- const errorToString = (error: unknown): string =>
280
- error instanceof Error ? error.message : String(error);
281
-
282
- const toolSignature = (toolName: string, input: unknown): string => {
283
- let serializedInput: string;
284
- try {
285
- serializedInput = JSON.stringify(input);
286
- } catch {
287
- serializedInput = String(input);
288
- }
289
- return `${toolName}\n${serializedInput}`;
290
- };
291
-
292
- const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
293
- ((typeof value === 'object' && value !== null) || typeof value === 'function')
294
- && typeof (value as { then?: unknown }).then === 'function';
295
-
296
- const isAsyncIterable = (value: unknown): value is AsyncIterable<unknown> =>
297
- ((typeof value === 'object' && value !== null) || typeof value === 'function')
298
- && typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function';
299
-
300
- const fallbackModelOutput = (output: unknown, error?: string): TAgentToolResultOutput => {
301
- if (error !== undefined) return { type: 'error-text', value: error };
302
- if (typeof output === 'string') return { type: 'text', value: output };
303
- let jsonOutput: unknown;
304
- try {
305
- jsonOutput = output === undefined ? null : JSON.parse(JSON.stringify(output));
306
- } catch {
307
- jsonOutput = String(output);
308
- }
309
- return { type: 'json', value: jsonOutput } as TAgentToolResultOutput;
310
- };
311
-
312
- const recordToolCall = (
313
- toolCalls: IAgentToolCallRecord[],
314
- toolCallIndexes: Map<string, number>,
315
- toolCall: { toolCallId?: unknown; toolName?: unknown; input?: unknown; args?: unknown },
316
- update: { output?: unknown; error?: unknown } = {},
317
- ): void => {
318
- const toolCallId = String(toolCall.toolCallId ?? '');
319
- const nextRecord: IAgentToolCallRecord = {
320
- toolCallId,
321
- toolName: String(toolCall.toolName ?? ''),
322
- input: parseToolInput(toolCall.input ?? toolCall.args),
323
- };
324
- const hasOutput = Object.prototype.hasOwnProperty.call(update, 'output');
325
- const hasError = Object.prototype.hasOwnProperty.call(update, 'error');
326
- if (hasOutput) nextRecord.output = update.output;
327
- if (hasError && update.error !== undefined) nextRecord.error = errorToString(update.error);
328
-
329
- const existingIndex = toolCallIndexes.get(toolCallId);
330
- if (existingIndex !== undefined) {
331
- const existing = toolCalls[existingIndex];
332
- existing.toolName = nextRecord.toolName || existing.toolName;
333
- if (nextRecord.input !== undefined) existing.input = nextRecord.input;
334
- if (hasOutput) existing.output = nextRecord.output;
335
- if (nextRecord.error !== undefined) existing.error = nextRecord.error;
336
- return;
337
- }
338
- toolCalls.push(nextRecord);
339
- toolCallIndexes.set(toolCallId, toolCalls.length - 1);
340
- };
341
-
342
- export class AgentSession implements IAgentSession {
343
- private readonly options: IAgentSessionOptions;
344
- private readonly events: TAgentEvent[] = [];
345
- private readonly eventIds = new Set<string>();
346
- private readonly generationHandles = new Map<string, IAgentGenerationHandle>();
347
- private readonly unresolvedLeaseCleanups = new Set<IGenerationLeaseCleanupOwnership>();
348
- private sequence = 0;
349
- private persistenceRevision = 0;
350
- private persistenceQueue: Promise<void> = Promise.resolve();
351
- private persistenceError: unknown;
352
- private readonly notificationDispatches = new Set<Promise<void>>();
353
- private readonly listeners = new Map<TAgentSessionChangeListener, IAgentSessionListenerState>();
354
- private readonly scheduledGenerations = new Map<string, IScheduledGeneration>();
355
- private readonly generationCallbackContext = new plugins.AsyncLocalStorage<IGenerationCallbackState>();
356
- private retentionPromise: Promise<void> | undefined;
357
- private maintenanceError: unknown;
358
- private generationQueue: Promise<void> = Promise.resolve();
359
- private currentGenerationAbortController: AbortController | undefined;
360
- private readonly sessionAbortController = new AbortController();
361
- private jobUnsubscribe: (() => void) | undefined;
362
- private closed = false;
363
- private closeFinalizationReached = false;
364
- private closeCleanupCompletedState = false;
365
-
366
- public get closeCleanupCompleted(): boolean {
367
- return this.closeCleanupCompletedState;
368
- }
369
-
370
- private refreshCloseCleanupCompleted(): void {
371
- this.closeCleanupCompletedState = this.closeFinalizationReached
372
- && this.jobUnsubscribe === undefined
373
- && this.unresolvedLeaseCleanups.size === 0;
374
- }
375
-
376
- public static async create(options: IAgentSessionOptions): Promise<AgentSession> {
377
- if (!options.eventStore) return new AgentSession(options);
378
- if (!options.sessionId?.trim()) {
379
- throw new Error('AgentSession.create() requires sessionId when eventStore is configured.');
380
- }
381
- let snapshot = await options.eventStore.load(options.sessionId);
382
- const v2Store = isAgentEventStoreV2(options.eventStore);
383
- if (
384
- snapshot
385
- && ((!v2Store && snapshot.schemaVersion !== 1)
386
- || (v2Store && snapshot.schemaVersion !== 1 && snapshot.schemaVersion !== 2))
387
- ) {
388
- throw new Error(
389
- `Unsupported agent event snapshot schema version ${String((snapshot as { schemaVersion?: unknown }).schemaVersion)}.`,
390
- );
391
- }
392
- if (!v2Store && snapshot?.events.some(isTransactionalControlEvent)) {
393
- throw new Error('Transactional events require an IAgentEventStoreV2-capable event store.');
394
- }
395
- if (snapshot?.schemaVersion === 2) {
396
- snapshot = validateAgentEventSnapshotV2(snapshot, options.sessionId);
397
- }
398
- sessionHydrations.set(options, snapshot);
399
- let session: AgentSession;
400
- try {
401
- session = new AgentSession(options);
402
- } finally {
403
- sessionHydrations.delete(options);
404
- }
405
- await session.interruptRestoredTransactions();
406
- await session.flushPersistence();
407
- await session.runEventRetentionLocked();
408
- session.attachJobSubscription();
409
- return session;
410
- }
411
-
1
+ import * as plugins from './plugins.node.js';
2
+ import { AgentSessionEngine } from './classes.sessionengine.js';
3
+ import type { IAgentSessionOptions } from './smartagent.interfaces.js';
4
+ import type { IAgentCallbackState, IAgentRuntimePlatform } from './runtime.interfaces.js';
5
+
6
+ const nodePlatform: IAgentRuntimePlatform = {
7
+ createClaimToken: () => plugins.crypto.randomBytes(32).toString('base64url'),
8
+ hashClaimToken: (token) => plugins.crypto.createHash('sha256').update(token).digest('hex'),
9
+ equalClaimDigests: (left, right) => plugins.crypto.timingSafeEqual(
10
+ Buffer.from(left, 'hex'), Buffer.from(right, 'hex'),
11
+ ),
12
+ isDeepEqual: plugins.isDeepStrictEqual,
13
+ reportListenerError: (error) => process.emitWarning(error instanceof Error ? error : String(error), {
14
+ code: 'SMARTAGENT_SESSION_LISTENER_ERROR',
15
+ }),
16
+ };
17
+
18
+ /** Node session facade; callback ancestry remains isolated with AsyncLocalStorage. */
19
+ export class AgentSession extends AgentSessionEngine {
412
20
  constructor(options: IAgentSessionOptions) {
413
- this.options = options;
414
- const hasHydration = sessionHydrations.has(options);
415
- const hydrationSnapshot = sessionHydrations.get(options);
416
- if (options.eventStore && !hasHydration) {
417
- throw new Error('Use AgentSession.create() when eventStore is configured.');
418
- }
419
- if (options.eventStore && !options.sessionId?.trim()) {
420
- throw new Error('sessionId is required when eventStore is configured.');
421
- }
422
- const tools = options.tools ?? {};
423
- if (Object.prototype.hasOwnProperty.call(tools, 'invalid')) {
424
- throw new Error('Tool name "invalid" is reserved for SmartAgent tool-call repair.');
425
- }
426
- const maxContextOverflowRetries = options.maxContextOverflowRetries ?? 3;
427
- if (!Number.isSafeInteger(maxContextOverflowRetries) || maxContextOverflowRetries < 0) {
428
- throw new Error('maxContextOverflowRetries must be a non-negative integer.');
429
- }
430
- const changeListenerTimeoutMs = options.changeListenerTimeoutMs ?? 30_000;
431
- if (!Number.isSafeInteger(changeListenerTimeoutMs) || changeListenerTimeoutMs <= 0) {
432
- throw new Error('changeListenerTimeoutMs must be a positive integer.');
433
- }
434
- const maxPendingSessionChanges = options.maxPendingSessionChanges ?? 1000;
435
- if (!Number.isSafeInteger(maxPendingSessionChanges) || maxPendingSessionChanges <= 0) {
436
- throw new Error('maxPendingSessionChanges must be a positive integer.');
437
- }
438
- const generationLeaseCleanupTimeoutMs = options.generationLeaseCleanupTimeoutMs ?? 30_000;
439
- if (!Number.isSafeInteger(generationLeaseCleanupTimeoutMs) || generationLeaseCleanupTimeoutMs <= 0) {
440
- throw new Error('generationLeaseCleanupTimeoutMs must be a positive integer.');
441
- }
442
- const maxArchivedTransactionTombstones = options.maxArchivedTransactionTombstones ?? 1000;
443
- if (
444
- !Number.isSafeInteger(maxArchivedTransactionTombstones)
445
- || maxArchivedTransactionTombstones <= 0
446
- ) {
447
- throw new Error('maxArchivedTransactionTombstones must be a positive integer.');
448
- }
449
- if (options.eventRetention) {
450
- if (
451
- !Number.isSafeInteger(options.eventRetention.maxEvents)
452
- || options.eventRetention.maxEvents <= 0
453
- ) {
454
- throw new Error('eventRetention.maxEvents must be a positive integer.');
455
- }
456
- if (!options.contextCompactor) {
457
- throw new Error('eventRetention requires contextCompactor.');
458
- }
459
- if (!options.eventStore?.archive) {
460
- throw new Error('eventRetention requires an eventStore with archive support.');
461
- }
462
- }
463
-
464
- if (hydrationSnapshot) {
465
- this.persistenceRevision = hydrationSnapshot.revision;
466
- for (const event of hydrationSnapshot.events) this.restoreEvent(event);
467
- }
468
- const initialEvents: TAgentEvent[] = [
469
- ...(options.events ?? []),
470
- ...modelMessagesToAgentEvents(options.messages ?? []),
471
- ];
472
- if (hydrationSnapshot && initialEvents.length > 0) {
473
- throw new Error('Persisted sessions cannot also receive initial events or messages.');
474
- }
475
- if (
476
- options.eventStore
477
- && !isAgentEventStoreV2(options.eventStore)
478
- && initialEvents.some(isTransactionalControlEvent)
479
- ) {
480
- throw new Error('Transactional events require an IAgentEventStoreV2-capable event store.');
481
- }
482
- for (const event of initialEvents) this.commitEvent(event, false);
483
- if (options.eventStore && (!hydrationSnapshot || initialEvents.length > 0)) {
484
- this.queuePersistence();
485
- }
486
- if (!options.eventStore) this.attachJobSubscription();
487
- }
488
-
489
- private attachJobSubscription(): void {
490
- if (this.jobUnsubscribe) return;
491
- this.jobUnsubscribe = this.options.executionContext?.jobs?.subscribe?.(async (jobEvent) => {
492
- if (this.closed) return;
493
- const job = jobEvent.job;
494
- const runtimeType = `${job.type}-${jobEvent.type}`;
495
- await this.pushRuntimeEvent({
496
- type: runtimeType,
497
- executionId: job.executionId,
498
- jobType: job.type,
499
- state: job.state,
500
- request: job.request,
501
- exitCode: job.exitCode,
502
- stdout: job.stdout,
503
- stderr: job.stderr,
504
- signal: job.signal,
505
- ...(jobEvent.type === 'output'
506
- ? {
507
- stream: jobEvent.stream!,
508
- chunk: jobEvent.chunk!,
509
- outputSequence: jobEvent.sequence!,
510
- }
511
- : {}),
512
- });
513
- });
514
- }
515
-
516
- private restoreEvent(event: TAgentEvent): void {
517
- if (this.eventIds.has(event.id)) throw new Error(`Duplicate agent event id "${event.id}".`);
518
- if (!Number.isSafeInteger(event.sequence) || (event.sequence ?? 0) <= this.sequence) {
519
- throw new Error(`Invalid persisted sequence for agent event "${event.id}".`);
520
- }
521
- this.events.push(event);
522
- this.eventIds.add(event.id);
523
- this.sequence = event.sequence!;
524
- }
525
-
526
- private queuePersistence(): Promise<void> {
527
- if (!this.options.eventStore) return Promise.resolve();
528
- const snapshot = structuredClone(this.events);
529
- const operation = this.persistenceQueue.then(async () => {
530
- if (this.persistenceError !== undefined) throw this.persistenceError;
531
- this.persistenceRevision = await this.options.eventStore!.save(
532
- this.options.sessionId!,
533
- snapshot,
534
- this.persistenceRevision,
535
- );
536
- });
537
- this.persistenceQueue = operation.catch((error) => {
538
- this.persistenceError ??= error;
539
- });
540
- return operation;
541
- }
542
-
543
- private async flushPersistence(): Promise<void> {
544
- await this.persistenceQueue;
545
- if (this.persistenceError !== undefined) throw this.persistenceError;
546
- }
547
-
548
- private reportListenerError(error: unknown): void {
549
- process.emitWarning(error instanceof Error ? error : String(error), {
550
- code: 'SMARTAGENT_SESSION_LISTENER_ERROR',
551
- });
552
- }
553
-
554
- private queueNotification(change: IAgentSessionChange, durability: Promise<void>): void {
555
- const dispatch = (async () => {
556
- try {
557
- await durability;
558
- } catch {
559
- return;
560
- }
561
- for (const state of this.listeners.values()) {
562
- this.enqueueListenerChange(state, change);
563
- }
564
- })();
565
- this.notificationDispatches.add(dispatch);
566
- void dispatch.then(
567
- () => this.notificationDispatches.delete(dispatch),
568
- () => this.notificationDispatches.delete(dispatch),
569
- );
570
- }
571
-
572
- private enqueueListenerChange(
573
- state: IAgentSessionListenerState,
574
- change: IAgentSessionChange,
575
- ): void {
576
- if (!state.subscribed) return;
577
- const maxPending = this.options.maxPendingSessionChanges ?? 1000;
578
- if (state.queue.length >= maxPending) {
579
- state.subscribed = false;
580
- state.queue.length = 0;
581
- this.listeners.delete(state.listener);
582
- this.reportListenerError(new Error(
583
- `Session change listener exceeded ${maxPending} pending changes and was removed.`,
584
- ));
585
- return;
586
- }
587
- state.queue.push(change);
588
- if (state.deliveryPromise) return;
589
- const delivery = this.deliverListenerChanges(state);
590
- state.deliveryPromise = delivery;
591
- void delivery.then(() => {
592
- if (state.deliveryPromise === delivery) state.deliveryPromise = undefined;
21
+ super(options, nodePlatform, {
22
+ kind: 'exposed',
23
+ callbackScope: new plugins.AsyncLocalStorage<IAgentCallbackState>(),
593
24
  });
594
25
  }
595
26
 
596
- private async deliverListenerChanges(state: IAgentSessionListenerState): Promise<void> {
597
- while (state.subscribed && state.queue.length > 0) {
598
- const change = state.queue.shift()!;
599
- let timer: NodeJS.Timeout | undefined;
600
- state.executing = true;
601
- try {
602
- await Promise.race([
603
- Promise.resolve().then(() => state.listener(change)),
604
- new Promise<never>((_resolve, reject) => {
605
- timer = setTimeout(
606
- () => reject(new Error(
607
- `Session change listener timed out after ${this.options.changeListenerTimeoutMs ?? 30_000}ms.`,
608
- )),
609
- this.options.changeListenerTimeoutMs ?? 30_000,
610
- );
611
- }),
612
- ]);
613
- } catch (error) {
614
- state.subscribed = false;
615
- state.queue.length = 0;
616
- this.listeners.delete(state.listener);
617
- this.reportListenerError(error);
618
- } finally {
619
- state.executing = false;
620
- if (timer) clearTimeout(timer);
621
- }
622
- }
623
- }
624
-
625
- private commitEvents<TEvent extends TAgentEvent>(
626
- events: readonly TEvent[],
627
- notify = true,
628
- ): TEvent[] {
629
- const nextIds = new Set<string>();
630
- for (const event of events) {
631
- if (this.eventIds.has(event.id) || nextIds.has(event.id)) {
632
- throw new Error(`Duplicate agent event id "${event.id}".`);
633
- }
634
- nextIds.add(event.id);
635
- }
636
- if (this.options.eventStore) structuredClone(events);
637
- const committedEvents = events.map((event) => ({
638
- ...event,
639
- sequence: ++this.sequence,
640
- }) as TEvent);
641
- for (const event of committedEvents) {
642
- this.events.push(event);
643
- this.eventIds.add(event.id);
644
- if (event.type === 'generation-outcome') {
645
- this.generationHandles.delete(event.generationId);
646
- }
647
- }
648
- if (notify && committedEvents.length > 0) {
649
- const durability = this.queuePersistence();
650
- this.queueNotification({ type: 'committed', events: committedEvents }, durability);
651
- }
652
- return committedEvents;
653
- }
654
-
655
- private commitEvent<TEvent extends TAgentEvent>(event: TEvent, notify = true): TEvent {
656
- return this.commitEvents([event], notify)[0];
657
- }
658
-
659
- public pushEvent(event: TAgentEvent): Promise<void> {
660
- if (this.closed) return Promise.reject(new Error('AgentSession is closed.'));
661
- if (this.persistenceError !== undefined) return Promise.reject(this.persistenceError);
662
- if (this.maintenanceError !== undefined) return Promise.reject(this.maintenanceError);
663
- if (
664
- isTransactionalControlEvent(event)
665
- && this.options.eventStore
666
- && !isAgentEventStoreV2(this.options.eventStore)
667
- ) {
668
- return Promise.reject(
669
- new Error('Transactional events require an IAgentEventStoreV2-capable event store.'),
670
- );
671
- }
672
- this.commitEvent(event);
673
- return this.flushPersistence().then(async () => {
674
- this.scheduleEventRetention();
675
- if (!this.currentGenerationAbortController && this.retentionPromise) {
676
- await this.retentionPromise;
677
- }
678
- });
679
- }
680
-
681
- public pushUserMessage(content: TAgentPrompt): Promise<void> {
682
- return this.pushEvent(createAgentEvent<IUserMessageEvent>({
683
- type: 'user-message',
684
- content,
685
- parentIds: this.events.length > 0 ? [this.events[this.events.length - 1].id] : undefined,
686
- }));
687
- }
688
-
689
- public pushRuntimeEvent(event: import('./smartagent.events.js').IAgentRuntimeEventPayload): Promise<void> {
690
- return this.pushEvent(createAgentEvent<IRuntimeEvent>({
691
- type: 'runtime-event',
692
- event,
693
- parentIds: this.events.length > 0 ? [this.events[this.events.length - 1].id] : undefined,
694
- }));
695
- }
696
-
697
- private transaction(generationId: string): IAgentGenerationTransaction {
698
- const transaction = getAgentGenerationTransactions(this.events).get(generationId);
699
- if (!transaction) {
700
- throw new Error(`Unknown transactional generation "${generationId}".`);
701
- }
702
- if (transaction.state === 'invalid') {
703
- throw new Error(`Transactional generation "${generationId}" has an invalid event sequence.`);
704
- }
705
- return transaction;
706
- }
707
-
708
- private archivedTransaction(
709
- generationId: string,
710
- ): IArchivedAgentGenerationTransaction | undefined {
711
- for (let index = this.events.length - 1; index >= 0; index--) {
712
- const event = this.events[index];
713
- if (event.type !== 'context-compaction') continue;
714
- const transaction = event.archivedTransactions?.find((entry) =>
715
- entry.generationId === generationId);
716
- if (transaction) return transaction;
717
- }
718
- return undefined;
719
- }
720
-
721
- private verifyGenerationClaim(
722
- transaction: IAgentGenerationHandle,
723
- ): IAgentGenerationTransaction | IArchivedAgentGenerationTransaction {
724
- if (!transaction?.generationId?.trim() || !transaction.claimToken?.trim()) {
725
- throw new Error('A valid transactional generation handle is required.');
726
- }
727
- const activeTransaction = getAgentGenerationTransactions(this.events).get(transaction.generationId);
728
- if (activeTransaction?.state === 'invalid') {
729
- throw new Error(
730
- `Transactional generation "${transaction.generationId}" has an invalid event sequence.`,
731
- );
732
- }
733
- const state = activeTransaction ?? this.archivedTransaction(transaction.generationId);
734
- if (!state) throw new Error(`Unknown transactional generation "${transaction.generationId}".`);
735
- const expectedDigest = 'begun' in state
736
- ? state.begun.claimTokenDigest
737
- : state.claimTokenDigest;
738
- const actualDigest = plugins.crypto
739
- .createHash('sha256')
740
- .update(transaction.claimToken)
741
- .digest('hex');
742
- if (
743
- !/^[a-f0-9]{64}$/.test(expectedDigest)
744
- || !plugins.crypto.timingSafeEqual(
745
- Buffer.from(expectedDigest, 'hex'),
746
- Buffer.from(actualDigest, 'hex'),
747
- )
748
- ) {
749
- throw new Error(`Invalid claim for transactional generation "${transaction.generationId}".`);
750
- }
751
- return state;
752
- }
753
-
754
- private async interruptRestoredTransactions(): Promise<void> {
755
- const openTransactions = getOpenAgentGenerationTransactions(this.events);
756
- if (openTransactions.length === 0) return;
757
- if (openTransactions.some((transaction) => transaction.state === 'invalid')) {
758
- throw new Error('Cannot restore a session with an invalid transactional generation sequence.');
759
- }
760
- this.commitEvents(openTransactions.map((transaction) =>
761
- createAgentEvent<IGenerationOutcomeEvent>({
762
- type: 'generation-outcome',
763
- generationId: transaction.generationId,
764
- outcome: 'interrupted',
765
- reason: 'Session restoration interrupted nonterminal transactional execution.',
766
- parentIds: [
767
- transaction.executionCompleted?.id
768
- ?? transaction.executionStarted?.id
769
- ?? transaction.begun.id,
770
- ],
771
- })));
772
- await this.flushPersistence();
773
- }
774
-
775
- private transactionalStoreCapabilityError(): Error | undefined {
776
- return this.options.eventStore && !isAgentEventStoreV2(this.options.eventStore)
777
- ? new Error('Transactional generations require an IAgentEventStoreV2-capable event store.')
778
- : undefined;
779
- }
780
-
781
- public beginGeneration(
782
- content: TAgentPrompt,
783
- options: IAgentBeginGenerationOptions = {},
784
- ): Promise<IAgentGenerationHandle> {
785
- const capabilityError = this.transactionalStoreCapabilityError();
786
- if (capabilityError) return Promise.reject(capabilityError);
787
- return this.queueExclusive(async () => {
788
- await this.flushPersistence();
789
- const generationId = options.generationId ?? createAgentEventId('generation');
790
- if (!generationId.trim()) throw new Error('Transactional generationId must not be empty.');
791
- const existingTransaction = getAgentGenerationTransactions(this.events).get(generationId);
792
- if (existingTransaction) {
793
- if (existingTransaction.state === 'invalid') {
794
- throw new Error(`Transactional generation "${generationId}" has an invalid event sequence.`);
795
- }
796
- const existingMessage = this.events.find((event): event is IUserMessageEvent =>
797
- event.type === 'user-message'
798
- && event.generationId === generationId
799
- && event.causationId === existingTransaction.begun.id);
800
- const handle = this.generationHandles.get(generationId);
801
- if (handle && existingMessage && plugins.isDeepStrictEqual(existingMessage.content, content)) {
802
- return handle;
803
- }
804
- throw new Error(
805
- `Transactional generation "${generationId}" already exists and its claim cannot be reissued.`,
806
- );
807
- }
808
- if (this.events.some((event) => event.generationId === generationId)) {
809
- throw new Error(`Generation id "${generationId}" is already correlated with legacy events.`);
810
- }
811
- if (this.archivedTransaction(generationId) || this.generationHandles.has(generationId)) {
812
- throw new Error(
813
- `Transactional generation "${generationId}" already exists and its claim cannot be reissued.`,
814
- );
815
- }
816
- const claimToken = plugins.crypto.randomBytes(32).toString('base64url');
817
- const claimTokenDigest = plugins.crypto
818
- .createHash('sha256')
819
- .update(claimToken)
820
- .digest('hex');
821
- const priorEvent = this.events[this.events.length - 1];
822
- const begun = createAgentEvent<IGenerationBegunEvent>({
823
- type: 'generation-begun',
824
- generationId,
825
- claimTokenDigest,
826
- parentIds: priorEvent ? [priorEvent.id] : undefined,
827
- });
828
- const userMessage = createAgentEvent<IUserMessageEvent>({
829
- type: 'user-message',
830
- content,
831
- generationId,
832
- causationId: begun.id,
833
- parentIds: [begun.id],
834
- });
835
- this.commitEvents([begun, userMessage]);
836
- const handle = Object.freeze({ generationId, claimToken });
837
- this.generationHandles.set(generationId, handle);
838
- await this.flushPersistence();
839
- this.scheduleEventRetention();
840
- return handle;
841
- });
842
- }
843
-
844
- private async appendGenerationOutcome(
845
- generationId: string,
846
- outcome: TAgentGenerationOutcome,
847
- reason?: string,
848
- ): Promise<void> {
849
- const transaction = this.transaction(generationId);
850
- if (
851
- transaction.state === 'accepted'
852
- || transaction.state === 'rejected'
853
- || transaction.state === 'interrupted'
854
- ) {
855
- if (transaction.state === outcome) return;
856
- throw new Error(
857
- `Transactional generation "${generationId}" already has outcome "${transaction.state}".`,
858
- );
859
- }
860
- if (outcome !== 'interrupted') {
861
- if (transaction.state !== 'execution-completed') {
862
- throw new Error(
863
- `Transactional generation "${generationId}" must be execution-completed before ${outcome}.`,
864
- );
865
- }
866
- if (getUncertainToolExecutionIntents(this.events, generationId).length > 0) {
867
- throw new Error(
868
- `Transactional generation "${generationId}" has unresolved tool execution intents.`,
869
- );
870
- }
871
- }
872
- const parent = transaction.executionCompleted
873
- ?? transaction.executionStarted
874
- ?? transaction.begun;
875
- this.commitEvent(createAgentEvent<IGenerationOutcomeEvent>({
876
- type: 'generation-outcome',
877
- generationId,
878
- outcome,
879
- reason,
880
- parentIds: [parent.id],
881
- }));
882
- await this.flushPersistence();
883
- this.scheduleEventRetention();
884
- }
885
-
886
- private transactionOutcomeErrorReason(error: unknown): string {
887
- if (this.options.transactionOutcomeErrorProjector) {
888
- try {
889
- const projected = this.options.transactionOutcomeErrorProjector(error);
890
- if (typeof projected === 'string' && projected.trim()) return projected;
891
- } catch {
892
- // Invalid projectors fall back to the normal diagnostic reason.
893
- }
894
- }
895
- return errorToString(error);
896
- }
897
-
898
- public finalizeGeneration(
899
- transaction: IAgentGenerationHandle,
900
- outcome: TAgentGenerationOutcome,
901
- ): Promise<void> {
902
- const capabilityError = this.transactionalStoreCapabilityError();
903
- if (capabilityError) return Promise.reject(capabilityError);
904
- return this.queueExclusive(async () => {
905
- if (!['accepted', 'rejected', 'interrupted'].includes(outcome)) {
906
- throw new Error(`Invalid transactional generation outcome "${String(outcome)}".`);
907
- }
908
- const state = this.verifyGenerationClaim(transaction);
909
- if (!('begun' in state)) {
910
- if (state.outcome === outcome) return;
911
- throw new Error(
912
- `Transactional generation "${transaction.generationId}" already has outcome "${state.outcome}".`,
913
- );
914
- }
915
- await this.appendGenerationOutcome(transaction.generationId, outcome);
916
- });
917
- }
918
-
919
- public listUncertainToolExecutions(): IToolExecutionIntentEvent[] {
920
- return structuredClone(getUncertainToolExecutionIntents(this.events));
921
- }
922
-
923
- public reconcileToolExecution(
924
- intentId: string,
925
- reconciliation: TAgentToolExecutionReconciliationOptions,
926
- ): Promise<void> {
927
- const capabilityError = this.transactionalStoreCapabilityError();
928
- if (capabilityError) return Promise.reject(capabilityError);
929
- return this.queueExclusive(async () => {
930
- await this.flushPersistence();
931
- const intent = this.events.find((event): event is IToolExecutionIntentEvent =>
932
- event.type === 'tool-execution-intent' && event.id === intentId);
933
- if (!intent) throw new Error(`Unknown tool execution intent "${intentId}".`);
934
- const defaultError = reconciliation.resolution === 'not-executed'
935
- ? 'Tool execution was reconciled as not executed.'
936
- : reconciliation.resolution === 'abandoned-unknown'
937
- ? 'Tool execution was abandoned with an unknown outcome.'
938
- : undefined;
939
- const error = reconciliation.resolution === 'executed'
940
- ? undefined
941
- : reconciliation.error ?? defaultError;
942
- const existing = this.events.find((event): event is IToolExecutionReconciliationEvent =>
943
- event.type === 'tool-execution-reconciliation' && event.intentId === intentId);
944
- if (existing) {
945
- const same = existing.resolution === reconciliation.resolution
946
- && existing.error === error
947
- && plugins.isDeepStrictEqual(
948
- existing.output,
949
- reconciliation.resolution === 'executed' ? reconciliation.output : undefined,
950
- )
951
- && plugins.isDeepStrictEqual(
952
- existing.modelOutput,
953
- reconciliation.resolution === 'executed' ? reconciliation.modelOutput : undefined,
954
- );
955
- if (!same) throw new Error(`Tool execution intent "${intentId}" has a conflicting reconciliation.`);
956
- return;
957
- }
958
- if (!getUncertainToolExecutionIntents(this.events).some((entry) => entry.id === intentId)) {
959
- throw new Error(`Tool execution intent "${intentId}" already has a terminal result.`);
960
- }
961
- if (
962
- reconciliation.resolution === 'executed'
963
- && (!Object.prototype.hasOwnProperty.call(reconciliation, 'output') || !reconciliation.modelOutput)
964
- ) {
965
- throw new Error('Executed tool reconciliation requires explicit output and modelOutput.');
966
- }
967
- const toolCall = [...this.events].reverse().find((event): event is IToolCallEvent =>
968
- event.type === 'tool-call'
969
- && event.generationId === intent.generationId
970
- && event.toolCallId === intent.toolCallId);
971
- const reconciliationEvent = createAgentEvent<IToolExecutionReconciliationEvent>({
972
- type: 'tool-execution-reconciliation',
973
- generationId: intent.generationId,
974
- inferenceId: intent.inferenceId,
975
- causationId: intent.id,
976
- intentId: intent.id,
977
- toolCallId: intent.toolCallId,
978
- toolName: intent.toolName,
979
- resolution: reconciliation.resolution,
980
- output: reconciliation.resolution === 'executed' ? reconciliation.output : undefined,
981
- modelOutput: reconciliation.resolution === 'executed' ? reconciliation.modelOutput : undefined,
982
- error,
983
- parentIds: [intent.id],
984
- });
985
- const resultEvent = createAgentEvent<IToolResultEvent>({
986
- type: 'tool-result',
987
- generationId: intent.generationId,
988
- inferenceId: intent.inferenceId,
989
- causationId: intent.inferenceId,
990
- executionIntentId: intent.id,
991
- toolCallId: intent.toolCallId,
992
- toolName: intent.toolName,
993
- output: reconciliation.resolution === 'executed' ? reconciliation.output : undefined,
994
- error,
995
- modelOutput: reconciliation.resolution === 'executed'
996
- ? reconciliation.modelOutput
997
- : { type: 'error-text', value: error! },
998
- parentIds: toolCall ? [toolCall.id] : [intent.id],
999
- });
1000
- this.commitEvents([reconciliationEvent, resultEvent]);
1001
- await this.flushPersistence();
1002
- this.scheduleEventRetention();
1003
- });
1004
- }
1005
-
1006
- public getEvents(): readonly TAgentEvent[] {
1007
- return this.events.slice();
1008
- }
1009
-
1010
- public getModelMessages(): plugins.ModelMessage[] {
1011
- return this.buildContext(this.events);
1012
- }
1013
-
1014
- private buildContext(events: readonly TAgentEvent[]): plugins.ModelMessage[] {
1015
- const visibleEvents = filterModelVisibleAgentEvents(events);
1016
- return this.options.contextBuilder
1017
- ? this.options.contextBuilder({ events: visibleEvents })
1018
- : buildModelMessages(visibleEvents);
1019
- }
1020
-
1021
- public subscribe(listener: TAgentSessionChangeListener): () => void {
1022
- if (this.closed) throw new Error('AgentSession is closed.');
1023
- const existing = this.listeners.get(listener);
1024
- if (existing) {
1025
- return () => {
1026
- existing.subscribed = false;
1027
- existing.queue.length = 0;
1028
- this.listeners.delete(listener);
1029
- };
1030
- }
1031
- const state: IAgentSessionListenerState = {
1032
- listener,
1033
- queue: [],
1034
- executing: false,
1035
- subscribed: true,
1036
- };
1037
- this.listeners.set(listener, state);
1038
- return () => {
1039
- state.subscribed = false;
1040
- state.queue.length = 0;
1041
- this.listeners.delete(listener);
1042
- };
1043
- }
1044
-
1045
- public scheduleGenerate(options: IAgentScheduleGenerateOptions): Promise<IAgentGenerateResult> {
1046
- if (this.closed) return Promise.reject(new Error('AgentSession is closed.'));
1047
- const capabilityError = options.transaction
1048
- ? this.transactionalStoreCapabilityError()
1049
- : undefined;
1050
- if (capabilityError) return Promise.reject(capabilityError);
1051
- if (!options.key.trim()) return Promise.reject(new Error('Scheduled generation key must not be empty.'));
1052
- const debounceMs = options.debounceMs ?? 50;
1053
- if (!Number.isSafeInteger(debounceMs) || debounceMs < 0) {
1054
- return Promise.reject(new Error('debounceMs must be a non-negative integer.'));
1055
- }
1056
- const existing = this.scheduledGenerations.get(options.key);
1057
- if (existing) {
1058
- if (
1059
- existing.model !== options.model
1060
- || existing.system !== options.system
1061
- || existing.tools !== options.tools
1062
- || existing.providerOptions !== options.providerOptions
1063
- || existing.cache !== options.cache
1064
- || existing.maxSteps !== options.maxSteps
1065
- || existing.abort !== options.abort
1066
- || existing.prepare !== options.prepare
1067
- || existing.transaction?.generationId !== options.transaction?.generationId
1068
- || existing.transaction?.claimToken !== options.transaction?.claimToken
1069
- ) {
1070
- return Promise.reject(new Error(`Conflicting options for scheduled generation "${options.key}".`));
1071
- }
1072
- if (existing.state === 'debouncing') {
1073
- clearTimeout(existing.timer);
1074
- existing.debounceMs = debounceMs;
1075
- existing.timer = setTimeout(() => this.startScheduledGeneration(options.key), debounceMs);
1076
- }
1077
- return existing.promise;
1078
- }
1079
-
1080
- if (options.abort?.aborted) {
1081
- const abortReason = options.abort.reason ?? new DOMException('Aborted', 'AbortError');
1082
- if (!options.transaction) return Promise.reject(abortReason);
1083
- return this.queueExclusive(async () => {
1084
- const transaction = this.verifyGenerationClaim(options.transaction!);
1085
- if (!('begun' in transaction)) {
1086
- if (transaction.outcome === 'interrupted') throw abortReason;
1087
- throw new Error(
1088
- `Transactional generation "${transaction.generationId}" already has outcome "${transaction.outcome}".`,
1089
- );
1090
- }
1091
- await this.appendGenerationOutcome(
1092
- options.transaction!.generationId,
1093
- 'interrupted',
1094
- this.transactionOutcomeErrorReason(abortReason),
1095
- );
1096
- throw abortReason;
1097
- });
1098
- }
1099
-
1100
- let resolve!: (result: IAgentGenerateResult) => void;
1101
- let reject!: (error: unknown) => void;
1102
- const promise = new Promise<IAgentGenerateResult>((resolvePromise, rejectPromise) => {
1103
- resolve = resolvePromise;
1104
- reject = rejectPromise;
1105
- });
1106
- const controller = new AbortController();
1107
- const onAbort = () => this.cancelScheduledGeneration(
1108
- options.key,
1109
- options.abort?.reason ?? new DOMException('Aborted', 'AbortError'),
1110
- );
1111
- options.abort?.addEventListener('abort', onAbort, { once: true });
1112
- const scheduled: IScheduledGeneration = {
1113
- state: 'debouncing',
1114
- debounceMs,
1115
- model: options.model,
1116
- system: options.system,
1117
- tools: options.tools,
1118
- providerOptions: options.providerOptions,
1119
- cache: options.cache,
1120
- maxSteps: options.maxSteps,
1121
- abort: options.abort,
1122
- transaction: options.transaction,
1123
- prepare: options.prepare,
1124
- timer: setTimeout(() => this.startScheduledGeneration(options.key), debounceMs),
1125
- controller,
1126
- removeAbortListener: () => options.abort?.removeEventListener('abort', onAbort),
1127
- promise,
1128
- resolve,
1129
- reject,
1130
- };
1131
- this.scheduledGenerations.set(options.key, scheduled);
1132
- return promise;
1133
- }
1134
-
1135
- private startScheduledGeneration(key: string): void {
1136
- const scheduled = this.scheduledGenerations.get(key);
1137
- if (!scheduled || scheduled.state !== 'debouncing') return;
1138
- scheduled.state = 'queued';
1139
- void this.generate({
1140
- model: scheduled.model,
1141
- system: scheduled.system,
1142
- tools: scheduled.tools,
1143
- providerOptions: scheduled.providerOptions,
1144
- cache: scheduled.cache,
1145
- maxSteps: scheduled.maxSteps,
1146
- abort: scheduled.controller.signal,
1147
- transaction: scheduled.transaction,
1148
- prepare: scheduled.prepare,
1149
- }).then(
1150
- (result) => this.settleScheduledGeneration(key, scheduled, () => scheduled.resolve(result)),
1151
- (error) => this.settleScheduledGeneration(key, scheduled, () => scheduled.reject(error)),
1152
- );
1153
- }
1154
-
1155
- private settleScheduledGeneration(
1156
- key: string,
1157
- scheduled: IScheduledGeneration,
1158
- settle: () => void,
1159
- ): void {
1160
- if (this.scheduledGenerations.get(key) === scheduled) {
1161
- this.scheduledGenerations.delete(key);
1162
- }
1163
- scheduled.removeAbortListener();
1164
- settle();
1165
- }
1166
-
1167
- public cancelScheduledGeneration(key: string, reason?: unknown): boolean {
1168
- const scheduled = this.scheduledGenerations.get(key);
1169
- if (!scheduled) return false;
1170
- clearTimeout(scheduled.timer);
1171
- this.scheduledGenerations.delete(key);
1172
- scheduled.removeAbortListener();
1173
- const cancellationReason = reason
1174
- ?? new DOMException('Scheduled generation cancelled', 'AbortError');
1175
- if (!scheduled.controller.signal.aborted) scheduled.controller.abort(cancellationReason);
1176
- if (!scheduled.transaction) {
1177
- scheduled.reject(cancellationReason);
1178
- return true;
1179
- }
1180
- let transaction: IAgentGenerationTransaction;
1181
- try {
1182
- const claim = this.verifyGenerationClaim(scheduled.transaction);
1183
- if (!('begun' in claim)) {
1184
- scheduled.reject(new Error(
1185
- `Transactional generation "${claim.generationId}" already has outcome "${claim.outcome}".`,
1186
- ));
1187
- return true;
1188
- }
1189
- transaction = claim;
1190
- } catch (error) {
1191
- scheduled.reject(error);
1192
- return true;
1193
- }
1194
- if (transaction.state === 'execution-started') return true;
1195
- void this.appendGenerationOutcome(
1196
- scheduled.transaction.generationId,
1197
- 'interrupted',
1198
- this.transactionOutcomeErrorReason(cancellationReason),
1199
- ).then(
1200
- () => scheduled.reject(cancellationReason),
1201
- (error) => scheduled.reject(error),
1202
- );
1203
- return true;
1204
- }
1205
-
1206
- private async performCompaction(
1207
- reason: 'context-overflow' | 'retention' | 'manual',
1208
- abortSignal?: AbortSignal,
1209
- ): Promise<IContextCompactionEvent | undefined> {
1210
- abortSignal?.throwIfAborted();
1211
- await this.flushPersistence();
1212
- abortSignal?.throwIfAborted();
1213
- const unresolvedIntents = getUncertainToolExecutionIntents(this.events);
1214
- if (unresolvedIntents.length > 0) {
1215
- if (reason === 'context-overflow' || reason === 'retention') return undefined;
1216
- throw new Error('Context compaction is blocked by unresolved tool execution intents.');
1217
- }
1218
- const openTransactions = getOpenAgentGenerationTransactions(this.events);
1219
- if (openTransactions.some((transaction) => transaction.state === 'invalid')) {
1220
- throw new Error('Context compaction is blocked by an invalid transactional generation sequence.');
1221
- }
1222
- let coveredEvents: TAgentEvent[];
1223
- if (openTransactions.length > 0) {
1224
- if (reason === 'retention') return undefined;
1225
- if (reason !== 'context-overflow') {
1226
- throw new Error('Context compaction is blocked while a transactional generation is open.');
1227
- }
1228
- const openGenerationIds = new Set(
1229
- openTransactions.map((transaction) => transaction.generationId),
1230
- );
1231
- const firstOpenIndex = this.events.findIndex((event) =>
1232
- !!event.generationId && openGenerationIds.has(event.generationId));
1233
- coveredEvents = firstOpenIndex < 0 ? [] : this.events.slice(0, firstOpenIndex);
1234
- } else {
1235
- coveredEvents = this.events.slice();
1236
- }
1237
- if (coveredEvents.length === 0) return undefined;
1238
- const visibleCoveredEvents = filterModelVisibleAgentEvents(coveredEvents);
1239
- const messages = this.buildContext(visibleCoveredEvents);
1240
- const replacementMessages = this.options.contextCompactor
1241
- ? await this.invokeGenerationCallback(
1242
- () => this.options.contextCompactor!(messages, visibleCoveredEvents, { abortSignal, reason }),
1243
- )
1244
- : reason === 'context-overflow' && this.options.onContextOverflow
1245
- ? await this.invokeGenerationCallback(
1246
- () => this.options.onContextOverflow!(messages, { abortSignal }),
1247
- )
1248
- : undefined;
1249
- if (!replacementMessages) {
1250
- throw new ContextOverflowError('Agent context compaction requires a contextCompactor.');
1251
- }
1252
- abortSignal?.throwIfAborted();
1253
- const archivedTransactionMap = new Map<string, IArchivedAgentGenerationTransaction>();
1254
- const rememberArchivedTransaction = (transaction: IArchivedAgentGenerationTransaction): void => {
1255
- archivedTransactionMap.delete(transaction.generationId);
1256
- archivedTransactionMap.set(transaction.generationId, transaction);
1257
- };
1258
- for (const event of coveredEvents) {
1259
- if (event.type !== 'context-compaction') continue;
1260
- for (const transaction of event.archivedTransactions ?? []) {
1261
- rememberArchivedTransaction(transaction);
1262
- }
1263
- }
1264
- const coveredTransactions = getAgentGenerationTransactions(coveredEvents);
1265
- for (const event of coveredEvents) {
1266
- if (event.type !== 'generation-outcome') continue;
1267
- const transaction = coveredTransactions.get(event.generationId);
1268
- if (!transaction || transaction.state === 'invalid') continue;
1269
- rememberArchivedTransaction({
1270
- generationId: transaction.generationId,
1271
- claimTokenDigest: transaction.begun.claimTokenDigest,
1272
- outcome: event.outcome,
1273
- });
1274
- }
1275
- const archivedTransactions = [...archivedTransactionMap.values()]
1276
- .slice(-(this.options.maxArchivedTransactionTombstones ?? 1000));
1277
- const compaction = this.commitEvent(createAgentEvent<IContextCompactionEvent>({
1278
- type: 'context-compaction',
1279
- coveredEventIds: coveredEvents.map((event) => event.id),
1280
- replacementEvents: modelMessagesToAgentEvents(replacementMessages),
1281
- archivedTransactions: archivedTransactions.length > 0
1282
- ? archivedTransactions
1283
- : undefined,
1284
- parentIds: [coveredEvents[coveredEvents.length - 1].id],
1285
- }));
1286
- await this.flushPersistence();
1287
- return compaction;
1288
- }
1289
-
1290
- private queueExclusive<TValue>(operation: () => Promise<TValue>): Promise<TValue> {
1291
- const run = this.generationQueue.then(() => {
1292
- if (this.closed) throw new Error('AgentSession is closed.');
1293
- if (this.maintenanceError !== undefined) throw this.maintenanceError;
1294
- return operation();
1295
- });
1296
- this.generationQueue = run.then(() => undefined, () => undefined);
1297
- return run;
1298
- }
1299
-
1300
- public compact(options: IAgentCompactOptions = {}): Promise<void> {
1301
- const combinedAbort = combineAbortSignals([options.abort, this.sessionAbortController.signal]);
1302
- const operation = this.queueExclusive(async () => {
1303
- combinedAbort.signal.throwIfAborted();
1304
- await this.performCompaction(options.reason ?? 'manual', combinedAbort.signal);
1305
- }).finally(combinedAbort.cleanup);
1306
- return waitWithAbort(operation, [options.abort, this.sessionAbortController.signal]);
1307
- }
1308
-
1309
- public archiveCompactedEvents(
1310
- compactionEventId?: string,
1311
- ): Promise<TAgentEventArchive | undefined> {
1312
- return this.queueExclusive(() => this.archiveCompactedEventsLocked(compactionEventId));
1313
- }
1314
-
1315
- private async archiveCompactedEventsLocked(
1316
- compactionEventId?: string,
1317
- ): Promise<TAgentEventArchive | undefined> {
1318
- if (getOpenAgentGenerationTransactions(this.events).length > 0) {
1319
- throw new Error('Archival is blocked while a transactional generation is open.');
1320
- }
1321
- if (getUncertainToolExecutionIntents(this.events).length > 0) {
1322
- throw new Error('Archival is blocked by unresolved tool execution intents.');
1323
- }
1324
- const eventStore = this.options.eventStore;
1325
- if (!eventStore?.archive) {
1326
- throw new Error('Archival requires an eventStore with archive support.');
1327
- }
1328
- const compaction = compactionEventId
1329
- ? this.events.find((event) => event.id === compactionEventId)
1330
- : [...this.events].reverse().find((event) => event.type === 'context-compaction');
1331
- if (!compaction || compaction.type !== 'context-compaction') return undefined;
1332
- const coveredIds = new Set(compaction.coveredEventIds);
1333
- const archivedEvents = this.events.filter((event) => coveredIds.has(event.id));
1334
- if (archivedEvents.length === 0) {
1335
- return isAgentEventStoreV2(eventStore)
1336
- ? eventStore.loadArchive?.(this.options.sessionId!, compaction.id)
1337
- : eventStore.loadArchive?.(this.options.sessionId!, compaction.id);
1338
- }
1339
- const archiveBase = {
1340
- sessionId: this.options.sessionId!,
1341
- archiveId: compaction.id,
1342
- createdAt: Date.now(),
1343
- events: structuredClone(archivedEvents),
1344
- };
1345
- const archive: TAgentEventArchive = isAgentEventStoreV2(eventStore)
1346
- ? { ...archiveBase, schemaVersion: 2 }
1347
- : { ...archiveBase, schemaVersion: 1 };
1348
- await this.flushPersistence();
1349
- if (isAgentEventStoreV2(eventStore)) {
1350
- await eventStore.archive?.(archive as IAgentEventArchiveV2);
1351
- } else {
1352
- await eventStore.archive?.(archive as IAgentEventArchive);
1353
- }
1354
- this.events.splice(0, this.events.length, ...this.events.filter((event) => !coveredIds.has(event.id)));
1355
- for (const event of archivedEvents) this.eventIds.delete(event.id);
1356
- const durability = this.queuePersistence();
1357
- this.queueNotification({ type: 'archived', events: archivedEvents }, durability);
1358
- await durability;
1359
- return archive;
1360
- }
1361
-
1362
- private async runEventRetentionLocked(): Promise<void> {
1363
- const retention = this.options.eventRetention;
1364
- if (!retention) return;
1365
- if (
1366
- getOpenAgentGenerationTransactions(this.events).length > 0
1367
- || getUncertainToolExecutionIntents(this.events).length > 0
1368
- ) return;
1369
- while (this.events.length > retention.maxEvents) {
1370
- const compaction = await this.performCompaction(
1371
- 'retention',
1372
- this.sessionAbortController.signal,
1373
- );
1374
- if (!compaction) return;
1375
- await this.archiveCompactedEventsLocked(compaction.id);
1376
- }
1377
- }
1378
-
1379
- private scheduleEventRetention(): void {
1380
- const retention = this.options.eventRetention;
1381
- if (
1382
- !retention
1383
- || this.events.length <= retention.maxEvents
1384
- || this.retentionPromise
1385
- || this.closed
1386
- ) return;
1387
- const operation = this.generationQueue.then(() => {
1388
- if (this.maintenanceError !== undefined) throw this.maintenanceError;
1389
- return this.runEventRetentionLocked();
1390
- });
1391
- this.generationQueue = operation.then(() => undefined, () => undefined);
1392
- this.retentionPromise = operation;
1393
- void operation.then(
1394
- () => {
1395
- if (this.retentionPromise === operation) this.retentionPromise = undefined;
1396
- },
1397
- (error) => {
1398
- const expectedCloseAbort = this.closed
1399
- && (error === this.sessionAbortController.signal.reason
1400
- || (error instanceof DOMException && error.name === 'AbortError'));
1401
- if (!expectedCloseAbort) this.maintenanceError ??= error;
1402
- if (this.retentionPromise === operation) this.retentionPromise = undefined;
1403
- },
1404
- );
1405
- }
1406
-
1407
- private eventsByIds(ids: ReadonlySet<string>): TAgentEvent[] {
1408
- return this.events.filter((event) => ids.has(event.id));
1409
- }
1410
-
1411
- private currentGenerationEvent(
1412
- generationId: string,
1413
- type: 'tool-call' | 'tool-result',
1414
- toolCallId: string,
1415
- inferenceId?: string,
1416
- ): IToolCallEvent | IToolResultEvent | undefined {
1417
- return this.events.find((event) =>
1418
- event.generationId === generationId
1419
- && event.type === type
1420
- && event.toolCallId === toolCallId
1421
- && (inferenceId === undefined || event.inferenceId === inferenceId),
1422
- ) as IToolCallEvent | IToolResultEvent | undefined;
1423
- }
1424
-
1425
- private reconcileToolResult(
1426
- generationId: string,
1427
- modelEvent: IToolResultEvent,
1428
- ): void {
1429
- const existingIndex = this.events.findIndex((event) =>
1430
- event.generationId === generationId
1431
- && event.type === 'tool-result'
1432
- && event.toolCallId === modelEvent.toolCallId
1433
- && event.inferenceId === modelEvent.inferenceId,
1434
- );
1435
- let provisionalIndex = existingIndex;
1436
- if (provisionalIndex === -1) {
1437
- for (let index = this.events.length - 1; index >= 0; index--) {
1438
- const event = this.events[index];
1439
- if (
1440
- event.generationId === generationId
1441
- && event.type === 'tool-result'
1442
- && event.toolCallId === modelEvent.toolCallId
1443
- && event.responseMessageCommitted !== true
1444
- ) {
1445
- provisionalIndex = index;
1446
- break;
1447
- }
1448
- }
1449
- }
1450
- if (provisionalIndex === -1) {
1451
- this.commitEvent(modelEvent);
1452
- return;
1453
- }
1454
- const existing = this.events[provisionalIndex] as IToolResultEvent;
1455
- this.events[provisionalIndex] = {
1456
- ...existing,
1457
- modelOutput: modelEvent.modelOutput,
1458
- responseMessageCommitted: true,
1459
- providerOptions: modelEvent.providerOptions,
1460
- messageProviderOptions: modelEvent.messageProviderOptions,
1461
- };
1462
- const updatedEvent = this.events[provisionalIndex];
1463
- const durability = this.queuePersistence();
1464
- this.queueNotification({ type: 'updated', events: [updatedEvent] }, durability);
1465
- }
1466
-
1467
- private commitResponseMessages(
1468
- messages: readonly plugins.ModelMessage[],
1469
- generationId: string,
1470
- inferenceId: string,
1471
- executionIntentIds: ReadonlyMap<string, string>,
1472
- ): void {
1473
- const convertedEvents = modelMessagesToAgentEvents(messages, {
1474
- causationId: inferenceId,
1475
- generationId,
1476
- inferenceId,
1477
- });
1478
- for (const event of convertedEvents) {
1479
- if (event.type === 'tool-call') {
1480
- if (!this.currentGenerationEvent(
1481
- generationId,
1482
- 'tool-call',
1483
- event.toolCallId,
1484
- inferenceId,
1485
- )) {
1486
- this.commitEvent(event);
1487
- }
1488
- } else if (event.type === 'tool-result') {
1489
- const executionIntentId = event.executionIntentId
1490
- ?? executionIntentIds.get(event.toolCallId);
1491
- this.reconcileToolResult(
1492
- generationId,
1493
- executionIntentId ? { ...event, executionIntentId } : event,
1494
- );
1495
- } else {
1496
- this.commitEvent(event);
1497
- }
1498
- }
1499
- }
1500
-
1501
- private wrapToolsForGeneration(
1502
- generationId: string,
1503
- tools: plugins.ToolSet,
1504
- preparations: Map<string, IToolCallPreparationLedgerEntry>,
1505
- completedPreparations: ReadonlySet<string>,
1506
- preparationErrors: ReadonlyMap<string, unknown>,
1507
- ledger: Map<string, IToolExecutionLedgerEntry>,
1508
- getMessages: () => plugins.ModelMessage[],
1509
- ): plugins.ToolSet {
1510
- const wrappedTools: plugins.ToolSet = {};
1511
- for (const [toolName, toolDefinition] of Object.entries(tools)) {
1512
- const executableTool = toolDefinition as typeof toolDefinition & {
1513
- execute?: (input: unknown, options: plugins.ToolExecutionOptions<unknown>) => unknown;
1514
- };
1515
- if (!executableTool.execute) {
1516
- wrappedTools[toolName] = toolDefinition;
1517
- continue;
1518
- }
1519
- const originalExecute = executableTool.execute.bind(executableTool);
1520
- wrappedTools[toolName] = {
1521
- ...toolDefinition,
1522
- execute: (input: unknown, executionOptions: plugins.ToolExecutionOptions<unknown>) => {
1523
- const toolCallId = executionOptions.toolCallId;
1524
- const signature = toolSignature(toolName, input);
1525
- const preparation = preparations.get(toolCallId);
1526
- if (!preparation) {
1527
- throw new Error(
1528
- `Tool call "${toolCallId}" executed before preparation in generation "${generationId}".`,
1529
- );
1530
- }
1531
- if (preparation.signature !== signature) {
1532
- throw new Error(`Tool call id collision for "${toolCallId}" in generation "${generationId}".`);
1533
- }
1534
- if (!completedPreparations.has(toolCallId)) {
1535
- throw preparationErrors.get(toolCallId)
1536
- ?? new Error(
1537
- `Tool call "${toolCallId}" executed before preparation completed in generation "${generationId}".`,
1538
- );
1539
- }
1540
- const existing = ledger.get(toolCallId);
1541
- if (existing) {
1542
- if (existing.signature !== signature) {
1543
- throw new Error(`Tool call id collision for "${toolCallId}" in generation "${generationId}".`);
1544
- }
1545
- return existing.promise;
1546
- }
1547
- const contextualExecutionOptions = {
1548
- ...executionOptions,
1549
- messages: getMessages(),
1550
- };
1551
- let resolveCompletion!: (output: unknown) => void;
1552
- let rejectCompletion!: (error: unknown) => void;
1553
- const promise = new Promise<unknown>((resolve, reject) => {
1554
- resolveCompletion = resolve;
1555
- rejectCompletion = reject;
1556
- });
1557
- void promise.catch(() => undefined);
1558
- ledger.set(toolCallId, { signature, promise });
1559
- try {
1560
- const result = this.invokeGenerationCallback(
1561
- () => originalExecute(input, contextualExecutionOptions),
1562
- );
1563
- if (isAsyncIterable(result)) {
1564
- return this.trackToolExecutionIterable(
1565
- result,
1566
- resolveCompletion,
1567
- rejectCompletion,
1568
- );
1569
- }
1570
- if (isPromiseLike(result)) {
1571
- const deferredIterable = (async function* () {
1572
- const output = await result;
1573
- if (isAsyncIterable(output)) {
1574
- yield* output;
1575
- } else {
1576
- yield output;
1577
- }
1578
- })();
1579
- return this.trackToolExecutionIterable(
1580
- deferredIterable,
1581
- resolveCompletion,
1582
- rejectCompletion,
1583
- );
1584
- }
1585
- resolveCompletion(result);
1586
- return result;
1587
- } catch (error) {
1588
- rejectCompletion(error);
1589
- throw error;
1590
- }
1591
- },
1592
- } as typeof toolDefinition;
1593
- }
1594
- wrappedTools.invalid = plugins.tool({
1595
- description: 'Sink for unrecognised tool calls; returns an error message to the model',
1596
- inputSchema: plugins.z.object({ tool: plugins.z.string(), error: plugins.z.string() }),
1597
- execute: async ({ tool, error }: { tool: string; error: string }) =>
1598
- `Unknown tool "${tool}": ${error}`,
1599
- });
1600
- return wrappedTools;
1601
- }
1602
-
1603
- private trackToolExecutionIterable(
1604
- iterable: AsyncIterable<unknown>,
1605
- resolveCompletion: (output: unknown) => void,
1606
- rejectCompletion: (error: unknown) => void,
1607
- ): AsyncIterable<unknown> {
1608
- const iterator = iterable[Symbol.asyncIterator]();
1609
- let settled = false;
1610
- let lastOutput: unknown;
1611
- const settleSuccess = () => {
1612
- if (settled) return;
1613
- settled = true;
1614
- resolveCompletion(lastOutput);
1615
- };
1616
- const settleError = (error: unknown) => {
1617
- if (settled) return;
1618
- settled = true;
1619
- rejectCompletion(error);
1620
- };
1621
- const incompleteError = () => new Error(
1622
- 'Tool execution stream ended before normal completion.',
1623
- );
1624
- const observe = (
1625
- operation: () => Promise<IteratorResult<unknown>>,
1626
- cacheOutput: boolean,
1627
- rejectWhenDone = false,
1628
- ): Promise<IteratorResult<unknown>> => {
1629
- let operationResult: Promise<IteratorResult<unknown>>;
1630
- try {
1631
- operationResult = Promise.resolve(operation());
1632
- } catch (error) {
1633
- settleError(error);
1634
- return Promise.reject(error);
1635
- }
1636
- return operationResult.then(
1637
- (result) => {
1638
- if (!result.done && cacheOutput) lastOutput = result.value;
1639
- if (result.done) {
1640
- if (rejectWhenDone) settleError(incompleteError());
1641
- else settleSuccess();
1642
- }
1643
- return result;
1644
- },
1645
- (error) => {
1646
- settleError(error);
1647
- throw error;
1648
- },
1649
- );
1650
- };
1651
- const wrappedIterator: AsyncIterator<unknown, unknown, unknown> & AsyncIterable<unknown> = {
1652
- next: (...args: [] | [unknown]) => observe(() => iterator.next(...args), true),
1653
- return: iterator.return
1654
- ? (...args: [] | [unknown]) => {
1655
- let returnResult: Promise<IteratorResult<unknown>>;
1656
- try {
1657
- returnResult = Promise.resolve(iterator.return!(...args));
1658
- settleError(incompleteError());
1659
- } catch (error) {
1660
- settleError(error);
1661
- return Promise.reject(error);
1662
- }
1663
- return returnResult.then(
1664
- (result) => result,
1665
- (error) => {
1666
- settleError(error);
1667
- throw error;
1668
- },
1669
- );
1670
- }
1671
- : async (...args: [] | [unknown]) => {
1672
- settleError(incompleteError());
1673
- return { done: true, value: args[0] };
1674
- },
1675
- throw: iterator.throw
1676
- ? (error?: unknown) => observe(() => iterator.throw!(error), true, true)
1677
- : async (error?: unknown) => {
1678
- settleError(error);
1679
- throw error;
1680
- },
1681
- [Symbol.asyncIterator]() {
1682
- return this;
1683
- },
1684
- };
1685
- return wrappedIterator;
1686
- }
1687
-
1688
- private ownGenerationLeaseCleanup(
1689
- lease: IAgentGenerationLease | undefined,
1690
- ): IGenerationLeaseCleanupOwnership | undefined {
1691
- if (!lease?.close) return undefined;
1692
- const ownership: IGenerationLeaseCleanupOwnership = { lease, settled: false };
1693
- this.unresolvedLeaseCleanups.add(ownership);
1694
- return ownership;
1695
- }
1696
-
1697
- private async cleanupGenerationLease(
1698
- ownership: IGenerationLeaseCleanupOwnership,
1699
- ): Promise<void> {
1700
- if (ownership.settled) return;
1701
- const timeoutMs = this.options.generationLeaseCleanupTimeoutMs ?? 30_000;
1702
- const timeoutError = new Error(`Generation lease cleanup timed out after ${timeoutMs}ms.`);
1703
- let timer: NodeJS.Timeout | undefined;
1704
- const closePromise = Promise.resolve().then(() =>
1705
- this.invokeGenerationCallback(() => ownership.lease.close!()));
1706
- void closePromise.then(
1707
- () => {
1708
- ownership.settled = true;
1709
- this.unresolvedLeaseCleanups.delete(ownership);
1710
- this.refreshCloseCleanupCompleted();
1711
- },
1712
- () => undefined,
1713
- );
1714
- const timeoutPromise = new Promise<never>((_resolve, reject) => {
1715
- timer = setTimeout(() => reject(timeoutError), timeoutMs);
1716
- });
1717
- try {
1718
- await Promise.race([closePromise, timeoutPromise]);
1719
- ownership.settled = true;
1720
- this.unresolvedLeaseCleanups.delete(ownership);
1721
- } catch (error) {
1722
- throw new AgentGenerationLeaseCleanupError(
1723
- ownership.lease,
1724
- error,
1725
- () => this.cleanupGenerationLease(ownership),
1726
- );
1727
- } finally {
1728
- if (timer) clearTimeout(timer);
1729
- }
1730
- }
1731
-
1732
- public generate(options: IAgentGenerateOptions = {}): Promise<IAgentGenerateResult> {
1733
- const capabilityError = options.transaction
1734
- ? this.transactionalStoreCapabilityError()
1735
- : undefined;
1736
- if (capabilityError) return Promise.reject(capabilityError);
1737
- const run = this.generationQueue.then(async () => {
1738
- if (!options.transaction) options.abort?.throwIfAborted();
1739
- if (!options.transaction && this.sessionAbortController.signal.aborted) {
1740
- throw this.sessionAbortController.signal.reason
1741
- ?? new DOMException('Agent session aborted', 'AbortError');
1742
- }
1743
- if (this.closed) throw new Error('AgentSession is closed.');
1744
- if (this.maintenanceError !== undefined) throw this.maintenanceError;
1745
- await this.runEventRetentionLocked();
1746
- return this.generateLocked(options);
1747
- });
1748
- this.generationQueue = run.then(() => undefined, () => undefined);
1749
- return options.transaction
1750
- ? run
1751
- : waitWithAbort(run, [options.abort, this.sessionAbortController.signal]);
1752
- }
1753
-
1754
- private resolveEffectiveGenerationRuntime(
1755
- options: IAgentGenerateOptions,
1756
- lease: IAgentGenerationLease | undefined,
1757
- ): IEffectiveGenerationRuntime {
1758
- const model = lease?.model ?? options.model ?? this.options.model;
1759
- if (!model) {
1760
- throw new Error(
1761
- 'Agent generation requires a model from its prepared lease, generate options, or session defaults.',
1762
- );
1763
- }
1764
- const system = lease?.system ?? options.system ?? this.options.system;
1765
- const tools = lease?.tools ?? options.tools ?? this.options.tools ?? {};
1766
- if (Object.prototype.hasOwnProperty.call(tools, 'invalid')) {
1767
- throw new Error('Tool name "invalid" is reserved for SmartAgent tool-call repair.');
1768
- }
1769
- const providerOptions = lease?.providerOptions
1770
- ?? options.providerOptions
1771
- ?? this.options.providerOptions;
1772
- const cache = lease?.cache ?? options.cache ?? this.options.cache ?? 'auto';
1773
- const maxSteps = lease?.maxSteps ?? options.maxSteps ?? this.options.maxSteps ?? 20;
1774
- if (!Number.isSafeInteger(maxSteps) || maxSteps <= 0) {
1775
- throw new Error('maxSteps must be a positive integer.');
1776
- }
1777
- const configuredCacheProvider = typeof cache === 'object' ? cache.provider : undefined;
1778
- const messageCacheProvider = cache === false
1779
- ? undefined
1780
- : configuredCacheProvider
1781
- ?? plugins.resolveSmartAiCacheProvider(model.provider, model.modelId);
1782
- const effectiveModel = messageCacheProvider
1783
- ? plugins.wrapLanguageModel({
1784
- model,
1785
- middleware: plugins.createSmartAiCachingMiddleware({
1786
- ...(typeof cache === 'object' ? cache : {}),
1787
- provider: messageCacheProvider,
1788
- }),
1789
- })
1790
- : model;
1791
- return {
1792
- model: effectiveModel,
1793
- system,
1794
- tools,
1795
- providerOptions: plugins.applySmartAiCacheProviderOptions({
1796
- provider: model.provider,
1797
- modelId: model.modelId,
1798
- providerOptions,
1799
- cache,
1800
- sessionId: this.options.sessionId,
1801
- }),
1802
- maxSteps,
1803
- };
1804
- }
1805
-
1806
- private async generateLocked(options: IAgentGenerateOptions): Promise<IAgentGenerateResult> {
1807
- await this.flushPersistence();
1808
- const maxContextOverflowRetries = this.options.maxContextOverflowRetries ?? 3;
1809
- const generationId = options.transaction?.generationId ?? createAgentEventId('generation');
1810
- const generationAbortController = new AbortController();
1811
- this.currentGenerationAbortController = generationAbortController;
1812
- const combinedAbort = combineAbortSignals([
1813
- this.sessionAbortController.signal,
1814
- generationAbortController.signal,
1815
- options.abort,
1816
- ]);
1817
-
1818
- let lease: IAgentGenerationLease | undefined;
1819
- let leaseCleanupOwnership: IGenerationLeaseCleanupOwnership | undefined;
1820
- let operationError: unknown;
1821
- let completedResult: IAgentGenerateResult | undefined;
1822
- let transactionExecutionStarted = false;
1823
- try {
1824
- if (options.transaction) {
1825
- const transaction = this.verifyGenerationClaim(options.transaction);
1826
- if (!('begun' in transaction) || transaction.state !== 'begun') {
1827
- const transactionState = 'begun' in transaction
1828
- ? transaction.state
1829
- : transaction.outcome;
1830
- throw new Error(
1831
- `Transactional generation "${generationId}" cannot execute from state "${transactionState}".`,
1832
- );
1833
- }
1834
- this.commitEvent(createAgentEvent<IGenerationExecutionStartedEvent>({
1835
- type: 'generation-execution-started',
1836
- generationId,
1837
- parentIds: [transaction.begun.id],
1838
- }));
1839
- await this.flushPersistence();
1840
- transactionExecutionStarted = true;
1841
- }
1842
- combinedAbort.signal.throwIfAborted();
1843
- lease = options.prepare
1844
- ? await this.invokeGenerationCallback(
1845
- () => options.prepare!({ generationId, abortSignal: combinedAbort.signal }),
1846
- )
1847
- : undefined;
1848
- leaseCleanupOwnership = this.ownGenerationLeaseCleanup(lease);
1849
- combinedAbort.signal.throwIfAborted();
1850
- const runtime = this.resolveEffectiveGenerationRuntime(options, lease);
1851
- const maxSteps = runtime.maxSteps;
1852
-
1853
- let stepCount = 0;
1854
- let attempt = 0;
1855
- let contextOverflowRetries = 0;
1856
- let totalInput = 0;
1857
- let totalOutput = 0;
1858
- let totalCacheRead = 0;
1859
- let totalCacheWrite = 0;
1860
- let currentInferenceId: string | undefined;
1861
- let latestInferenceVisibleIds = new Set(this.events.map((event) => event.id));
1862
- const completedInferenceIds = new Set<string>();
1863
- const reasoningTextById = new Map<string, string>();
1864
- const toolCalls: IAgentToolCallRecord[] = [];
1865
- const toolCallIndexes = new Map<string, number>();
1866
- const preparations = new Map<string, IToolCallPreparationLedgerEntry>();
1867
- const completedPreparations = new Set<string>();
1868
- const preparationErrors = new Map<string, unknown>();
1869
- const ledger = new Map<string, IToolExecutionLedgerEntry>();
1870
- const executionIntentIds = new Map<string, string>();
1871
- const preliminaryToolOutputs = new Map<string, IAgentToolCallUpdateEvent>();
1872
-
1873
- const beginInference = async (): Promise<string> => {
1874
- const marker = this.commitEvent(createAgentEvent<IRuntimeEvent>({
1875
- type: 'runtime-event',
1876
- event: {
1877
- type: 'inference-started',
1878
- generationId,
1879
- completedSteps: stepCount,
1880
- },
1881
- modelVisible: false,
1882
- generationId,
1883
- parentIds: this.events.length > 0
1884
- ? [this.events[this.events.length - 1].id]
1885
- : undefined,
1886
- }));
1887
- currentInferenceId = marker.id;
1888
- latestInferenceVisibleIds = new Set(this.events.map((event) => event.id));
1889
- await this.flushPersistence();
1890
- return marker.id;
1891
- };
1892
- const allTools = this.wrapToolsForGeneration(
1893
- generationId,
1894
- runtime.tools,
1895
- preparations,
1896
- completedPreparations,
1897
- preparationErrors,
1898
- ledger,
1899
- () => this.buildContext(this.eventsByIds(latestInferenceVisibleIds)),
1900
- );
1901
-
1902
- const flushReasoning = (): void => {
1903
- for (const [id, reasoningText] of reasoningTextById) {
1904
- this.options.onReasoningEnd?.(id, reasoningText);
1905
- }
1906
- reasoningTextById.clear();
1907
- };
1908
-
1909
- while (true) {
1910
- const remainingSteps = maxSteps - stepCount;
1911
- if (remainingSteps <= 0) {
1912
- throw new Error(`Agent generation exhausted its ${maxSteps} step limit during retry.`);
1913
- }
1914
- let streamError: unknown;
1915
- try {
1916
- currentInferenceId = undefined;
1917
- const result = plugins.streamText({
1918
- model: runtime.model,
1919
- instructions: runtime.system,
1920
- messages: this.getModelMessages(),
1921
- allowSystemInMessages: true,
1922
- tools: allTools,
1923
- providerOptions: runtime.providerOptions,
1924
- stopWhen: plugins.stepCountIs(remainingSteps),
1925
- maxRetries: 0,
1926
- abortSignal: combinedAbort.signal,
1927
- prepareStep: async () => {
1928
- await beginInference();
1929
- return { messages: this.buildContext(this.eventsByIds(latestInferenceVisibleIds)) };
1930
- },
1931
- onError: ({ error }) => {
1932
- streamError = error;
1933
- },
1934
- repairToolCall: async ({ toolCall, tools: availableTools, error }) => {
1935
- const lowerName = toolCall.toolName.toLowerCase();
1936
- if (
1937
- lowerName !== toolCall.toolName
1938
- && Object.prototype.hasOwnProperty.call(availableTools, lowerName)
1939
- ) {
1940
- return { ...toolCall, toolName: lowerName };
1941
- }
1942
- return {
1943
- ...toolCall,
1944
- toolName: 'invalid',
1945
- input: JSON.stringify({ tool: toolCall.toolName, error: String(error) }),
1946
- };
1947
- },
1948
- onChunk: ({ chunk }) => {
1949
- const chunkType = String((chunk as { type?: unknown }).type ?? '');
1950
- if (chunkType === 'text-delta') {
1951
- const textChunk = chunk as { delta?: string; textDelta?: string; text?: string };
1952
- this.options.onToken?.(textChunk.delta ?? textChunk.textDelta ?? textChunk.text ?? '');
1953
- return;
1954
- }
1955
- if (chunkType === 'tool-result') {
1956
- const toolResultChunk = chunk as {
1957
- preliminary?: boolean;
1958
- toolCallId: string;
1959
- toolName: string;
1960
- input: unknown;
1961
- output: unknown;
1962
- };
1963
- if (toolResultChunk.preliminary === true) {
1964
- const previous = preliminaryToolOutputs.get(toolResultChunk.toolCallId);
1965
- if (!previous || !plugins.isDeepStrictEqual(previous.output, toolResultChunk.output)) {
1966
- const event: IAgentToolCallUpdateEvent = {
1967
- toolCallId: toolResultChunk.toolCallId,
1968
- toolName: toolResultChunk.toolName,
1969
- input: toolResultChunk.input,
1970
- output: toolResultChunk.output,
1971
- };
1972
- preliminaryToolOutputs.set(toolResultChunk.toolCallId, event);
1973
- this.options.onToolCallUpdate?.(event);
1974
- }
1975
- }
1976
- return;
1977
- }
1978
- const reasoningChunk = chunk as {
1979
- id?: string;
1980
- delta?: string;
1981
- textDelta?: string;
1982
- text?: string;
1983
- providerMetadata?: unknown;
1984
- };
1985
- const id = reasoningChunk.id ?? 'reasoning';
1986
- if (chunkType === 'reasoning-start') {
1987
- reasoningTextById.set(id, '');
1988
- this.options.onReasoningStart?.(id, reasoningChunk.providerMetadata);
1989
- } else if (chunkType === 'reasoning-delta') {
1990
- const delta = reasoningChunk.delta ?? reasoningChunk.textDelta ?? reasoningChunk.text ?? '';
1991
- if (!reasoningTextById.has(id)) {
1992
- reasoningTextById.set(id, '');
1993
- this.options.onReasoningStart?.(id, reasoningChunk.providerMetadata);
1994
- }
1995
- reasoningTextById.set(id, (reasoningTextById.get(id) ?? '') + delta);
1996
- this.options.onReasoningDelta?.(id, delta, reasoningChunk.providerMetadata);
1997
- } else if (chunkType === 'reasoning-end') {
1998
- const text = reasoningTextById.get(id) ?? '';
1999
- reasoningTextById.delete(id);
2000
- this.options.onReasoningEnd?.(id, text, reasoningChunk.providerMetadata);
2001
- }
2002
- },
2003
- onToolExecutionStart: ({ toolCall }) => {
2004
- const input = parseToolInput(toolCall.input);
2005
- const signature = toolSignature(toolCall.toolName, input);
2006
- const existing = preparations.get(toolCall.toolCallId);
2007
- if (existing) {
2008
- if (existing.signature !== signature) {
2009
- throw new Error(`Tool call id collision for "${toolCall.toolCallId}" in generation "${generationId}".`);
2010
- }
2011
- return existing.promise;
2012
- }
2013
- const promise = Promise.resolve().then(async () => {
2014
- const startedBeforeInference = !currentInferenceId;
2015
- const inferenceId = currentInferenceId ?? await beginInference();
2016
- const approvalAssistantEvent = startedBeforeInference
2017
- ? [...this.events].reverse().find((candidate): candidate is IAssistantMessageEvent =>
2018
- candidate.type === 'assistant-message'
2019
- && Array.isArray(candidate.content)
2020
- && candidate.content.some((part) =>
2021
- part.type === 'tool-call' && part.toolCallId === toolCall.toolCallId,
2022
- ),
2023
- )
2024
- : undefined;
2025
- let callEvent = [...this.events].reverse().find((event): event is IToolCallEvent =>
2026
- event.type === 'tool-call'
2027
- && event.toolCallId === toolCall.toolCallId
2028
- && (
2029
- event.generationId === generationId
2030
- || (
2031
- !!approvalAssistantEvent?.id
2032
- && event.parentIds?.includes(approvalAssistantEvent.id) === true
2033
- )
2034
- ));
2035
- if (callEvent && toolSignature(callEvent.toolName, callEvent.input) !== signature) {
2036
- throw new Error(`Tool call id collision for "${toolCall.toolCallId}" in generation "${generationId}".`);
2037
- }
2038
- if (!callEvent) {
2039
- callEvent = this.commitEvent(createAgentEvent<IToolCallEvent>({
2040
- type: 'tool-call',
2041
- toolCallId: toolCall.toolCallId,
2042
- toolName: toolCall.toolName,
2043
- input,
2044
- generationId,
2045
- inferenceId,
2046
- causationId: inferenceId,
2047
- parentIds: [inferenceId],
2048
- }));
2049
- }
2050
- await this.flushPersistence();
2051
- recordToolCall(toolCalls, toolCallIndexes, toolCall);
2052
- if (options.transaction) {
2053
- let intent = [...this.events].reverse().find(
2054
- (event): event is IToolExecutionIntentEvent =>
2055
- event.type === 'tool-execution-intent'
2056
- && event.generationId === generationId
2057
- && event.toolCallId === toolCall.toolCallId,
2058
- );
2059
- if (intent && toolSignature(intent.toolName, intent.input) !== signature) {
2060
- throw new Error(`Tool call id collision for "${toolCall.toolCallId}" in generation "${generationId}".`);
2061
- }
2062
- if (!intent) {
2063
- intent = this.commitEvent(createAgentEvent<IToolExecutionIntentEvent>({
2064
- type: 'tool-execution-intent',
2065
- generationId,
2066
- inferenceId: callEvent.inferenceId,
2067
- causationId: callEvent.id,
2068
- toolCallId: toolCall.toolCallId,
2069
- toolName: toolCall.toolName,
2070
- input,
2071
- parentIds: [callEvent.id],
2072
- }));
2073
- }
2074
- executionIntentIds.set(toolCall.toolCallId, intent.id);
2075
- await this.flushPersistence();
2076
- }
2077
- completedPreparations.add(toolCall.toolCallId);
2078
- const callbackEvent: IAgentToolCallStartEvent = {
2079
- toolCallId: toolCall.toolCallId,
2080
- toolName: toolCall.toolName,
2081
- input,
2082
- };
2083
- this.options.onToolCallStart?.(callbackEvent);
2084
- this.options.onToolCall?.(callbackEvent.toolName, callbackEvent.input);
2085
- });
2086
- preparations.set(toolCall.toolCallId, { signature, promise });
2087
- void promise.catch((error) => {
2088
- if (!completedPreparations.has(toolCall.toolCallId)) {
2089
- preparationErrors.set(toolCall.toolCallId, error);
2090
- }
2091
- });
2092
- return promise;
2093
- },
2094
- onToolExecutionEnd: async (finish) => {
2095
- preliminaryToolOutputs.delete(finish.toolCall.toolCallId);
2096
- const inferenceId = currentInferenceId ?? await beginInference();
2097
- const previousResult = this.currentGenerationEvent(
2098
- generationId,
2099
- 'tool-result',
2100
- finish.toolCall.toolCallId,
2101
- );
2102
- if (this.currentGenerationEvent(
2103
- generationId,
2104
- 'tool-result',
2105
- finish.toolCall.toolCallId,
2106
- inferenceId,
2107
- )) return;
2108
- const input = parseToolInput(finish.toolCall.input);
2109
- const startEvent: IAgentToolCallStartEvent = {
2110
- toolCallId: finish.toolCall.toolCallId,
2111
- toolName: finish.toolCall.toolName,
2112
- input,
2113
- };
2114
- const finishEvent: TAgentToolCallFinishEvent = finish.toolOutput.type === 'tool-result'
2115
- ? { ...startEvent, success: true, output: finish.toolOutput.output }
2116
- : { ...startEvent, success: false, error: errorToString(finish.toolOutput.error) };
2117
- const parentCall = this.currentGenerationEvent(
2118
- generationId,
2119
- 'tool-call',
2120
- finish.toolCall.toolCallId,
2121
- inferenceId,
2122
- );
2123
- const toolWithModelOutput = allTools[finish.toolCall.toolName] as {
2124
- toModelOutput?: (options: {
2125
- toolCallId: string;
2126
- input: unknown;
2127
- output: unknown;
2128
- }) => Promise<TAgentToolResultOutput> | TAgentToolResultOutput;
2129
- } | undefined;
2130
- const modelOutput = finishEvent.success && toolWithModelOutput?.toModelOutput
2131
- ? await this.invokeGenerationCallback(
2132
- () => toolWithModelOutput.toModelOutput!({
2133
- toolCallId: finish.toolCall.toolCallId,
2134
- input,
2135
- output: finishEvent.output,
2136
- }),
2137
- )
2138
- : fallbackModelOutput(
2139
- finishEvent.success ? finishEvent.output : undefined,
2140
- finishEvent.success ? undefined : finishEvent.error,
2141
- );
2142
- const executionIntentId = executionIntentIds.get(finish.toolCall.toolCallId);
2143
- this.commitEvent(createAgentEvent<IToolResultEvent>({
2144
- type: 'tool-result',
2145
- toolCallId: finish.toolCall.toolCallId,
2146
- toolName: finish.toolCall.toolName,
2147
- ...(executionIntentId ? { executionIntentId } : {}),
2148
- output: finishEvent.success ? finishEvent.output : undefined,
2149
- error: finishEvent.success ? undefined : finishEvent.error,
2150
- modelOutput,
2151
- generationId,
2152
- inferenceId,
2153
- causationId: inferenceId,
2154
- parentIds: parentCall ? [parentCall.id] : [inferenceId],
2155
- }));
2156
- await this.flushPersistence();
2157
- recordToolCall(
2158
- toolCalls,
2159
- toolCallIndexes,
2160
- finish.toolCall,
2161
- finishEvent.success ? { output: finishEvent.output } : { error: finishEvent.error },
2162
- );
2163
- if (previousResult) return;
2164
- this.options.onToolCallFinish?.(finishEvent);
2165
- this.options.onToolResult?.(
2166
- finishEvent.toolName,
2167
- finishEvent.success ? finishEvent.output : undefined,
2168
- );
2169
- },
2170
- onStepEnd: async (step) => {
2171
- if (!currentInferenceId) throw new Error('Model step finished outside an inference.');
2172
- if (!completedInferenceIds.has(currentInferenceId)) {
2173
- completedInferenceIds.add(currentInferenceId);
2174
- stepCount++;
2175
- }
2176
- totalInput += step.usage.inputTokens ?? 0;
2177
- totalOutput += step.usage.outputTokens ?? 0;
2178
- totalCacheRead += step.usage.inputTokenDetails.cacheReadTokens ?? 0;
2179
- totalCacheWrite += step.usage.inputTokenDetails.cacheWriteTokens ?? 0;
2180
- for (const toolCall of step.toolCalls) {
2181
- recordToolCall(toolCalls, toolCallIndexes, toolCall);
2182
- }
2183
- for (const toolResult of step.toolResults) {
2184
- recordToolCall(toolCalls, toolCallIndexes, toolResult, { output: toolResult.output });
2185
- }
2186
- for (const part of step.content) {
2187
- if (part.type === 'tool-error') {
2188
- recordToolCall(toolCalls, toolCallIndexes, part, { error: part.error });
2189
- }
2190
- }
2191
- this.commitResponseMessages(
2192
- step.response.messages,
2193
- generationId,
2194
- currentInferenceId,
2195
- executionIntentIds,
2196
- );
2197
- await this.flushPersistence();
2198
- attempt = 0;
2199
- contextOverflowRetries = 0;
2200
- },
2201
- });
2202
-
2203
- const text = await result.text;
2204
- const finishReason = await result.finishReason;
2205
- await result.response;
2206
- if (streamError) throw streamError;
2207
- flushReasoning();
2208
- await this.flushPersistence();
2209
-
2210
- const resultVisibleIds = new Set(latestInferenceVisibleIds);
2211
- for (const event of this.events) {
2212
- if (event.generationId === generationId) resultVisibleIds.add(event.id);
2213
- }
2214
- const generationResult: IAgentGenerateResult = {
2215
- text,
2216
- messages: this.buildContext(this.eventsByIds(resultVisibleIds)),
2217
- steps: stepCount,
2218
- finishReason,
2219
- usage: {
2220
- inputTokens: totalInput,
2221
- outputTokens: totalOutput,
2222
- totalTokens: totalInput + totalOutput,
2223
- cacheReadTokens: totalCacheRead,
2224
- cacheWriteTokens: totalCacheWrite,
2225
- },
2226
- toolCalls,
2227
- events: this.getEvents(),
2228
- };
2229
- await this.runEventRetentionLocked();
2230
- generationResult.events = this.getEvents();
2231
- completedResult = generationResult;
2232
- return generationResult;
2233
- } catch (error) {
2234
- flushReasoning();
2235
- const effectiveError = streamError ?? error;
2236
- if (combinedAbort.signal.aborted) {
2237
- throw combinedAbort.signal.reason ?? new DOMException('Aborted', 'AbortError');
2238
- }
2239
- if (effectiveError instanceof DOMException && effectiveError.name === 'AbortError') {
2240
- throw effectiveError;
2241
- }
2242
-
2243
- if (
2244
- currentInferenceId
2245
- && !completedInferenceIds.has(currentInferenceId)
2246
- && this.events.some((event) =>
2247
- event.generationId === generationId
2248
- && event.inferenceId === currentInferenceId
2249
- && event.type === 'tool-result',
2250
- )
2251
- ) {
2252
- completedInferenceIds.add(currentInferenceId);
2253
- stepCount++;
2254
- }
2255
-
2256
- if (isRetryableError(effectiveError) && attempt < MAX_RETRY_ATTEMPTS && stepCount < maxSteps) {
2257
- attempt++;
2258
- const errorWithHeaders = effectiveError as {
2259
- responseHeaders?: Record<string, string>;
2260
- headers?: Record<string, string>;
2261
- };
2262
- await sleep(
2263
- retryDelay(attempt, errorWithHeaders.responseHeaders ?? errorWithHeaders.headers),
2264
- combinedAbort.signal,
2265
- );
2266
- continue;
2267
- }
2268
-
2269
- if (isContextOverflow(effectiveError)) {
2270
- if (!this.options.contextCompactor && !this.options.onContextOverflow) {
2271
- throw new ContextOverflowError();
2272
- }
2273
- if (contextOverflowRetries >= maxContextOverflowRetries) {
2274
- throw new ContextOverflowError(
2275
- `Agent context overflow persisted after ${maxContextOverflowRetries} compaction retries`,
2276
- );
2277
- }
2278
- combinedAbort.signal.throwIfAborted();
2279
- contextOverflowRetries++;
2280
- await this.performCompaction('context-overflow', combinedAbort.signal);
2281
- continue;
2282
- }
2283
- throw effectiveError;
2284
- }
2285
- }
2286
- } catch (error) {
2287
- operationError = error;
2288
- } finally {
2289
- let cleanupError: unknown;
2290
- if (leaseCleanupOwnership) {
2291
- try {
2292
- await this.cleanupGenerationLease(leaseCleanupOwnership);
2293
- } catch (error) {
2294
- cleanupError = error;
2295
- }
2296
- }
2297
- if (operationError === undefined && combinedAbort.signal.aborted) {
2298
- operationError = combinedAbort.signal.reason
2299
- ?? new DOMException('Aborted', 'AbortError');
2300
- }
2301
- let effectiveError = operationError !== undefined && cleanupError !== undefined
2302
- ? new AggregateError(
2303
- [operationError, cleanupError],
2304
- 'Agent generation and generation lease cleanup both failed.',
2305
- )
2306
- : operationError ?? cleanupError;
2307
- try {
2308
- if (options.transaction && transactionExecutionStarted) {
2309
- if (effectiveError === undefined && combinedAbort.signal.aborted) {
2310
- effectiveError = combinedAbort.signal.reason
2311
- ?? new DOMException('Aborted', 'AbortError');
2312
- }
2313
- try {
2314
- if (effectiveError !== undefined) {
2315
- await this.appendGenerationOutcome(
2316
- generationId,
2317
- 'interrupted',
2318
- this.transactionOutcomeErrorReason(effectiveError),
2319
- );
2320
- } else {
2321
- const transaction = this.transaction(generationId);
2322
- if (transaction.state !== 'execution-started') {
2323
- throw new Error(
2324
- `Transactional generation "${generationId}" cannot complete from state "${transaction.state}".`,
2325
- );
2326
- }
2327
- combinedAbort.signal.throwIfAborted();
2328
- this.commitEvent(createAgentEvent<IGenerationExecutionCompletedEvent>({
2329
- type: 'generation-execution-completed',
2330
- generationId,
2331
- parentIds: [transaction.executionStarted!.id],
2332
- }));
2333
- await this.flushPersistence();
2334
- if (combinedAbort.signal.aborted) {
2335
- const abortError = combinedAbort.signal.reason
2336
- ?? new DOMException('Aborted', 'AbortError');
2337
- await this.appendGenerationOutcome(
2338
- generationId,
2339
- 'interrupted',
2340
- this.transactionOutcomeErrorReason(abortError),
2341
- );
2342
- throw abortError;
2343
- }
2344
- if (completedResult) completedResult.events = this.getEvents();
2345
- }
2346
- } catch (outcomeError) {
2347
- if (effectiveError !== undefined) {
2348
- throw new AggregateError(
2349
- [effectiveError, outcomeError],
2350
- 'Agent generation failed and its interrupted outcome could not be persisted.',
2351
- );
2352
- }
2353
- throw outcomeError;
2354
- }
2355
- }
2356
- if (effectiveError !== undefined) throw effectiveError;
2357
- } finally {
2358
- combinedAbort.cleanup();
2359
- if (this.currentGenerationAbortController === generationAbortController) {
2360
- this.currentGenerationAbortController = undefined;
2361
- }
2362
- }
2363
- }
2364
- throw operationError ?? new Error('Agent generation ended without a result.');
2365
- }
2366
-
2367
- public abortCurrentGeneration(reason?: unknown): void {
2368
- this.currentGenerationAbortController?.abort(
2369
- reason ?? new DOMException('Current agent generation aborted', 'AbortError'),
2370
- );
2371
- }
2372
-
2373
- public async abortSession(
2374
- reason?: unknown,
2375
- options: IAgentSessionAbortOptions = {},
2376
- ): Promise<void> {
2377
- if (!this.sessionAbortController.signal.aborted) {
2378
- this.sessionAbortController.abort(
2379
- reason ?? new DOMException('Agent session aborted', 'AbortError'),
2380
- );
2381
- }
2382
- this.abortCurrentGeneration(reason);
2383
- for (const key of [...this.scheduledGenerations.keys()]) {
2384
- this.cancelScheduledGeneration(
2385
- key,
2386
- reason ?? new DOMException('Agent session aborted', 'AbortError'),
2387
- );
2388
- }
2389
- if (options.abortBackgroundJobs) {
2390
- const jobs = this.options.executionContext?.jobs;
2391
- if (!jobs?.abort) return;
2392
- const runningJobs = (await jobs.list()).filter((job) => job.state === 'running');
2393
- await Promise.all(runningJobs.map((job) => jobs.abort!(job.executionId)));
2394
- }
2395
- }
2396
-
2397
- public async getBackgroundExecution(executionId: string): Promise<plugins.IToolJobState> {
2398
- const jobs = this.options.executionContext?.jobs;
2399
- if (!jobs) throw new Error('This AgentSession has no background job context.');
2400
- return jobs.get(executionId);
2401
- }
2402
-
2403
- public async abortBackgroundExecution(executionId: string): Promise<void> {
2404
- const jobs = this.options.executionContext?.jobs;
2405
- if (!jobs?.abort) throw new Error('This AgentSession cannot abort background jobs.');
2406
- await jobs.abort(executionId);
2407
- }
2408
-
2409
- public async close(): Promise<void> {
2410
- if (this.generationCallbackContext.getStore()?.active) {
2411
- throw new Error(
2412
- 'AgentSession.close() cannot be called from an active tool or compactor callback.',
2413
- );
2414
- }
2415
- this.closeFinalizationReached = false;
2416
- this.refreshCloseCleanupCompleted();
2417
- if (!this.closed) {
2418
- this.closed = true;
2419
- const closeReason = new DOMException('AgentSession closed', 'AbortError');
2420
- if (!this.sessionAbortController.signal.aborted) {
2421
- this.sessionAbortController.abort(closeReason);
2422
- }
2423
- this.abortCurrentGeneration(closeReason);
2424
- for (const key of [...this.scheduledGenerations.keys()]) {
2425
- this.cancelScheduledGeneration(key, closeReason);
2426
- }
2427
- }
2428
- let closeError: unknown;
2429
- const rememberCloseError = (error: unknown, message: string) => {
2430
- closeError = closeError === undefined
2431
- ? error
2432
- : new AggregateError([closeError, error], message);
2433
- };
2434
- try {
2435
- this.jobUnsubscribe?.();
2436
- this.jobUnsubscribe = undefined;
2437
- } catch (error) {
2438
- rememberCloseError(error, 'AgentSession close encountered multiple cleanup errors.');
2439
- }
2440
- try {
2441
- await this.generationQueue;
2442
- for (const transaction of getOpenAgentGenerationTransactions(this.events)) {
2443
- await this.appendGenerationOutcome(
2444
- transaction.generationId,
2445
- 'interrupted',
2446
- 'AgentSession closed before the transactional generation was finalized.',
2447
- );
2448
- }
2449
- if (this.retentionPromise) await this.retentionPromise;
2450
- await this.flushPersistence();
2451
- if (this.maintenanceError !== undefined) throw this.maintenanceError;
2452
- } catch (error) {
2453
- rememberCloseError(error, 'AgentSession close encountered persistence or maintenance errors.');
2454
- } finally {
2455
- for (const ownership of [...this.unresolvedLeaseCleanups]) {
2456
- try {
2457
- await this.cleanupGenerationLease(ownership);
2458
- } catch (error) {
2459
- rememberCloseError(error, 'AgentSession close encountered multiple cleanup errors.');
2460
- }
2461
- }
2462
- this.generationHandles.clear();
2463
- try {
2464
- while (this.notificationDispatches.size > 0) {
2465
- await Promise.all(this.notificationDispatches);
2466
- }
2467
- } catch (error) {
2468
- rememberCloseError(error, 'AgentSession close encountered notification cleanup errors.');
2469
- }
2470
- const listenerStates = [...this.listeners.values()];
2471
- this.listeners.clear();
2472
- try {
2473
- await Promise.all(listenerStates
2474
- .filter((state) => !state.executing && state.deliveryPromise)
2475
- .map((state) => state.deliveryPromise));
2476
- } catch (error) {
2477
- rememberCloseError(error, 'AgentSession close encountered listener cleanup errors.');
2478
- } finally {
2479
- this.closeFinalizationReached = true;
2480
- this.refreshCloseCleanupCompleted();
2481
- }
2482
- }
2483
- if (closeError !== undefined) throw closeError;
2484
- }
2485
-
2486
- private invokeGenerationCallback<TValue>(callback: () => TValue): TValue {
2487
- const state: IGenerationCallbackState = { active: true };
2488
- return this.generationCallbackContext.run(state, () => {
2489
- try {
2490
- const result = callback();
2491
- return this.adoptGenerationCallbackResult(result, state) as TValue;
2492
- } catch (error) {
2493
- state.active = false;
2494
- throw error;
2495
- }
2496
- });
2497
- }
2498
-
2499
- private adoptGenerationCallbackResult(
2500
- result: unknown,
2501
- state: IGenerationCallbackState,
2502
- ): unknown {
2503
- try {
2504
- if (isAsyncIterable(result)) {
2505
- return this.wrapGenerationCallbackIterable(result, state);
2506
- }
2507
- if (isPromiseLike(result)) {
2508
- return Promise.resolve(result).then(
2509
- (resolvedResult) => this.adoptGenerationCallbackResult(resolvedResult, state),
2510
- (error) => {
2511
- state.active = false;
2512
- throw error;
2513
- },
2514
- );
2515
- }
2516
- state.active = false;
2517
- return result;
2518
- } catch (error) {
2519
- state.active = false;
2520
- throw error;
2521
- }
2522
- }
2523
-
2524
- private wrapGenerationCallbackIterable(
2525
- iterable: AsyncIterable<unknown>,
2526
- state: IGenerationCallbackState,
2527
- ): AsyncIterable<unknown> {
2528
- const iterator = this.generationCallbackContext.run(
2529
- state,
2530
- () => iterable[Symbol.asyncIterator](),
2531
- );
2532
- let settled = false;
2533
- const settle = () => {
2534
- if (settled) return;
2535
- settled = true;
2536
- state.active = false;
2537
- };
2538
- const invoke = (
2539
- operation: () => Promise<IteratorResult<unknown>>,
2540
- ): Promise<IteratorResult<unknown>> => {
2541
- let operationResult: Promise<IteratorResult<unknown>>;
2542
- try {
2543
- operationResult = this.generationCallbackContext.run(
2544
- state,
2545
- () => Promise.resolve(operation()),
2546
- );
2547
- } catch (error) {
2548
- settle();
2549
- return Promise.reject(error);
2550
- }
2551
- return operationResult.then(
2552
- (iteratorResult) => {
2553
- if (iteratorResult.done) settle();
2554
- return iteratorResult;
2555
- },
2556
- (error) => {
2557
- settle();
2558
- throw error;
2559
- },
2560
- );
2561
- };
2562
- const wrappedIterator: AsyncIterator<unknown, unknown, unknown> & AsyncIterable<unknown> = {
2563
- next: (...args: [] | [unknown]) => invoke(() => iterator.next(...args)),
2564
- return: iterator.return
2565
- ? async (...args: [] | [unknown]) => {
2566
- let iteratorResult = await invoke(() => iterator.return!(...args));
2567
- while (!iteratorResult.done) {
2568
- iteratorResult = await invoke(() => iterator.next());
2569
- }
2570
- return iteratorResult;
2571
- }
2572
- : async (...args: [] | [unknown]) => {
2573
- settle();
2574
- return { done: true, value: args[0] };
2575
- },
2576
- throw: iterator.throw
2577
- ? (error?: unknown) => invoke(() => iterator.throw!(error))
2578
- : async (error?: unknown) => {
2579
- settle();
2580
- throw error;
2581
- },
2582
- [Symbol.asyncIterator]() {
2583
- return this;
2584
- },
2585
- };
2586
- return wrappedIterator;
27
+ public static create(options: IAgentSessionOptions): Promise<AgentSession> {
28
+ return this.createWithFactory(options, () => new AgentSession(options));
2587
29
  }
2588
30
  }