@modelprofile.com/flexharness-agent 9.9.1 → 10.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.
- package/dist_ts_agent/classes.sessionengine.d.ts +3 -2
- package/dist_ts_agent/classes.sessionengine.js +208 -20
- package/dist_ts_agent/index.d.ts +4 -2
- package/dist_ts_agent/index.js +3 -2
- package/dist_ts_agent/smartagent.events.d.ts +9 -0
- package/dist_ts_agent/smartagent.events.js +1 -1
- package/dist_ts_agent/smartagent.interfaces.d.ts +30 -2
- package/dist_ts_agent/smartagent.interfaces.js +1 -1
- package/dist_ts_agent/smartagent.pendinginput.d.ts +58 -0
- package/dist_ts_agent/smartagent.pendinginput.js +92 -0
- package/dist_ts_agent/smartagent.persistence.js +9 -1
- package/dist_ts_agent/smartagent.transactions.d.ts +9 -0
- package/dist_ts_agent/smartagent.transactions.js +92 -5
- package/package.json +5 -5
- package/readme.md +27 -0
- package/ts_agent/classes.sessionengine.ts +210 -11
- package/ts_agent/index.ts +11 -0
- package/ts_agent/readme.md +27 -0
- package/ts_agent/smartagent.events.ts +9 -0
- package/ts_agent/smartagent.interfaces.ts +31 -1
- package/ts_agent/smartagent.pendinginput.ts +116 -0
- package/ts_agent/smartagent.persistence.ts +9 -0
- package/ts_agent/smartagent.transactions.ts +106 -5
|
@@ -20,8 +20,13 @@ import {
|
|
|
20
20
|
type IUserMessageEvent,
|
|
21
21
|
type TAgentEvent,
|
|
22
22
|
type TAgentGenerationOutcome,
|
|
23
|
+
type TAgentToolCallPart,
|
|
23
24
|
type TAgentToolResultOutput,
|
|
24
25
|
} from './smartagent.events.js';
|
|
26
|
+
import type {
|
|
27
|
+
AgentPendingInputQueue,
|
|
28
|
+
IAgentAppliedInput,
|
|
29
|
+
} from './smartagent.pendinginput.js';
|
|
25
30
|
import {
|
|
26
31
|
AgentGenerationLeaseCleanupError,
|
|
27
32
|
ContextOverflowError,
|
|
@@ -31,6 +36,7 @@ import {
|
|
|
31
36
|
type IAgentGenerationLease,
|
|
32
37
|
type IAgentCompactOptions,
|
|
33
38
|
type IAgentBeginGenerationOptions,
|
|
39
|
+
type IAgentFinalizeGenerationOptions,
|
|
34
40
|
type IAgentSession,
|
|
35
41
|
type IAgentSessionAbortOptions,
|
|
36
42
|
type IAgentSessionChange,
|
|
@@ -101,8 +107,11 @@ interface IScheduledGeneration {
|
|
|
101
107
|
cache?: TAgentCacheSetting;
|
|
102
108
|
maxSteps?: number;
|
|
103
109
|
abort?: AbortSignal;
|
|
110
|
+
stop?: AbortSignal;
|
|
104
111
|
transaction?: IAgentGenerationHandle;
|
|
105
112
|
prepare?: TAgentGenerationPrepare;
|
|
113
|
+
pendingInput?: AgentPendingInputQueue;
|
|
114
|
+
onInputApplied?: (input: IAgentAppliedInput) => void | Promise<void>;
|
|
106
115
|
timer: NodeJS.Timeout;
|
|
107
116
|
controller: AbortController;
|
|
108
117
|
removeAbortListener: () => void;
|
|
@@ -316,6 +325,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
316
325
|
private sequence = 0;
|
|
317
326
|
private persistenceRevision = 0;
|
|
318
327
|
private persistenceQueue: Promise<void> = Promise.resolve();
|
|
328
|
+
private pendingPersistence?: { snapshot: TAgentEvent[]; task: Promise<void> };
|
|
319
329
|
private persistenceError: unknown;
|
|
320
330
|
private readonly notificationDispatches = new Set<Promise<void>>();
|
|
321
331
|
private readonly listeners = new Map<TAgentSessionChangeListener, IAgentSessionListenerState>();
|
|
@@ -401,6 +411,14 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
401
411
|
if (!Number.isSafeInteger(maxContextOverflowRetries) || maxContextOverflowRetries < 0) {
|
|
402
412
|
throw new Error('maxContextOverflowRetries must be a non-negative integer.');
|
|
403
413
|
}
|
|
414
|
+
if (options.contextCompactionBytes !== undefined && options.contextCompactionBytes !== false
|
|
415
|
+
&& (!Number.isSafeInteger(options.contextCompactionBytes)
|
|
416
|
+
|| options.contextCompactionBytes < 1 || options.contextCompactionBytes > 32 * 1024 * 1024)) {
|
|
417
|
+
throw new Error('contextCompactionBytes must be false or an integer from 1 through 33554432.');
|
|
418
|
+
}
|
|
419
|
+
if (typeof options.contextCompactionBytes === 'number' && !options.contextCompactor) {
|
|
420
|
+
throw new Error('contextCompactionBytes requires a contextCompactor.');
|
|
421
|
+
}
|
|
404
422
|
const changeListenerTimeoutMs = options.changeListenerTimeoutMs ?? 30_000;
|
|
405
423
|
if (!Number.isSafeInteger(changeListenerTimeoutMs) || changeListenerTimeoutMs <= 0) {
|
|
406
424
|
throw new Error('changeListenerTimeoutMs must be a positive integer.');
|
|
@@ -500,14 +518,24 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
500
518
|
private queuePersistence(): Promise<void> {
|
|
501
519
|
if (!this.options.eventStore) return Promise.resolve();
|
|
502
520
|
const snapshot = structuredClone(this.events);
|
|
521
|
+
if (this.pendingPersistence) {
|
|
522
|
+
this.pendingPersistence.snapshot = snapshot;
|
|
523
|
+
return this.pendingPersistence.task;
|
|
524
|
+
}
|
|
525
|
+
let pending: NonNullable<AgentSessionEngine['pendingPersistence']>;
|
|
503
526
|
const operation = this.persistenceQueue.then(async () => {
|
|
527
|
+
if (this.pendingPersistence === pending) this.pendingPersistence = undefined;
|
|
504
528
|
if (this.persistenceError !== undefined) throw this.persistenceError;
|
|
505
529
|
this.persistenceRevision = await this.options.eventStore!.save(
|
|
506
530
|
this.options.sessionId!,
|
|
507
|
-
snapshot,
|
|
531
|
+
pending.snapshot,
|
|
508
532
|
this.persistenceRevision,
|
|
509
533
|
);
|
|
510
534
|
});
|
|
535
|
+
// One durable snapshot covers every commit queued before this write begins. In-flight snapshots
|
|
536
|
+
// stay immutable, and callers still wait for the write that contains their event.
|
|
537
|
+
pending = { snapshot, task: operation };
|
|
538
|
+
this.pendingPersistence = pending;
|
|
511
539
|
this.persistenceQueue = operation.catch((error) => {
|
|
512
540
|
this.persistenceError ??= error;
|
|
513
541
|
});
|
|
@@ -821,6 +849,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
821
849
|
generationId: string,
|
|
822
850
|
outcome: TAgentGenerationOutcome,
|
|
823
851
|
reason?: string,
|
|
852
|
+
stopped = false,
|
|
824
853
|
): Promise<void> {
|
|
825
854
|
const transaction = this.transaction(generationId);
|
|
826
855
|
if (
|
|
@@ -853,6 +882,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
853
882
|
generationId,
|
|
854
883
|
outcome,
|
|
855
884
|
reason,
|
|
885
|
+
...(stopped ? { stopped: true } : {}),
|
|
856
886
|
parentIds: [parent.id],
|
|
857
887
|
}));
|
|
858
888
|
await this.flushPersistence();
|
|
@@ -874,6 +904,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
874
904
|
public finalizeGeneration(
|
|
875
905
|
transaction: IAgentGenerationHandle,
|
|
876
906
|
outcome: TAgentGenerationOutcome,
|
|
907
|
+
options: IAgentFinalizeGenerationOptions = {},
|
|
877
908
|
): Promise<void> {
|
|
878
909
|
const capabilityError = this.transactionalStoreCapabilityError();
|
|
879
910
|
if (capabilityError) return Promise.reject(capabilityError);
|
|
@@ -881,6 +912,9 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
881
912
|
if (!['accepted', 'rejected', 'interrupted'].includes(outcome)) {
|
|
882
913
|
throw new Error(`Invalid transactional generation outcome "${String(outcome)}".`);
|
|
883
914
|
}
|
|
915
|
+
if (options.stopped === true && outcome !== 'interrupted') {
|
|
916
|
+
throw new Error('Only an interrupted transactional generation can be stopped.');
|
|
917
|
+
}
|
|
884
918
|
const state = this.verifyGenerationClaim(transaction);
|
|
885
919
|
if (!('begun' in state)) {
|
|
886
920
|
if (state.outcome === outcome) return;
|
|
@@ -888,7 +922,24 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
888
922
|
`Transactional generation "${transaction.generationId}" already has outcome "${state.outcome}".`,
|
|
889
923
|
);
|
|
890
924
|
}
|
|
891
|
-
await this.appendGenerationOutcome(
|
|
925
|
+
await this.appendGenerationOutcome(
|
|
926
|
+
transaction.generationId,
|
|
927
|
+
outcome,
|
|
928
|
+
undefined,
|
|
929
|
+
options.stopped === true,
|
|
930
|
+
);
|
|
931
|
+
if (this.options.contextCompactor && this.options.contextCompactionBytes !== false
|
|
932
|
+
&& this.options.eventStore?.archive
|
|
933
|
+
&& getOpenAgentGenerationTransactions(this.events).length === 0
|
|
934
|
+
&& getUncertainToolExecutionIntents(this.events).length === 0) {
|
|
935
|
+
const activeIds = new Set(this.events.map((event) => event.id));
|
|
936
|
+
for (const event of [...this.events]) {
|
|
937
|
+
if (event.type === 'context-compaction' && event.automatic
|
|
938
|
+
&& event.coveredEventIds.some((id) => activeIds.has(id))) {
|
|
939
|
+
await this.archiveCompactedEventsLocked(event.id);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
892
943
|
});
|
|
893
944
|
}
|
|
894
945
|
|
|
@@ -1024,6 +1075,9 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1024
1075
|
? this.transactionalStoreCapabilityError()
|
|
1025
1076
|
: undefined;
|
|
1026
1077
|
if (capabilityError) return Promise.reject(capabilityError);
|
|
1078
|
+
if (options.stop && !options.transaction) {
|
|
1079
|
+
return Promise.reject(new Error('A stop signal requires a transactional generation.'));
|
|
1080
|
+
}
|
|
1027
1081
|
if (!options.key.trim()) return Promise.reject(new Error('Scheduled generation key must not be empty.'));
|
|
1028
1082
|
const debounceMs = options.debounceMs ?? 50;
|
|
1029
1083
|
if (!Number.isSafeInteger(debounceMs) || debounceMs < 0) {
|
|
@@ -1039,7 +1093,10 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1039
1093
|
|| existing.cache !== options.cache
|
|
1040
1094
|
|| existing.maxSteps !== options.maxSteps
|
|
1041
1095
|
|| existing.abort !== options.abort
|
|
1096
|
+
|| existing.stop !== options.stop
|
|
1042
1097
|
|| existing.prepare !== options.prepare
|
|
1098
|
+
|| existing.pendingInput !== options.pendingInput
|
|
1099
|
+
|| existing.onInputApplied !== options.onInputApplied
|
|
1043
1100
|
|| existing.transaction?.generationId !== options.transaction?.generationId
|
|
1044
1101
|
|| existing.transaction?.claimToken !== options.transaction?.claimToken
|
|
1045
1102
|
) {
|
|
@@ -1053,8 +1110,9 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1053
1110
|
return existing.promise;
|
|
1054
1111
|
}
|
|
1055
1112
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1113
|
+
const endedBy = options.abort?.aborted ? options.abort : options.stop?.aborted ? options.stop : undefined;
|
|
1114
|
+
if (endedBy) {
|
|
1115
|
+
const abortReason = endedBy.reason ?? new DOMException('Aborted', 'AbortError');
|
|
1058
1116
|
if (!options.transaction) return Promise.reject(abortReason);
|
|
1059
1117
|
return this.queueExclusive(async () => {
|
|
1060
1118
|
const transaction = this.verifyGenerationClaim(options.transaction!);
|
|
@@ -1082,9 +1140,11 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1082
1140
|
const controller = new AbortController();
|
|
1083
1141
|
const onAbort = () => this.cancelScheduledGeneration(
|
|
1084
1142
|
options.key,
|
|
1085
|
-
options.abort?.
|
|
1143
|
+
(options.abort?.aborted ? options.abort.reason : options.stop?.reason)
|
|
1144
|
+
?? new DOMException('Aborted', 'AbortError'),
|
|
1086
1145
|
);
|
|
1087
1146
|
options.abort?.addEventListener('abort', onAbort, { once: true });
|
|
1147
|
+
options.stop?.addEventListener('abort', onAbort, { once: true });
|
|
1088
1148
|
const scheduled: IScheduledGeneration = {
|
|
1089
1149
|
state: 'debouncing',
|
|
1090
1150
|
debounceMs,
|
|
@@ -1095,11 +1155,17 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1095
1155
|
cache: options.cache,
|
|
1096
1156
|
maxSteps: options.maxSteps,
|
|
1097
1157
|
abort: options.abort,
|
|
1158
|
+
stop: options.stop,
|
|
1098
1159
|
transaction: options.transaction,
|
|
1099
1160
|
prepare: options.prepare,
|
|
1161
|
+
pendingInput: options.pendingInput,
|
|
1162
|
+
onInputApplied: options.onInputApplied,
|
|
1100
1163
|
timer: setTimeout(() => this.startScheduledGeneration(options.key), debounceMs),
|
|
1101
1164
|
controller,
|
|
1102
|
-
removeAbortListener: () =>
|
|
1165
|
+
removeAbortListener: () => {
|
|
1166
|
+
options.abort?.removeEventListener('abort', onAbort);
|
|
1167
|
+
options.stop?.removeEventListener('abort', onAbort);
|
|
1168
|
+
},
|
|
1103
1169
|
promise,
|
|
1104
1170
|
resolve,
|
|
1105
1171
|
reject,
|
|
@@ -1120,8 +1186,11 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1120
1186
|
cache: scheduled.cache,
|
|
1121
1187
|
maxSteps: scheduled.maxSteps,
|
|
1122
1188
|
abort: scheduled.controller.signal,
|
|
1189
|
+
stop: scheduled.stop,
|
|
1123
1190
|
transaction: scheduled.transaction,
|
|
1124
1191
|
prepare: scheduled.prepare,
|
|
1192
|
+
pendingInput: scheduled.pendingInput,
|
|
1193
|
+
onInputApplied: scheduled.onInputApplied,
|
|
1125
1194
|
}).then(
|
|
1126
1195
|
(result) => this.settleScheduledGeneration(key, scheduled, () => scheduled.resolve(result)),
|
|
1127
1196
|
(error) => this.settleScheduledGeneration(key, scheduled, () => scheduled.reject(error)),
|
|
@@ -1149,6 +1218,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1149
1218
|
const cancellationReason = reason
|
|
1150
1219
|
?? new DOMException('Scheduled generation cancelled', 'AbortError');
|
|
1151
1220
|
if (!scheduled.controller.signal.aborted) scheduled.controller.abort(cancellationReason);
|
|
1221
|
+
scheduled.pendingInput?.close();
|
|
1152
1222
|
if (!scheduled.transaction) {
|
|
1153
1223
|
scheduled.reject(cancellationReason);
|
|
1154
1224
|
return true;
|
|
@@ -1180,7 +1250,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1180
1250
|
}
|
|
1181
1251
|
|
|
1182
1252
|
private async performCompaction(
|
|
1183
|
-
reason: 'context-overflow' | 'retention' | 'manual',
|
|
1253
|
+
reason: 'context-overflow' | 'context-budget' | 'retention' | 'manual',
|
|
1184
1254
|
reportUsage: TAgentModelCallUsageReporter,
|
|
1185
1255
|
abortSignal?: AbortSignal,
|
|
1186
1256
|
): Promise<IContextCompactionEvent | undefined> {
|
|
@@ -1189,7 +1259,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1189
1259
|
abortSignal?.throwIfAborted();
|
|
1190
1260
|
const unresolvedIntents = getUncertainToolExecutionIntents(this.events);
|
|
1191
1261
|
if (unresolvedIntents.length > 0) {
|
|
1192
|
-
if (reason === 'context-overflow' || reason === 'retention') return undefined;
|
|
1262
|
+
if (reason === 'context-overflow' || reason === 'context-budget' || reason === 'retention') return undefined;
|
|
1193
1263
|
throw new Error('Context compaction is blocked by unresolved tool execution intents.');
|
|
1194
1264
|
}
|
|
1195
1265
|
const openTransactions = getOpenAgentGenerationTransactions(this.events);
|
|
@@ -1199,7 +1269,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1199
1269
|
let coveredEvents: TAgentEvent[];
|
|
1200
1270
|
if (openTransactions.length > 0) {
|
|
1201
1271
|
if (reason === 'retention') return undefined;
|
|
1202
|
-
if (reason !== 'context-overflow') {
|
|
1272
|
+
if (reason !== 'context-overflow' && reason !== 'context-budget') {
|
|
1203
1273
|
throw new Error('Context compaction is blocked while a transactional generation is open.');
|
|
1204
1274
|
}
|
|
1205
1275
|
const openGenerationIds = new Set(
|
|
@@ -1257,6 +1327,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1257
1327
|
.slice(-(this.options.maxArchivedTransactionTombstones ?? 1000));
|
|
1258
1328
|
const compaction = this.commitEvent(createAgentEvent<IContextCompactionEvent>({
|
|
1259
1329
|
type: 'context-compaction',
|
|
1330
|
+
...(reason === 'context-budget' ? { automatic: true as const } : {}),
|
|
1260
1331
|
coveredEventIds: coveredEvents.map((event) => event.id),
|
|
1261
1332
|
replacementEvents: modelMessagesToAgentEvents(replacementMessages),
|
|
1262
1333
|
archivedTransactions: archivedTransactions.length > 0
|
|
@@ -1492,6 +1563,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1492
1563
|
preparationErrors: ReadonlyMap<string, unknown>,
|
|
1493
1564
|
ledger: Map<string, IToolExecutionLedgerEntry>,
|
|
1494
1565
|
getMessages: () => plugins.ModelMessage[],
|
|
1566
|
+
onPreview: (event: IAgentToolCallUpdateEvent) => void,
|
|
1495
1567
|
): plugins.ToolSet {
|
|
1496
1568
|
const wrappedTools: plugins.ToolSet = {};
|
|
1497
1569
|
for (const [toolName, toolDefinition] of Object.entries(tools)) {
|
|
@@ -1551,6 +1623,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1551
1623
|
result,
|
|
1552
1624
|
resolveCompletion,
|
|
1553
1625
|
rejectCompletion,
|
|
1626
|
+
(output) => onPreview({ toolCallId, toolName, input, output }),
|
|
1554
1627
|
);
|
|
1555
1628
|
}
|
|
1556
1629
|
if (isPromiseLike(result)) {
|
|
@@ -1566,6 +1639,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1566
1639
|
deferredIterable,
|
|
1567
1640
|
resolveCompletion,
|
|
1568
1641
|
rejectCompletion,
|
|
1642
|
+
(output) => onPreview({ toolCallId, toolName, input, output }),
|
|
1569
1643
|
);
|
|
1570
1644
|
}
|
|
1571
1645
|
resolveCompletion(result);
|
|
@@ -1590,6 +1664,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1590
1664
|
iterable: AsyncIterable<unknown>,
|
|
1591
1665
|
resolveCompletion: (output: unknown) => void,
|
|
1592
1666
|
rejectCompletion: (error: unknown) => void,
|
|
1667
|
+
onOutput: (output: unknown) => void,
|
|
1593
1668
|
): AsyncIterable<unknown> {
|
|
1594
1669
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
1595
1670
|
let settled = false;
|
|
@@ -1621,7 +1696,11 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1621
1696
|
}
|
|
1622
1697
|
return operationResult.then(
|
|
1623
1698
|
(result) => {
|
|
1624
|
-
if (!result.done && cacheOutput)
|
|
1699
|
+
if (!result.done && cacheOutput) {
|
|
1700
|
+
lastOutput = result.value;
|
|
1701
|
+
try { onOutput(result.value); }
|
|
1702
|
+
catch (error) { settleError(error); throw error; }
|
|
1703
|
+
}
|
|
1625
1704
|
if (result.done) {
|
|
1626
1705
|
if (rejectWhenDone) settleError(incompleteError());
|
|
1627
1706
|
else settleSuccess();
|
|
@@ -1720,6 +1799,9 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1720
1799
|
? this.transactionalStoreCapabilityError()
|
|
1721
1800
|
: undefined;
|
|
1722
1801
|
if (capabilityError) return Promise.reject(capabilityError);
|
|
1802
|
+
if (options.stop && !options.transaction) {
|
|
1803
|
+
return Promise.reject(new Error('A stop signal requires a transactional generation.'));
|
|
1804
|
+
}
|
|
1723
1805
|
const run = this.generationQueue.then(async () => {
|
|
1724
1806
|
if (!options.transaction) options.abort?.throwIfAborted();
|
|
1725
1807
|
if (!options.transaction && this.sessionAbortController.signal.aborted) {
|
|
@@ -1730,6 +1812,9 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1730
1812
|
if (this.maintenanceError !== undefined) throw this.maintenanceError;
|
|
1731
1813
|
await this.runEventRetentionLocked();
|
|
1732
1814
|
return this.generateLocked(options);
|
|
1815
|
+
}).finally(() => {
|
|
1816
|
+
// The generation takes no further input; what it never took in stays with the queue.
|
|
1817
|
+
options.pendingInput?.close();
|
|
1733
1818
|
});
|
|
1734
1819
|
this.generationQueue = run.then(() => undefined, () => undefined);
|
|
1735
1820
|
// A transactional generation is not raced against its abort signal: it settles when its
|
|
@@ -1801,6 +1886,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1801
1886
|
this.sessionAbortController.signal,
|
|
1802
1887
|
generationAbortController.signal,
|
|
1803
1888
|
options.abort,
|
|
1889
|
+
options.stop,
|
|
1804
1890
|
]);
|
|
1805
1891
|
|
|
1806
1892
|
let lease: IAgentGenerationLease | undefined;
|
|
@@ -1842,6 +1928,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1842
1928
|
let attempt = 0;
|
|
1843
1929
|
let retriedMs = 0;
|
|
1844
1930
|
let contextOverflowRetries = 0;
|
|
1931
|
+
let automaticCompactionChecked = false;
|
|
1845
1932
|
let totalInput = 0;
|
|
1846
1933
|
let totalOutput = 0;
|
|
1847
1934
|
let totalCacheRead = 0;
|
|
@@ -1862,6 +1949,8 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1862
1949
|
const reportCompactionUsage: TAgentModelCallUsageReporter = (call) =>
|
|
1863
1950
|
recordModelCallUsage({ ...call, source: 'compaction', generationId });
|
|
1864
1951
|
let currentInferenceId: string | undefined;
|
|
1952
|
+
/** Text the current inference streamed so far, kept if the inference is interrupted. */
|
|
1953
|
+
let inferenceText = '';
|
|
1865
1954
|
let latestInferenceVisibleIds = new Set(this.events.map((event) => event.id));
|
|
1866
1955
|
const completedInferenceIds = new Set<string>();
|
|
1867
1956
|
const reasoningTextById = new Map<string, string>();
|
|
@@ -1873,6 +1962,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1873
1962
|
const ledger = new Map<string, IToolExecutionLedgerEntry>();
|
|
1874
1963
|
const executionIntentIds = new Map<string, string>();
|
|
1875
1964
|
const preliminaryToolOutputs = new Map<string, IAgentToolCallUpdateEvent>();
|
|
1965
|
+
const finishedToolCallIds = new Set<string>();
|
|
1876
1966
|
|
|
1877
1967
|
const beginInference = async (): Promise<string> => {
|
|
1878
1968
|
const marker = this.commitEvent(createAgentEvent<IRuntimeEvent>({
|
|
@@ -1889,10 +1979,90 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1889
1979
|
: undefined,
|
|
1890
1980
|
}));
|
|
1891
1981
|
currentInferenceId = marker.id;
|
|
1982
|
+
inferenceText = '';
|
|
1892
1983
|
latestInferenceVisibleIds = new Set(this.events.map((event) => event.id));
|
|
1893
1984
|
await this.flushPersistence();
|
|
1894
1985
|
return marker.id;
|
|
1895
1986
|
};
|
|
1987
|
+
const applyPendingInput = async (): Promise<void> => {
|
|
1988
|
+
const inputs = options.pendingInput?.drain() ?? [];
|
|
1989
|
+
if (inputs.length === 0) return;
|
|
1990
|
+
const applied: IAgentAppliedInput[] = [];
|
|
1991
|
+
try {
|
|
1992
|
+
for (const input of inputs) {
|
|
1993
|
+
const event = this.commitEvent(createAgentEvent<IUserMessageEvent>({
|
|
1994
|
+
type: 'user-message',
|
|
1995
|
+
content: input.content,
|
|
1996
|
+
inputId: input.inputId,
|
|
1997
|
+
generationId,
|
|
1998
|
+
parentIds: this.events.length > 0
|
|
1999
|
+
? [this.events[this.events.length - 1].id]
|
|
2000
|
+
: undefined,
|
|
2001
|
+
}));
|
|
2002
|
+
applied.push({
|
|
2003
|
+
inputId: input.inputId,
|
|
2004
|
+
eventId: event.id,
|
|
2005
|
+
generationId,
|
|
2006
|
+
completedSteps: stepCount,
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
await this.flushPersistence();
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
// Input that is not durable was not taken in: the queue returns it from close().
|
|
2012
|
+
options.pendingInput!.restore(inputs);
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
// Every durable input is reported, also when reporting an earlier one failed.
|
|
2016
|
+
const callbackErrors: unknown[] = [];
|
|
2017
|
+
for (const input of applied) {
|
|
2018
|
+
try {
|
|
2019
|
+
await this.invokeGenerationCallback(() => options.onInputApplied?.(input));
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
callbackErrors.push(error);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (callbackErrors.length === 1) throw callbackErrors[0];
|
|
2025
|
+
if (callbackErrors.length > 1) {
|
|
2026
|
+
throw new AggregateError(callbackErrors, 'Reporting applied pending input failed.');
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2029
|
+
/**
|
|
2030
|
+
* Keeps what a stopped inference produced: the text it streamed and the tool calls it
|
|
2031
|
+
* issued, as one assistant message of the generation. Tool calls without a result are
|
|
2032
|
+
* closed when the stopped generation is rendered for the model.
|
|
2033
|
+
*/
|
|
2034
|
+
const commitInterruptedInference = async (): Promise<void> => {
|
|
2035
|
+
const inferenceId = currentInferenceId;
|
|
2036
|
+
if (!options.stop?.aborted || !inferenceId || completedInferenceIds.has(inferenceId)) return;
|
|
2037
|
+
completedInferenceIds.add(inferenceId);
|
|
2038
|
+
if (this.events.some((event) =>
|
|
2039
|
+
event.type === 'assistant-message' && event.inferenceId === inferenceId)) return;
|
|
2040
|
+
const toolCallParts: TAgentToolCallPart[] = this.events
|
|
2041
|
+
.filter((event): event is IToolCallEvent =>
|
|
2042
|
+
event.type === 'tool-call'
|
|
2043
|
+
&& event.generationId === generationId
|
|
2044
|
+
&& event.inferenceId === inferenceId)
|
|
2045
|
+
.map((event) => ({
|
|
2046
|
+
type: 'tool-call',
|
|
2047
|
+
toolCallId: event.toolCallId,
|
|
2048
|
+
toolName: event.toolName,
|
|
2049
|
+
input: event.input,
|
|
2050
|
+
...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }),
|
|
2051
|
+
...(event.providerOptions === undefined ? {} : { providerOptions: event.providerOptions }),
|
|
2052
|
+
}));
|
|
2053
|
+
if (!inferenceText && toolCallParts.length === 0) return;
|
|
2054
|
+
this.commitEvent(createAgentEvent<IAssistantMessageEvent>({
|
|
2055
|
+
type: 'assistant-message',
|
|
2056
|
+
content: [
|
|
2057
|
+
...(inferenceText ? [{ type: 'text' as const, text: inferenceText }] : []),
|
|
2058
|
+
...toolCallParts,
|
|
2059
|
+
],
|
|
2060
|
+
generationId,
|
|
2061
|
+
inferenceId,
|
|
2062
|
+
causationId: inferenceId,
|
|
2063
|
+
}));
|
|
2064
|
+
await this.flushPersistence();
|
|
2065
|
+
};
|
|
1896
2066
|
const allTools = this.wrapToolsForGeneration(
|
|
1897
2067
|
generationId,
|
|
1898
2068
|
runtime.tools,
|
|
@@ -1901,6 +2071,13 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1901
2071
|
preparationErrors,
|
|
1902
2072
|
ledger,
|
|
1903
2073
|
() => this.buildContext(this.eventsByIds(latestInferenceVisibleIds)),
|
|
2074
|
+
(event) => {
|
|
2075
|
+
const previous = preliminaryToolOutputs.get(event.toolCallId);
|
|
2076
|
+
if (!previous || !this.platform.isDeepEqual(previous.output, event.output)) {
|
|
2077
|
+
preliminaryToolOutputs.set(event.toolCallId, event);
|
|
2078
|
+
this.options.onToolCallUpdate?.(event);
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
1904
2081
|
);
|
|
1905
2082
|
|
|
1906
2083
|
const flushReasoning = (): void => {
|
|
@@ -1918,6 +2095,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1918
2095
|
let streamError: unknown;
|
|
1919
2096
|
try {
|
|
1920
2097
|
currentInferenceId = undefined;
|
|
2098
|
+
inferenceText = '';
|
|
1921
2099
|
const result = plugins.streamText({
|
|
1922
2100
|
model: runtime.model,
|
|
1923
2101
|
instructions: runtime.system,
|
|
@@ -1929,6 +2107,15 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1929
2107
|
maxRetries: 0,
|
|
1930
2108
|
abortSignal: combinedAbort.signal,
|
|
1931
2109
|
prepareStep: async () => {
|
|
2110
|
+
await applyPendingInput();
|
|
2111
|
+
if (!automaticCompactionChecked && this.options.contextCompactor
|
|
2112
|
+
&& this.options.contextCompactionBytes !== false) {
|
|
2113
|
+
automaticCompactionChecked = true;
|
|
2114
|
+
const bytes = new TextEncoder().encode(JSON.stringify(this.getModelMessages())).byteLength;
|
|
2115
|
+
if (bytes > (this.options.contextCompactionBytes ?? 256 * 1024)) {
|
|
2116
|
+
await this.performCompaction('context-budget', reportCompactionUsage, combinedAbort.signal);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
1932
2119
|
await beginInference();
|
|
1933
2120
|
return { messages: this.buildContext(this.eventsByIds(latestInferenceVisibleIds)) };
|
|
1934
2121
|
},
|
|
@@ -1959,7 +2146,9 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1959
2146
|
const chunkType = String((chunk as { type?: unknown }).type ?? '');
|
|
1960
2147
|
if (chunkType === 'text-delta') {
|
|
1961
2148
|
const textChunk = chunk as { delta?: string; textDelta?: string; text?: string };
|
|
1962
|
-
|
|
2149
|
+
const delta = textChunk.delta ?? textChunk.textDelta ?? textChunk.text ?? '';
|
|
2150
|
+
inferenceText += delta;
|
|
2151
|
+
this.options.onToken?.(delta);
|
|
1963
2152
|
return;
|
|
1964
2153
|
}
|
|
1965
2154
|
if (chunkType === 'tool-result') {
|
|
@@ -1971,6 +2160,10 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
1971
2160
|
output: unknown;
|
|
1972
2161
|
};
|
|
1973
2162
|
if (toolResultChunk.preliminary === true) {
|
|
2163
|
+
// Locally executed previews are delivered at the iterator boundary, not from
|
|
2164
|
+
// this independently backpressured SDK stream, which may arrive after finish.
|
|
2165
|
+
if (ledger.has(toolResultChunk.toolCallId)) return;
|
|
2166
|
+
if (finishedToolCallIds.has(toolResultChunk.toolCallId)) return;
|
|
1974
2167
|
const previous = preliminaryToolOutputs.get(toolResultChunk.toolCallId);
|
|
1975
2168
|
if (!previous || !this.platform.isDeepEqual(previous.output, toolResultChunk.output)) {
|
|
1976
2169
|
const event: IAgentToolCallUpdateEvent = {
|
|
@@ -2102,6 +2295,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
2102
2295
|
return promise;
|
|
2103
2296
|
},
|
|
2104
2297
|
onToolExecutionEnd: async (finish) => {
|
|
2298
|
+
finishedToolCallIds.add(finish.toolCall.toolCallId);
|
|
2105
2299
|
preliminaryToolOutputs.delete(finish.toolCall.toolCallId);
|
|
2106
2300
|
const inferenceId = currentInferenceId ?? await beginInference();
|
|
2107
2301
|
const previousResult = this.currentGenerationEvent(
|
|
@@ -2245,8 +2439,11 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
2245
2439
|
flushReasoning();
|
|
2246
2440
|
const effectiveError = streamError ?? error;
|
|
2247
2441
|
if (combinedAbort.signal.aborted) {
|
|
2442
|
+
await commitInterruptedInference();
|
|
2248
2443
|
throw combinedAbort.signal.reason ?? new DOMException('Aborted', 'AbortError');
|
|
2249
2444
|
}
|
|
2445
|
+
// A failed attempt is retried or fails the generation; its partial text is not kept.
|
|
2446
|
+
inferenceText = '';
|
|
2250
2447
|
if (effectiveError instanceof DOMException && effectiveError.name === 'AbortError') {
|
|
2251
2448
|
throw effectiveError;
|
|
2252
2449
|
}
|
|
@@ -2344,6 +2541,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
2344
2541
|
generationId,
|
|
2345
2542
|
'interrupted',
|
|
2346
2543
|
this.transactionOutcomeErrorReason(effectiveError),
|
|
2544
|
+
options.stop?.aborted === true,
|
|
2347
2545
|
);
|
|
2348
2546
|
} else {
|
|
2349
2547
|
const transaction = this.transaction(generationId);
|
|
@@ -2366,6 +2564,7 @@ export class AgentSessionEngine implements IAgentSession {
|
|
|
2366
2564
|
generationId,
|
|
2367
2565
|
'interrupted',
|
|
2368
2566
|
this.transactionOutcomeErrorReason(abortError),
|
|
2567
|
+
options.stop?.aborted === true,
|
|
2369
2568
|
);
|
|
2370
2569
|
throw abortError;
|
|
2371
2570
|
}
|
package/ts_agent/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
getAgentGenerationTransactions,
|
|
7
7
|
getOpenAgentGenerationTransactions,
|
|
8
8
|
getUncertainToolExecutionIntents,
|
|
9
|
+
isGenerationInModelContext,
|
|
9
10
|
isTransactionalGeneration,
|
|
10
11
|
} from './smartagent.transactions.js';
|
|
11
12
|
export {
|
|
@@ -13,6 +14,15 @@ export {
|
|
|
13
14
|
createAgentEventId,
|
|
14
15
|
modelMessagesToAgentEvents,
|
|
15
16
|
} from './smartagent.events.js';
|
|
17
|
+
export {
|
|
18
|
+
AgentPendingInputQueue,
|
|
19
|
+
AgentPendingInputRejectedError,
|
|
20
|
+
} from './smartagent.pendinginput.js';
|
|
21
|
+
export type {
|
|
22
|
+
IAgentAppliedInput,
|
|
23
|
+
IAgentPendingInput,
|
|
24
|
+
TAgentPendingInputRejection,
|
|
25
|
+
} from './smartagent.pendinginput.js';
|
|
16
26
|
export { ToolRegistry } from './smartagent.classes.toolregistry.js';
|
|
17
27
|
export { AgentModelCallUsageRecorder } from './smartagent.usage.js';
|
|
18
28
|
export type { IAgentModelCallUsageRecorderModel } from './smartagent.usage.js';
|
|
@@ -35,6 +45,7 @@ export type {
|
|
|
35
45
|
IAgentCompactOptions,
|
|
36
46
|
IAgentEventRetentionOptions,
|
|
37
47
|
IAgentBeginGenerationOptions,
|
|
48
|
+
IAgentFinalizeGenerationOptions,
|
|
38
49
|
IAgentGenerateOptions,
|
|
39
50
|
IAgentGenerateResult,
|
|
40
51
|
IAgentGenerationHandle,
|
package/ts_agent/readme.md
CHANGED
|
@@ -274,10 +274,34 @@ export async function createReviewedDraft(
|
|
|
274
274
|
|
|
275
275
|
The `review` callback belongs to your application. While awaiting acceptance, `candidate.messages` includes the candidate, but `getModelMessages()` excludes it from ordinary future context. Acceptance includes the generation; rejection keeps it in the canonical event history while excluding it from future context. Generation IDs identify transactions within a session; use a distinct ID for each new transaction.
|
|
276
276
|
|
|
277
|
+
`generate({ transaction, stop })` takes a second signal for the owner's stop, the way a user stops a turn. It ends the generation like `abort`, but the interrupted outcome records the stop (`stopped: true`, also accepted by `finalizeGeneration(transaction, 'interrupted', { stopped: true })` for a generation stopped after its execution completed). A stopped generation stays in future context once its model produced output: its prompt, the input it took in, the text its running model step streamed and the tool calls it issued. Each tool call without a recorded result is closed with an error result saying its effect is unknown, so the next request is well-formed for every provider. A generation stopped before any model output leaves no trace, so its prompt can be sent again without appearing twice. `isGenerationInModelContext(events, generationId)` applies the same rule to a list of events, for a caller that needs to know whether a finished generation stayed in context. Aborted and failed generations, and generations a restart interrupted, stay out of future context as before. `stop` requires a transactional generation.
|
|
278
|
+
|
|
277
279
|
Closing a session or restoring a stored session interrupts unfinished transactions. Restoration does not automatically rerun them. Persisted transactions require an `IAgentEventStoreV2` store with `eventSchemaVersion: 2`; without a store, transactions are process-local.
|
|
278
280
|
|
|
279
281
|
A rejected generation does not roll back external tool effects. The runtime records transactional tool-execution intents and results. If a crash leaves an intent without a known result, inspect `listUncertainToolExecutions()` and reconcile the external operation through `reconcileToolExecution()` before treating its outcome as known. This is not a general exactly-once guarantee for external services.
|
|
280
282
|
|
|
283
|
+
## Take input while a generation runs
|
|
284
|
+
|
|
285
|
+
Pass an `AgentPendingInputQueue` to `generate({ pendingInput })` to let the user steer a running generation. The generation takes waiting input in at every step boundary, after the tool results of the previous model step are recorded and before the next model call, and records each input as a user message of the generation (`inputId` set). A running tool call is never interrupted. `onInputApplied` is called for each input once it is durable, before the next model call; every durable input is reported, also when reporting an earlier one throws, and the generation then fails with that error. Input the generation could not record durably counts as never taken in: the generation gives it back to the queue with `restore()`.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { AgentPendingInputQueue } from '@modelprofile.com/flexharness-agent';
|
|
289
|
+
|
|
290
|
+
const pendingInput = new AgentPendingInputQueue();
|
|
291
|
+
const transaction = await session.beginGeneration('Plan the release.');
|
|
292
|
+
const generation = session.generate({
|
|
293
|
+
transaction,
|
|
294
|
+
pendingInput,
|
|
295
|
+
onInputApplied: ({ inputId, eventId }) => markApplied(inputId, eventId),
|
|
296
|
+
});
|
|
297
|
+
// While the generation runs:
|
|
298
|
+
pendingInput.add({ inputId: 'steer-1', content: 'Keep the migration out of it.' });
|
|
299
|
+
await generation;
|
|
300
|
+
const unapplied = pendingInput.close(); // input the generation never took in
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The generation closes the queue when it ends, whether it completed, failed or was aborted. `close()` returns the inputs it never took in, the same list on every call once the generation has ended; input that arrives during the last model step is among them, and so is input the generation drained but could not record durably. After closing, `add()` throws `AgentPendingInputRejectedError` with reason `closed`, and a reused `inputId` is refused with reason `duplicate`. The caller decides what becomes of unapplied input, for example sending it as the next turn.
|
|
304
|
+
|
|
281
305
|
## Events, storage and context
|
|
282
306
|
|
|
283
307
|
Canonical events retain user and assistant messages, tool calls and results, runtime events, compaction records and transaction outcomes. `getEvents()` returns this history; `getModelMessages()` projects the model-visible conversation through the configured `contextBuilder`, which defaults to `buildModelMessages`.
|
|
@@ -297,12 +321,15 @@ export function openStoredSession(eventStore: IAgentEventStoreV2, sessionId: str
|
|
|
297
321
|
|
|
298
322
|
Use an application-owned durable adapter for persistent sessions. Its `save(sessionId, events, expectedRevision)` must atomically compare the revision and return the new revision, or reject a conflict. Preserve the schema and supported values in canonical events; the package exports schema validators and `AgentEventStoreConflictError` for adapter implementations. Keep one active owner per session; compare-and-swap detects conflicting writes rather than merging competing sessions.
|
|
299
323
|
|
|
324
|
+
Commits queued before a save starts share one durable snapshot containing them all. An in-flight snapshot is immutable, and every caller and change notification still waits for the save containing its event. The adapter's revision advances per durable snapshot, not necessarily per event.
|
|
325
|
+
|
|
300
326
|
`InMemoryAgentEventStore` is useful for tests and sessions that only need process-local storage. The Node root also exports the explicit `FileAgentEventStore` adapter. Neither storage selection nor a durable backend is implicit.
|
|
301
327
|
|
|
302
328
|
### Bound model context and active events
|
|
303
329
|
|
|
304
330
|
- `contextBuilder({ events })` controls the model-message projection.
|
|
305
331
|
- `contextCompactor(messages, events, { reason, abortSignal, reportUsage })` returns replacement model messages. Provide it to use `session.compact()` or automatic event retention. Report the usage of its model calls through `reportUsage`; see [What is counted](#what-is-counted).
|
|
332
|
+
- A compactor also enables proactive compaction with reason `context-budget` before inference. `contextCompactionBytes` defaults to 256 KiB of UTF-8 JSON model messages, accepts 1 through 32 MiB, and may be `false` to disable it. Only settled history is covered, never the current unsettled generation. Budget compactions carry `automatic: true`; after transactional finalization their covered events are archived when the store supports it and no open transaction or uncertain tool intent remains. Manual compaction is not automatically archived.
|
|
306
333
|
- `eventRetention: { maxEvents }` triggers compaction and archival when the active event count exceeds the threshold. It also requires an event store with `archive()` support.
|
|
307
334
|
- Context overflow invokes the configured compactor, or the `onContextOverflow(messages, { abortSignal, reportUsage })` handler. Without either, generation throws `ContextOverflowError`. `maxContextOverflowRetries` defaults to `3`.
|
|
308
335
|
- Open transactions and uncertain tool intents constrain when compaction and archival can proceed. Resolve them before manual compaction.
|
|
@@ -35,6 +35,8 @@ export interface IUserMessageEvent extends IAgentEventBase {
|
|
|
35
35
|
type: 'user-message';
|
|
36
36
|
content: TAgentUserMessage['content'];
|
|
37
37
|
providerOptions?: TAgentUserMessage['providerOptions'];
|
|
38
|
+
/** Set when a running generation took the message in from its pending input. */
|
|
39
|
+
inputId?: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface IAssistantMessageEvent extends IAgentEventBase {
|
|
@@ -106,6 +108,11 @@ export interface IGenerationOutcomeEvent extends IAgentEventBase {
|
|
|
106
108
|
generationId: string;
|
|
107
109
|
outcome: TAgentGenerationOutcome;
|
|
108
110
|
reason?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Set on an `interrupted` outcome whose owner stopped the generation: what its model produced
|
|
113
|
+
* stays in the model context.
|
|
114
|
+
*/
|
|
115
|
+
stopped?: boolean;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
export interface IToolExecutionIntentEvent extends IAgentEventBase {
|
|
@@ -143,6 +150,8 @@ export type TAgentConversationEvent =
|
|
|
143
150
|
|
|
144
151
|
export interface IContextCompactionEvent extends IAgentEventBase {
|
|
145
152
|
type: 'context-compaction';
|
|
153
|
+
/** A proactive budget compaction whose settled source events may be archived automatically. */
|
|
154
|
+
automatic?: true;
|
|
146
155
|
coveredEventIds: string[];
|
|
147
156
|
replacementEvents: TAgentConversationEvent[];
|
|
148
157
|
archivedTransactions?: IArchivedAgentGenerationTransaction[];
|