@modelprofile.com/flexharness 11.3.1 → 12.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/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.core.d.ts +4 -0
- package/dist_ts/classes.flexharness.core.js +69 -25
- package/dist_ts/classes.flexharness.imports.js +4 -3
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -1
- package/dist_ts/classes.flexharness.reversions.js +43 -29
- package/dist_ts/classes.flexharness.sessions.js +2 -6
- package/dist_ts/classes.stores.js +20 -18
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +24 -2
- package/dist_ts/interfaces.flexharness.d.ts +8 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.projectionchange.d.ts +34 -0
- package/dist_ts/utils.projectionchange.js +135 -0
- package/dist_ts/utils.storecodecs.d.ts +10 -3
- package/dist_ts/utils.storecodecs.js +29 -4
- package/dist_ts/utils.storerules.d.ts +1 -1
- package/dist_ts/utils.storerules.js +1 -1
- package/dist_ts_migration/v3_legacyflexharness.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +5 -4
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +44 -25
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +152 -20
- package/dist_ts_stores_nosqldb/classes.recordstore.js +369 -66
- package/dist_ts_stores_nosqldb/index.d.ts +1 -1
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/dist_ts_stores_nosqldb/plugins.d.ts +1 -1
- package/dist_ts_stores_nosqldb/plugins.js +1 -1
- package/dist_ts_stores_testing/contract.d.ts +33 -0
- package/dist_ts_stores_testing/contract.js +566 -0
- package/dist_ts_stores_testing/fixtures.d.ts +20 -0
- package/dist_ts_stores_testing/fixtures.js +112 -0
- package/dist_ts_stores_testing/index.d.ts +2 -0
- package/dist_ts_stores_testing/index.js +3 -0
- package/dist_ts_stores_testing/plugins.d.ts +5 -0
- package/dist_ts_stores_testing/plugins.js +7 -0
- package/package.json +9 -3
- package/readme.md +6 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.core.ts +74 -24
- package/ts/classes.flexharness.imports.ts +3 -2
- package/ts/classes.flexharness.reversions.ts +53 -28
- package/ts/classes.flexharness.sessions.ts +2 -5
- package/ts/classes.stores.ts +40 -37
- package/ts/index.ts +5 -0
- package/ts/interfaces.flexharness.ts +8 -1
- package/ts/interfaces.ts +27 -2
- package/ts/plugins.ts +12 -7
- package/ts/readme.md +6 -3
- package/ts/utils.projectionchange.ts +190 -0
- package/ts/utils.storecodecs.ts +42 -7
- package/ts/utils.storerules.ts +2 -2
- package/ts_migration/v3_legacyflexharness.ts +11 -6
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +59 -46
- package/ts_stores_nosqldb/classes.recordstore.ts +445 -64
- package/ts_stores_nosqldb/index.ts +1 -0
- package/ts_stores_nosqldb/plugins.ts +4 -3
- package/ts_stores_nosqldb/readme.md +6 -2
- package/ts_stores_testing/contract.ts +776 -0
- package/ts_stores_testing/fixtures.ts +127 -0
- package/ts_stores_testing/index.ts +17 -0
- package/ts_stores_testing/plugins.ts +20 -0
- package/ts_stores_testing/readme.md +62 -0
package/ts/classes.stores.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
IFlexProjectManagementStore,
|
|
15
15
|
IFlexProjectManagementTombstone,
|
|
16
16
|
IFlexProjectManagementWriteContext,
|
|
17
|
-
|
|
17
|
+
IFlexProjectionChange,
|
|
18
18
|
IFlexProjectionStore,
|
|
19
19
|
IFlexResultStore,
|
|
20
20
|
IFlexScopeSnapshot,
|
|
@@ -36,10 +36,15 @@ import {
|
|
|
36
36
|
assertFlexProjectManagementSnapshot,
|
|
37
37
|
assertFlexProjectManagementTombstone,
|
|
38
38
|
} from './utils.projectmanagement.js';
|
|
39
|
+
import {
|
|
40
|
+
createChangedProjectionSnapshot,
|
|
41
|
+
validateProjectionChangeSave,
|
|
42
|
+
} from './utils.projectionchange.js';
|
|
39
43
|
import {
|
|
40
44
|
assertFlexToolJobSnapshot,
|
|
41
|
-
|
|
45
|
+
createChangedAgentEventSnapshot,
|
|
42
46
|
createToolJobSnapshot,
|
|
47
|
+
normalizeAgentEventChange,
|
|
43
48
|
normalizeJsonObjectUndefined,
|
|
44
49
|
validateAgentEventArchive,
|
|
45
50
|
validateAgentEventSnapshot,
|
|
@@ -95,20 +100,17 @@ class InMemoryProjectionStore implements IFlexProjectionStore {
|
|
|
95
100
|
public async save(
|
|
96
101
|
storageKey: string,
|
|
97
102
|
sessionId: string,
|
|
98
|
-
|
|
103
|
+
change: IFlexProjectionChange,
|
|
99
104
|
expectedRevision: number,
|
|
100
105
|
): Promise<void> {
|
|
101
106
|
const key = providerKey(storageKey, sessionId);
|
|
102
|
-
const validated =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(value) => assertFlexProjectionSnapshot(value, sessionId),
|
|
106
|
-
);
|
|
107
|
-
const actualRevision = this.snapshots.get(key)?.revision ?? 0;
|
|
107
|
+
const validated = validateProjectionChangeSave(change, expectedRevision);
|
|
108
|
+
const current = this.snapshots.get(key);
|
|
109
|
+
const actualRevision = current?.revision ?? 0;
|
|
108
110
|
if (actualRevision !== expectedRevision) {
|
|
109
111
|
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
110
112
|
}
|
|
111
|
-
this.snapshots.set(key, validated);
|
|
113
|
+
this.snapshots.set(key, createChangedProjectionSnapshot(current, validated, sessionId));
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
public async deleteSession(storageKey: string, sessionId: string): Promise<void> {
|
|
@@ -203,10 +205,9 @@ export class InMemoryFlexProjectManagementStore implements IFlexProjectManagemen
|
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
class InMemoryAgentEventStore implements plugins.
|
|
207
|
-
|
|
208
|
-
private
|
|
209
|
-
private readonly archives = new Map<string, plugins.IAgentEventArchiveV2>();
|
|
208
|
+
class InMemoryAgentEventStore implements plugins.IAgentEventStore {
|
|
209
|
+
private snapshot: plugins.IAgentEventSnapshot | undefined;
|
|
210
|
+
private readonly archives = new Map<string, plugins.IAgentEventArchive>();
|
|
210
211
|
|
|
211
212
|
constructor(private readonly sessionId: string) {}
|
|
212
213
|
|
|
@@ -218,29 +219,32 @@ class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
public async load(sessionId: string): Promise<plugins.
|
|
222
|
+
public async load(sessionId: string): Promise<plugins.IAgentEventSnapshot | undefined> {
|
|
222
223
|
this.assertSession(sessionId);
|
|
223
224
|
return this.snapshot ? cloneSerializable(this.snapshot) : undefined;
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
public async save(
|
|
227
228
|
sessionId: string,
|
|
228
|
-
|
|
229
|
+
change: plugins.IAgentEventChange,
|
|
229
230
|
expectedRevision: number,
|
|
230
231
|
): Promise<number> {
|
|
231
232
|
this.assertSession(sessionId);
|
|
232
233
|
validateExpectedRevision(expectedRevision);
|
|
233
|
-
const
|
|
234
|
-
validateAgentEventSnapshot(snapshot, sessionId);
|
|
234
|
+
const normalized = normalizeAgentEventChange(change);
|
|
235
235
|
const actualRevision = this.snapshot?.revision ?? 0;
|
|
236
236
|
if (actualRevision !== expectedRevision) {
|
|
237
237
|
throw new plugins.AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision);
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
const snapshot = validateAgentEventSnapshot(
|
|
240
|
+
createChangedAgentEventSnapshot(sessionId, this.snapshot, normalized),
|
|
241
|
+
sessionId,
|
|
242
|
+
);
|
|
243
|
+
this.snapshot = snapshot;
|
|
240
244
|
return snapshot.revision;
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
public async archive(archive: plugins.
|
|
247
|
+
public async archive(archive: plugins.IAgentEventArchive): Promise<void> {
|
|
244
248
|
this.assertSession(archive.sessionId);
|
|
245
249
|
const normalized = normalizeJsonObjectUndefined(archive, '$archive');
|
|
246
250
|
const validated = validateAgentEventArchive(normalized, this.sessionId, archive.archiveId);
|
|
@@ -255,7 +259,7 @@ class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
255
259
|
public async loadArchive(
|
|
256
260
|
sessionId: string,
|
|
257
261
|
archiveId: string,
|
|
258
|
-
): Promise<plugins.
|
|
262
|
+
): Promise<plugins.IAgentEventArchive | undefined> {
|
|
259
263
|
this.assertSession(sessionId);
|
|
260
264
|
validateIdentifier(archiveId, 'archiveId');
|
|
261
265
|
const archive = this.archives.get(archiveId);
|
|
@@ -829,17 +833,14 @@ class JsonProjectionStore implements IFlexProjectionStore {
|
|
|
829
833
|
});
|
|
830
834
|
}
|
|
831
835
|
|
|
836
|
+
/** Applies the change and writes the whole projection again. */
|
|
832
837
|
public async save(
|
|
833
838
|
storageKey: string,
|
|
834
839
|
sessionId: string,
|
|
835
|
-
|
|
840
|
+
change: IFlexProjectionChange,
|
|
836
841
|
expectedRevision: number,
|
|
837
842
|
): Promise<void> {
|
|
838
|
-
const validated =
|
|
839
|
-
snapshot,
|
|
840
|
-
expectedRevision,
|
|
841
|
-
(value) => assertFlexProjectionSnapshot(value, sessionId),
|
|
842
|
-
);
|
|
843
|
+
const validated = validateProjectionChangeSave(change, expectedRevision);
|
|
843
844
|
const filePath = this.root.sessionPath('projections', storageKey, sessionId);
|
|
844
845
|
await JsonFileRoot.inQueue(filePath, async () => {
|
|
845
846
|
const current = await this.root.read(filePath, (value) => {
|
|
@@ -850,7 +851,7 @@ class JsonProjectionStore implements IFlexProjectionStore {
|
|
|
850
851
|
if (actualRevision !== expectedRevision) {
|
|
851
852
|
throw new FlexHarnessStoreConflictError(providerKey(storageKey, sessionId), expectedRevision, actualRevision);
|
|
852
853
|
}
|
|
853
|
-
await this.root.write(filePath, validated);
|
|
854
|
+
await this.root.write(filePath, createChangedProjectionSnapshot(current, validated, sessionId));
|
|
854
855
|
});
|
|
855
856
|
}
|
|
856
857
|
|
|
@@ -1066,9 +1067,8 @@ async function clearJsonAgentEventSession(
|
|
|
1066
1067
|
});
|
|
1067
1068
|
}
|
|
1068
1069
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1070
|
+
/** Stores a session's event log in one file; a save rewrites the whole log. */
|
|
1071
|
+
class JsonAgentEventStore implements plugins.IAgentEventStore {
|
|
1072
1072
|
constructor(
|
|
1073
1073
|
private readonly root: JsonFileRoot,
|
|
1074
1074
|
private readonly storageKey: string,
|
|
@@ -1083,7 +1083,7 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
1083
1083
|
}
|
|
1084
1084
|
}
|
|
1085
1085
|
|
|
1086
|
-
public async load(sessionId: string): Promise<plugins.
|
|
1086
|
+
public async load(sessionId: string): Promise<plugins.IAgentEventSnapshot | undefined> {
|
|
1087
1087
|
this.assertSession(sessionId);
|
|
1088
1088
|
const filePath = this.root.sessionPath('events', this.storageKey, this.sessionId);
|
|
1089
1089
|
return JsonFileRoot.inQueue(this.root.eventQueueKey(this.storageKey, this.sessionId), async () => {
|
|
@@ -1097,13 +1097,12 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
1097
1097
|
|
|
1098
1098
|
public async save(
|
|
1099
1099
|
sessionId: string,
|
|
1100
|
-
|
|
1100
|
+
change: plugins.IAgentEventChange,
|
|
1101
1101
|
expectedRevision: number,
|
|
1102
1102
|
): Promise<number> {
|
|
1103
1103
|
this.assertSession(sessionId);
|
|
1104
1104
|
validateExpectedRevision(expectedRevision);
|
|
1105
|
-
const
|
|
1106
|
-
validateAgentEventSnapshot(snapshot, sessionId);
|
|
1105
|
+
const normalized = normalizeAgentEventChange(change);
|
|
1107
1106
|
const filePath = this.root.sessionPath('events', this.storageKey, this.sessionId);
|
|
1108
1107
|
return JsonFileRoot.inQueue(this.root.eventQueueKey(this.storageKey, this.sessionId), async () => {
|
|
1109
1108
|
const current = await this.root.read(
|
|
@@ -1114,12 +1113,16 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
1114
1113
|
if (actualRevision !== expectedRevision) {
|
|
1115
1114
|
throw new plugins.AgentEventStoreConflictError(sessionId, expectedRevision, actualRevision);
|
|
1116
1115
|
}
|
|
1116
|
+
const snapshot = validateAgentEventSnapshot(
|
|
1117
|
+
createChangedAgentEventSnapshot(sessionId, current, normalized),
|
|
1118
|
+
sessionId,
|
|
1119
|
+
);
|
|
1117
1120
|
await this.root.write(filePath, snapshot);
|
|
1118
1121
|
return snapshot.revision;
|
|
1119
1122
|
});
|
|
1120
1123
|
}
|
|
1121
1124
|
|
|
1122
|
-
public async archive(archive: plugins.
|
|
1125
|
+
public async archive(archive: plugins.IAgentEventArchive): Promise<void> {
|
|
1123
1126
|
this.assertSession(archive.sessionId);
|
|
1124
1127
|
const normalized = normalizeJsonObjectUndefined(archive, '$archive');
|
|
1125
1128
|
const validated = validateAgentEventArchive(normalized, this.sessionId, archive.archiveId);
|
|
@@ -1144,7 +1147,7 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
1144
1147
|
public async loadArchive(
|
|
1145
1148
|
sessionId: string,
|
|
1146
1149
|
archiveId: string,
|
|
1147
|
-
): Promise<plugins.
|
|
1150
|
+
): Promise<plugins.IAgentEventArchive | undefined> {
|
|
1148
1151
|
this.assertSession(sessionId);
|
|
1149
1152
|
const filePath = this.root.archivePath(this.storageKey, this.sessionId, archiveId);
|
|
1150
1153
|
return JsonFileRoot.inQueue(this.root.eventQueueKey(this.storageKey, this.sessionId), async () => {
|
package/ts/index.ts
CHANGED
|
@@ -21,6 +21,11 @@ export {
|
|
|
21
21
|
assertFlexProjectManagementTombstone,
|
|
22
22
|
createEmptyFlexProjectManagementSnapshot,
|
|
23
23
|
} from './utils.projectmanagement.js';
|
|
24
|
+
export {
|
|
25
|
+
applyFlexProjectionChange,
|
|
26
|
+
assertFlexProjectionChange,
|
|
27
|
+
createWholeProjectionChange,
|
|
28
|
+
} from './utils.projectionchange.js';
|
|
24
29
|
export { assertFlexToolJobSnapshot } from './utils.storecodecs.js';
|
|
25
30
|
export {
|
|
26
31
|
FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
|
|
@@ -2,6 +2,7 @@ import type * as plugins from './plugins.js';
|
|
|
2
2
|
import type { FlexHarnessAbortError } from './errors.js';
|
|
3
3
|
import type { FlexToolCatalog } from './classes.toolcatalog.js';
|
|
4
4
|
import type { FlexCodeSandbox } from './classes.codesandbox.js';
|
|
5
|
+
import type { IFlexProjectionLists } from './utils.projectionchange.js';
|
|
5
6
|
import type {
|
|
6
7
|
IFlexAgentContextInvocation,
|
|
7
8
|
IFlexAffectedWorkspace,
|
|
@@ -69,6 +70,12 @@ export interface IStoredSessionState {
|
|
|
69
70
|
pendingReversionReleases: IFlexPendingReversionRelease[];
|
|
70
71
|
projectionSchemaVersion: 1 | 2 | 3;
|
|
71
72
|
projectionBaseline?: TFlexProjectionSnapshot;
|
|
73
|
+
/**
|
|
74
|
+
* The lists as the store holds them, so a save sends only the entries that differ. Entries are
|
|
75
|
+
* frozen from their first save on and replaced rather than edited. Absent while the stored lists
|
|
76
|
+
* are unknown or of an older schema; the next save then sends whole lists.
|
|
77
|
+
*/
|
|
78
|
+
savedProjectionLists?: IFlexProjectionLists;
|
|
72
79
|
projectionReconciliationRequired: boolean;
|
|
73
80
|
projectionRevision: number;
|
|
74
81
|
projectionQueue: Promise<void>;
|
|
@@ -79,7 +86,7 @@ export interface IStoredSessionState {
|
|
|
79
86
|
rememberedPermissionKeys: Set<string>;
|
|
80
87
|
permissionRevision: number;
|
|
81
88
|
permissionQueue: Promise<void>;
|
|
82
|
-
eventStore: plugins.
|
|
89
|
+
eventStore: plugins.IAgentEventStore;
|
|
83
90
|
agentSession: plugins.IAgentSession;
|
|
84
91
|
agentSessionAbortCompleted: boolean;
|
|
85
92
|
agentSessionCloseCompleted: boolean;
|
package/ts/interfaces.ts
CHANGED
|
@@ -1231,6 +1231,27 @@ export interface IFlexProjectionSnapshotV3 {
|
|
|
1231
1231
|
pendingReversionReleases: IFlexPendingReversionRelease[];
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* How a stored list changed: the new list is the stored entries from `start` up to `end`,
|
|
1236
|
+
* followed by `items`. `{ start: 0, end: 0 }` carries the whole list.
|
|
1237
|
+
*/
|
|
1238
|
+
export interface IFlexListChange<TItem> {
|
|
1239
|
+
start: number;
|
|
1240
|
+
end: number;
|
|
1241
|
+
items: TItem[];
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* How a session's projection changed since the store's last save: the current projection with
|
|
1246
|
+
* its two growing lists as list changes. A change applies only to the current schema; to any
|
|
1247
|
+
* other stored projection only whole lists apply.
|
|
1248
|
+
*/
|
|
1249
|
+
export interface IFlexProjectionChange
|
|
1250
|
+
extends Omit<IFlexProjectionSnapshotV3, 'messages' | 'reversionSegments'> {
|
|
1251
|
+
messages: IFlexListChange<IFlexMessage>;
|
|
1252
|
+
reversionSegments: IFlexListChange<IFlexReversionSegment>;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1234
1255
|
export type IFlexProjectionSnapshot =
|
|
1235
1256
|
| IFlexProjectionSnapshotV1
|
|
1236
1257
|
| IFlexProjectionSnapshotV2
|
|
@@ -1436,10 +1457,14 @@ export interface IFlexScopeStore {
|
|
|
1436
1457
|
|
|
1437
1458
|
export interface IFlexProjectionStore {
|
|
1438
1459
|
load(storageKey: string, sessionId: string): Promise<TFlexProjectionSnapshot | undefined>;
|
|
1460
|
+
/**
|
|
1461
|
+
* Applies `change` to the stored projection while its revision is `expectedRevision`; the
|
|
1462
|
+
* change's revision is the next one.
|
|
1463
|
+
*/
|
|
1439
1464
|
save(
|
|
1440
1465
|
storageKey: string,
|
|
1441
1466
|
sessionId: string,
|
|
1442
|
-
|
|
1467
|
+
change: IFlexProjectionChange,
|
|
1443
1468
|
expectedRevision: number,
|
|
1444
1469
|
): Promise<void>;
|
|
1445
1470
|
deleteSession(storageKey: string, sessionId: string): Promise<void>;
|
|
@@ -1460,7 +1485,7 @@ export interface IFlexAgentEventStoreProvider {
|
|
|
1460
1485
|
getStore(
|
|
1461
1486
|
storageKey: string,
|
|
1462
1487
|
sessionId: string,
|
|
1463
|
-
): Promise<plugins.
|
|
1488
|
+
): Promise<plugins.IAgentEventStore> | plugins.IAgentEventStore;
|
|
1464
1489
|
releaseSession?(storageKey: string, sessionId: string): Promise<void> | void;
|
|
1465
1490
|
deleteSession(storageKey: string, sessionId: string): Promise<void>;
|
|
1466
1491
|
}
|
package/ts/plugins.ts
CHANGED
|
@@ -94,6 +94,8 @@ export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> =>
|
|
|
94
94
|
import {
|
|
95
95
|
AgentEventStoreConflictError,
|
|
96
96
|
AgentPendingInputQueue,
|
|
97
|
+
applyAgentEventChange,
|
|
98
|
+
assertAgentEventChange,
|
|
97
99
|
AgentPendingInputRejectedError,
|
|
98
100
|
AgentSession,
|
|
99
101
|
AgentToolCallScheduler,
|
|
@@ -105,14 +107,16 @@ import {
|
|
|
105
107
|
isGenerationInModelContext,
|
|
106
108
|
modelMessagesToAgentEvents,
|
|
107
109
|
tool,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
validateAgentEventArchive,
|
|
111
|
+
validateAgentEventSnapshot,
|
|
110
112
|
z,
|
|
111
113
|
} from '@modelprofile.com/flexharness-agent';
|
|
112
114
|
|
|
113
115
|
export {
|
|
114
116
|
AgentEventStoreConflictError,
|
|
115
117
|
AgentPendingInputQueue,
|
|
118
|
+
applyAgentEventChange,
|
|
119
|
+
assertAgentEventChange,
|
|
116
120
|
AgentPendingInputRejectedError,
|
|
117
121
|
AgentSession,
|
|
118
122
|
AgentToolCallScheduler,
|
|
@@ -124,17 +128,18 @@ export {
|
|
|
124
128
|
isGenerationInModelContext,
|
|
125
129
|
modelMessagesToAgentEvents,
|
|
126
130
|
tool,
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
validateAgentEventArchive,
|
|
132
|
+
validateAgentEventSnapshot,
|
|
129
133
|
z,
|
|
130
134
|
};
|
|
131
135
|
export type {
|
|
132
136
|
IAgentAppliedInput,
|
|
133
137
|
ISmartAiCacheDispatch,
|
|
134
|
-
|
|
138
|
+
IAgentEventArchive,
|
|
139
|
+
IAgentEventChange,
|
|
135
140
|
IAgentContextCompactionOptions,
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
IAgentEventSnapshot,
|
|
142
|
+
IAgentEventStore,
|
|
138
143
|
IAgentGenerateResult,
|
|
139
144
|
IAgentGenerationHandle,
|
|
140
145
|
IAgentGenerationLease,
|
package/ts/readme.md
CHANGED
|
@@ -68,6 +68,7 @@ or provider SDKs.
|
|
|
68
68
|
| `/mcp` | MCP clients, AI SDK tool conversion and `createMcpToolCatalog`, the tool catalog of MCP tools | `@modelcontextprotocol/sdk` |
|
|
69
69
|
| `/migration` | Existing versioned harness migrations | None |
|
|
70
70
|
| `/stores/nosqldb` | `NoSqlFlexHarnessStores`: cross-process CAS stores on `@lossless.org/client/nosqldb` | `@lossless.org/client` |
|
|
71
|
+
| `/stores/testing` | `createFlexStoreContractTests`: the contract every built-in `IFlexHarnessStores` passes, to run against a custom store with any test runner | None |
|
|
71
72
|
|
|
72
73
|
The integrations in the last column use optional peer dependencies. Install them
|
|
73
74
|
only for the entrypoints your project imports, for example:
|
|
@@ -1128,7 +1129,9 @@ Current FlexHarness persistence is separated by trust and lifecycle domain throu
|
|
|
1128
1129
|
|
|
1129
1130
|
`InMemoryFlexHarnessStores` implements all six required domains with revision-based compare-and-swap behavior for tests and ephemeral processes. It is the default when `stores` is omitted. Custom `IFlexHarnessStores` implementations must provide `projectManagement` even when project-management tools are disabled, because deletion cleanup always writes the generation fence.
|
|
1130
1131
|
|
|
1131
|
-
A custom store validates what it loads and saves with the validators the built-in stores apply. Each throws `FlexHarnessStoreFormatError` for a malformed or wrong-schema snapshot: `assertFlexScopeSnapshot()`, `assertFlexProjectionSnapshot(value, sessionId)`, which also requires every message and staged terminal to belong to `sessionId`, `assertFlexPermissionSnapshot()`, `assertFlexProjectManagementRecord()` and `assertFlexToolJobSnapshot()`. Agent event snapshots and archives have `
|
|
1132
|
+
A custom store validates what it loads and saves with the validators the built-in stores apply. Each throws `FlexHarnessStoreFormatError` for a malformed or wrong-schema snapshot: `assertFlexScopeSnapshot()`, `assertFlexProjectionSnapshot(value, sessionId)`, which also requires every message and staged terminal to belong to `sessionId`, `assertFlexPermissionSnapshot()`, `assertFlexProjectManagementRecord()` and `assertFlexToolJobSnapshot()`. Agent event snapshots and archives have `validateAgentEventSnapshot()` and `validateAgentEventArchive()` in `@modelprofile.com/flexharness-agent`.
|
|
1133
|
+
|
|
1134
|
+
Projections and Agent event histories are saved as changes, so a save costs what changed rather than the whole session. `IFlexProjectionStore.save(storageKey, sessionId, change, expectedRevision)` receives an `IFlexProjectionChange`: the schema-3 projection with `messages` and `reversionSegments` given as `IFlexListChange`s `{ start, end, items }`, each of which keeps the stored entries from `start` up to `end` and appends `items`. A list change with `start` and `end` at 0 replaces the list whole; FlexHarness sends whole lists when it cannot know which entries the store holds: for a projection it loaded in an older schema and after a save with an uncertain outcome. The Agent event store of each session receives an `IAgentEventChange` of removed, replaced and appended events, as the `@modelprofile.com/flexharness-agent` readme describes. A store that keeps snapshots checks a change's form with `assertFlexProjectionChange()` or `assertAgentEventChange()`, applies it with `applyFlexProjectionChange()` or `applyAgentEventChange()`, and validates the result as above; `createWholeProjectionChange(snapshot)` makes the change that stores a projection whole. FlexHarness freezes every message and reversion segment it saved and replaces an entry instead of editing it, which is how it tells the entries the store already holds. `/stores/testing` exports `createFlexStoreContractTests(backend)`, the cases every built-in store passes, for a custom store to run with any test runner; its readme shows how.
|
|
1132
1135
|
|
|
1133
1136
|
Custom Agent event and job providers may implement `releaseSession(storageKey, sessionId)` to release session-bound wrappers, handles, or caches without deleting durable data. FlexHarness calls these hooks only after the corresponding AgentSession or execution context has released runtime ownership. A failed release remains owned for a later retirement or disposal retry. `deleteSession()` and `deleteSessionGenerationCohort()` are the separate destructive operations for durable session data.
|
|
1134
1137
|
|
|
@@ -1145,13 +1148,13 @@ After every harness using a `JsonFileFlexHarnessStores` instance has been dispos
|
|
|
1145
1148
|
|
|
1146
1149
|
The JSON stores are explicitly not cross-process safe. When several processes can access the same storage namespace, every core `IFlexHarnessStores` domain and `stores.projectManagement` must use database-backed or equivalent cross-process CAS. Process-local CAS for the core stores or for PM alone is insufficient: session generation creation, cleanup tombstones, PM replacement, and stale-writer rejection must all retain their respective atomic preconditions across processes.
|
|
1147
1150
|
|
|
1148
|
-
`NoSqlFlexHarnessStores` from `/stores/nosqldb` is that database-backed bundle for every domain. Pass it an initialized `SmartdataDb` and `await stores.prepare()` at startup, before the first harness uses it; the subpath readme describes chunked snapshots and failure reporting.
|
|
1151
|
+
`NoSqlFlexHarnessStores` from `/stores/nosqldb` is that database-backed bundle for every domain. Pass it an initialized `SmartdataDb` and `await stores.prepare()` at startup, before the first harness uses it; the subpath readme describes chunked snapshots, change logs and failure reporting.
|
|
1149
1152
|
|
|
1150
1153
|
Direct store operations and non-run session mutations surface conflicts as `FlexHarnessStoreConflictError` or the corresponding AgentSession store conflict. Malformed, wrong-schema, or non-JSON snapshots are surfaced as `FlexHarnessStoreFormatError`. A write or deletion that changed its target but cannot confirm parent-directory durability surfaces `FlexHarnessStoreCommitUncertainError` with the affected path, operation, and cause. Run persistence failures cross the external error boundary and therefore become `FlexHarnessExternalError`. FlexHarness does not merge conflicts.
|
|
1151
1154
|
|
|
1152
1155
|
Each domain serializes its own mutations. A successful run first persists a hidden completed projection, then finalizes the canonical Agent generation as `accepted`, then promotes the hidden projection publicly. Recovery uses the canonical generation outcome to promote an accepted stage or publish a failed/cancelled projection. Failed and cancelled generations remain auditable but never enter future model context.
|
|
1153
1156
|
|
|
1154
|
-
Projection stores accept schema version 1, 2, or 3 from `load()`, but every `save()` receives the current schema-3 shape. Schema 3 records explicit reversion protocol, transcript/workspace provenance, and V2 disposition. A terminal V2 segment must be conclusively `revertible`, `no-change`, or `nonrevertible`; a pending V2 capture remains owned by its capture WAL and is never treated as transcript history.
|
|
1157
|
+
Projection stores accept schema version 1, 2, or 3 from `load()`, but every `save()` receives a change that makes the current schema-3 shape. A change to a loaded schema-1 or schema-2 projection carries both lists whole. Schema 3 records explicit reversion protocol, transcript/workspace provenance, and V2 disposition. A terminal V2 segment must be conclusively `revertible`, `no-change`, or `nonrevertible`; a pending V2 capture remains owned by its capture WAL and is never treated as transcript history.
|
|
1155
1158
|
|
|
1156
1159
|
Current schema-3 projections also persist optional `metrics` state. Older strict readers that do not recognize this key, or the token breakdown keys inside it (`reportedLifetimeInputTokens`, `reportedLifetimeCacheReadTokens`, `reportedLifetimeCacheWriteTokens`, `lifetimeTokenBreakdownComplete`), cannot reopen a projection after they have been written; downgrade or mixed-version access to that session is unsupported.
|
|
1157
1160
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { FlexHarnessStoreFormatError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFlexListChange,
|
|
4
|
+
IFlexMessage,
|
|
5
|
+
IFlexProjectionChange,
|
|
6
|
+
IFlexProjectionSnapshotCurrent,
|
|
7
|
+
IFlexReversionSegment,
|
|
8
|
+
TFlexProjectionSnapshot,
|
|
9
|
+
} from './interfaces.js';
|
|
10
|
+
import {
|
|
11
|
+
assertFlexProjectionSnapshot,
|
|
12
|
+
assertJsonSerializable,
|
|
13
|
+
cloneSerializable,
|
|
14
|
+
deepFreeze,
|
|
15
|
+
} from './utils.json.js';
|
|
16
|
+
import { assertExactKeys, assertRecord } from './utils.storecodecs.js';
|
|
17
|
+
import { validateExpectedRevision } from './utils.storerules.js';
|
|
18
|
+
|
|
19
|
+
const projectionChangeKeys = [
|
|
20
|
+
'schemaVersion',
|
|
21
|
+
'revision',
|
|
22
|
+
'metrics',
|
|
23
|
+
'messages',
|
|
24
|
+
'stagedTerminals',
|
|
25
|
+
'reversionSegments',
|
|
26
|
+
'revertCursor',
|
|
27
|
+
'excludedRunIds',
|
|
28
|
+
'pendingReversion',
|
|
29
|
+
'pendingReversionReleases',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function assertListChange(value: unknown, path: string): void {
|
|
33
|
+
assertRecord(value, path);
|
|
34
|
+
assertExactKeys(value, ['start', 'end', 'items'], path);
|
|
35
|
+
if (!Number.isSafeInteger(value.start) || Number(value.start) < 0) {
|
|
36
|
+
throw new FlexHarnessStoreFormatError(`${path}.start must be a non-negative integer.`);
|
|
37
|
+
}
|
|
38
|
+
if (!Number.isSafeInteger(value.end) || Number(value.end) < Number(value.start)) {
|
|
39
|
+
throw new FlexHarnessStoreFormatError(`${path}.end must be an integer from start.`);
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(value.items)) {
|
|
42
|
+
throw new FlexHarnessStoreFormatError(`${path}.items must be an array.`);
|
|
43
|
+
}
|
|
44
|
+
for (let index = 0; index < value.items.length; index++) {
|
|
45
|
+
assertRecord(value.items[index], `${path}.items[${index}]`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks the form of a projection change: its fields, a positive revision and two list changes
|
|
51
|
+
* of records. Whether its content makes a valid projection is decided on the snapshot it
|
|
52
|
+
* produces, by `assertFlexProjectionSnapshot`.
|
|
53
|
+
*/
|
|
54
|
+
export function assertFlexProjectionChange(value: unknown): asserts value is IFlexProjectionChange {
|
|
55
|
+
assertJsonSerializable(value, '$change');
|
|
56
|
+
assertRecord(value, '$change');
|
|
57
|
+
assertExactKeys(value, projectionChangeKeys, '$change');
|
|
58
|
+
if (value.schemaVersion !== 3) {
|
|
59
|
+
throw new FlexHarnessStoreFormatError('Projection change schemaVersion must be 3.');
|
|
60
|
+
}
|
|
61
|
+
if (!Number.isSafeInteger(value.revision) || Number(value.revision) < 1) {
|
|
62
|
+
throw new FlexHarnessStoreFormatError('$change.revision must be a positive integer.');
|
|
63
|
+
}
|
|
64
|
+
assertListChange(value.messages, '$change.messages');
|
|
65
|
+
assertListChange(value.reversionSegments, '$change.reversionSegments');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function applyListChange<TItem>(
|
|
69
|
+
stored: readonly TItem[],
|
|
70
|
+
change: IFlexListChange<TItem>,
|
|
71
|
+
path: string,
|
|
72
|
+
): TItem[] {
|
|
73
|
+
if (change.end > stored.length) {
|
|
74
|
+
throw new FlexHarnessStoreFormatError(
|
|
75
|
+
`${path} keeps entries up to ${change.end}, but the store holds ${stored.length}.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const next = stored.slice(change.start, change.end);
|
|
79
|
+
for (const item of change.items) next.push(item);
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Applies `change` to a stored projection and returns the new snapshot; neither input is
|
|
85
|
+
* modified. A stored projection of an older schema offers no entries to keep, so only whole
|
|
86
|
+
* lists apply to it. It rejects a change that keeps entries the stored lists lack; the change's
|
|
87
|
+
* form is `assertFlexProjectionChange`'s to check and the result's content
|
|
88
|
+
* `assertFlexProjectionSnapshot`'s.
|
|
89
|
+
*/
|
|
90
|
+
export function applyFlexProjectionChange(
|
|
91
|
+
stored: TFlexProjectionSnapshot | undefined,
|
|
92
|
+
change: IFlexProjectionChange,
|
|
93
|
+
): IFlexProjectionSnapshotCurrent {
|
|
94
|
+
const lists = stored?.schemaVersion === 3 ? stored : undefined;
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: 3,
|
|
97
|
+
revision: change.revision,
|
|
98
|
+
...(change.metrics === undefined ? {} : { metrics: change.metrics }),
|
|
99
|
+
messages: applyListChange(lists?.messages ?? [], change.messages, '$change.messages'),
|
|
100
|
+
stagedTerminals: change.stagedTerminals,
|
|
101
|
+
reversionSegments: applyListChange(
|
|
102
|
+
lists?.reversionSegments ?? [],
|
|
103
|
+
change.reversionSegments,
|
|
104
|
+
'$change.reversionSegments',
|
|
105
|
+
),
|
|
106
|
+
revertCursor: change.revertCursor,
|
|
107
|
+
excludedRunIds: change.excludedRunIds,
|
|
108
|
+
...(change.pendingReversion === undefined ? {} : { pendingReversion: change.pendingReversion }),
|
|
109
|
+
pendingReversionReleases: change.pendingReversionReleases,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A copy of a change a store was asked to save at `expectedRevision`, after checking its form. */
|
|
114
|
+
export function validateProjectionChangeSave(
|
|
115
|
+
change: IFlexProjectionChange,
|
|
116
|
+
expectedRevision: number,
|
|
117
|
+
): IFlexProjectionChange {
|
|
118
|
+
assertFlexProjectionChange(change);
|
|
119
|
+
validateExpectedRevision(expectedRevision);
|
|
120
|
+
if (change.revision !== expectedRevision + 1) {
|
|
121
|
+
throw new FlexHarnessStoreFormatError(
|
|
122
|
+
`Change revision ${change.revision} must equal expected revision ${expectedRevision} plus one.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return cloneSerializable(change);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The snapshot `change` makes of a stored projection, validated in full for `sessionId`. */
|
|
129
|
+
export function createChangedProjectionSnapshot(
|
|
130
|
+
stored: TFlexProjectionSnapshot | undefined,
|
|
131
|
+
change: IFlexProjectionChange,
|
|
132
|
+
sessionId: string,
|
|
133
|
+
): IFlexProjectionSnapshotCurrent {
|
|
134
|
+
const snapshot = applyFlexProjectionChange(stored, change);
|
|
135
|
+
assertFlexProjectionSnapshot(snapshot, sessionId);
|
|
136
|
+
return snapshot;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A projection's message and reversion segment lists, entry by entry. */
|
|
140
|
+
export interface IFlexProjectionLists {
|
|
141
|
+
messages: readonly IFlexMessage[];
|
|
142
|
+
reversionSegments: readonly IFlexReversionSegment[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Freezes the lists' entries, which a change refers to by identity once they are saved. */
|
|
146
|
+
export function freezeProjectionEntries(lists: IFlexProjectionLists): void {
|
|
147
|
+
for (const message of lists.messages) deepFreeze(message);
|
|
148
|
+
for (const segment of lists.reversionSegments) deepFreeze(segment);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The list change that turns `saved` into `current`. It keeps the run of entries the two share by
|
|
153
|
+
* identity, starting where `current` starts, and sends the rest; without such a run it sends the
|
|
154
|
+
* whole list.
|
|
155
|
+
*/
|
|
156
|
+
function createListChange<TItem>(
|
|
157
|
+
saved: readonly TItem[] | undefined,
|
|
158
|
+
current: readonly TItem[],
|
|
159
|
+
): IFlexListChange<TItem> {
|
|
160
|
+
const start = saved === undefined || current.length === 0 ? -1 : saved.indexOf(current[0]);
|
|
161
|
+
if (saved === undefined || start < 0) return { start: 0, end: 0, items: current.slice() };
|
|
162
|
+
let kept = 0;
|
|
163
|
+
while (start + kept < saved.length && kept < current.length && saved[start + kept] === current[kept]) {
|
|
164
|
+
kept++;
|
|
165
|
+
}
|
|
166
|
+
return { start, end: start + kept, items: current.slice(kept) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The change that turns the stored projection into `snapshot`, given the lists the store holds
|
|
171
|
+
* (`saved`). Entries are compared by identity, so they must be replaced, never edited, once
|
|
172
|
+
* saved. Without `saved` both lists are sent whole.
|
|
173
|
+
*/
|
|
174
|
+
export function createProjectionChange(
|
|
175
|
+
saved: IFlexProjectionLists | undefined,
|
|
176
|
+
snapshot: IFlexProjectionSnapshotCurrent,
|
|
177
|
+
): IFlexProjectionChange {
|
|
178
|
+
return {
|
|
179
|
+
...snapshot,
|
|
180
|
+
messages: createListChange(saved?.messages, snapshot.messages),
|
|
181
|
+
reversionSegments: createListChange(saved?.reversionSegments, snapshot.reversionSegments),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The change that stores `snapshot` whole. */
|
|
186
|
+
export function createWholeProjectionChange(
|
|
187
|
+
snapshot: IFlexProjectionSnapshotCurrent,
|
|
188
|
+
): IFlexProjectionChange {
|
|
189
|
+
return createProjectionChange(undefined, snapshot);
|
|
190
|
+
}
|
package/ts/utils.storecodecs.ts
CHANGED
|
@@ -96,25 +96,25 @@ export function assertRecord(value: unknown, path: string): asserts value is Rec
|
|
|
96
96
|
|
|
97
97
|
export function createAgentEventSnapshot(
|
|
98
98
|
sessionId: string,
|
|
99
|
-
events:
|
|
99
|
+
events: plugins.TAgentEvent[],
|
|
100
100
|
revision: number,
|
|
101
|
-
): plugins.
|
|
101
|
+
): plugins.IAgentEventSnapshot {
|
|
102
102
|
return {
|
|
103
103
|
schemaVersion: 2,
|
|
104
104
|
sessionId,
|
|
105
105
|
revision,
|
|
106
106
|
updatedAt: events.reduce((latest, event) => Math.max(latest, event.timestamp), 0),
|
|
107
|
-
events
|
|
107
|
+
events,
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export function validateAgentEventSnapshot(
|
|
112
112
|
value: unknown,
|
|
113
113
|
sessionId: string,
|
|
114
|
-
): plugins.
|
|
114
|
+
): plugins.IAgentEventSnapshot {
|
|
115
115
|
assertJsonSerializable(value, '$snapshot');
|
|
116
116
|
try {
|
|
117
|
-
return plugins.
|
|
117
|
+
return plugins.validateAgentEventSnapshot(value, sessionId);
|
|
118
118
|
} catch (error) {
|
|
119
119
|
throw new FlexHarnessStoreFormatError(
|
|
120
120
|
`$snapshot is not a canonical schema-2 Agent event snapshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -127,10 +127,10 @@ export function validateAgentEventArchive(
|
|
|
127
127
|
value: unknown,
|
|
128
128
|
sessionId: string,
|
|
129
129
|
archiveId: string,
|
|
130
|
-
): plugins.
|
|
130
|
+
): plugins.IAgentEventArchive {
|
|
131
131
|
assertJsonSerializable(value, '$archive');
|
|
132
132
|
try {
|
|
133
|
-
return plugins.
|
|
133
|
+
return plugins.validateAgentEventArchive(value, sessionId, archiveId);
|
|
134
134
|
} catch (error) {
|
|
135
135
|
throw new FlexHarnessStoreFormatError(
|
|
136
136
|
`$archive is not a canonical schema-2 Agent event archive: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -139,6 +139,41 @@ export function validateAgentEventArchive(
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** A JSON copy of `change`, checked by `assertAgentEventChange`. */
|
|
143
|
+
export function normalizeAgentEventChange(change: unknown): plugins.IAgentEventChange {
|
|
144
|
+
const normalized = normalizeJsonObjectUndefined(change, '$change');
|
|
145
|
+
try {
|
|
146
|
+
plugins.assertAgentEventChange(normalized);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new FlexHarnessStoreFormatError(
|
|
149
|
+
`$change is not a valid Agent event change: ${error instanceof Error ? error.message : String(error)}`,
|
|
150
|
+
{ cause: error },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The next snapshot of `current` after a normalized `change`; the store validates it before it
|
|
158
|
+
* keeps it.
|
|
159
|
+
*/
|
|
160
|
+
export function createChangedAgentEventSnapshot(
|
|
161
|
+
sessionId: string,
|
|
162
|
+
current: plugins.IAgentEventSnapshot | undefined,
|
|
163
|
+
change: plugins.IAgentEventChange,
|
|
164
|
+
): plugins.IAgentEventSnapshot {
|
|
165
|
+
let events: plugins.TAgentEvent[];
|
|
166
|
+
try {
|
|
167
|
+
events = plugins.applyAgentEventChange(current?.events ?? [], change);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new FlexHarnessStoreFormatError(
|
|
170
|
+
`$change does not apply to the stored Agent event log: ${error instanceof Error ? error.message : String(error)}`,
|
|
171
|
+
{ cause: error },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return createAgentEventSnapshot(sessionId, events, (current?.revision ?? 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
|
|
142
177
|
export function assertFlexToolJobSnapshot(value: unknown): asserts value is plugins.IToolJobSnapshot {
|
|
143
178
|
assertJsonSerializable(value, '$snapshot');
|
|
144
179
|
assertRecord(value, '$snapshot');
|