@modelprofile.com/flexharness 10.0.0 → 10.1.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.stores.js +11 -119
- package/dist_ts/errors.d.ts +7 -2
- package/dist_ts/errors.js +1 -1
- package/dist_ts/utils.storerules.d.ts +28 -0
- package/dist_ts/utils.storerules.js +107 -0
- package/dist_ts_chat_harness/classes.transcript.d.ts +88 -0
- package/dist_ts_chat_harness/classes.transcript.js +268 -0
- package/dist_ts_chat_harness/functions.intents.d.ts +62 -0
- package/dist_ts_chat_harness/functions.intents.js +96 -0
- package/dist_ts_chat_harness/functions.mapping.d.ts +33 -0
- package/dist_ts_chat_harness/functions.mapping.js +220 -0
- package/dist_ts_chat_harness/index.d.ts +3 -0
- package/dist_ts_chat_harness/index.js +4 -0
- package/dist_ts_chat_harness/plugins.d.ts +1 -0
- package/dist_ts_chat_harness/plugins.js +2 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +31 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +312 -0
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +110 -0
- package/dist_ts_stores_nosqldb/classes.recordstore.js +437 -0
- package/dist_ts_stores_nosqldb/index.d.ts +2 -0
- package/dist_ts_stores_nosqldb/index.js +3 -0
- package/dist_ts_stores_nosqldb/plugins.d.ts +7 -0
- package/dist_ts_stores_nosqldb/plugins.js +10 -0
- package/package.json +24 -4
- package/readme.md +4 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.stores.ts +28 -172
- package/ts/errors.ts +8 -2
- package/ts/readme.md +4 -0
- package/ts/utils.storerules.ts +177 -0
- package/ts_chat_harness/classes.transcript.ts +365 -0
- package/ts_chat_harness/functions.intents.ts +169 -0
- package/ts_chat_harness/functions.mapping.ts +267 -0
- package/ts_chat_harness/index.ts +33 -0
- package/ts_chat_harness/plugins.ts +18 -0
- package/ts_chat_harness/readme.md +114 -0
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +465 -0
- package/ts_stores_nosqldb/classes.recordstore.ts +560 -0
- package/ts_stores_nosqldb/index.ts +9 -0
- package/ts_stores_nosqldb/plugins.ts +38 -0
- package/ts_stores_nosqldb/readme.md +53 -0
package/ts/classes.stores.ts
CHANGED
|
@@ -35,8 +35,6 @@ import {
|
|
|
35
35
|
assertFlexProjectManagementTombstone,
|
|
36
36
|
} from './utils.projectmanagement.js';
|
|
37
37
|
import {
|
|
38
|
-
assertExactKeys,
|
|
39
|
-
assertRecord,
|
|
40
38
|
createAgentEventSnapshot,
|
|
41
39
|
createToolJobSnapshot,
|
|
42
40
|
normalizeJsonObjectUndefined,
|
|
@@ -44,101 +42,17 @@ import {
|
|
|
44
42
|
validateAgentEventSnapshot,
|
|
45
43
|
validateToolJobSnapshot,
|
|
46
44
|
} from './utils.storecodecs.js';
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (typeof value !== 'string' || !value.trim()) {
|
|
59
|
-
throw new FlexHarnessValidationError(`${name} must be a non-empty string.`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function validateExpectedRevision(expectedRevision: number): void {
|
|
64
|
-
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
|
|
65
|
-
throw new FlexHarnessValidationError('expectedRevision must be a non-negative integer.');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function validateProjectManagementWriteContext(
|
|
70
|
-
writeContext: IFlexProjectManagementWriteContext,
|
|
71
|
-
): void {
|
|
72
|
-
assertJsonSerializable(writeContext, '$writeContext');
|
|
73
|
-
assertRecord(writeContext, 'writeContext');
|
|
74
|
-
assertExactKeys(writeContext, ['actor', 'runId', 'agent', 'toolCallId'], 'writeContext');
|
|
75
|
-
if (writeContext.actor !== 'agent' && writeContext.actor !== 'application') {
|
|
76
|
-
throw new FlexHarnessValidationError('writeContext.actor must be agent or application.');
|
|
77
|
-
}
|
|
78
|
-
for (const key of ['runId', 'agent', 'toolCallId'] as const) {
|
|
79
|
-
if (writeContext[key] !== undefined) validateIdentifier(writeContext[key], `writeContext.${key}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function validateSnapshotSave<TSnapshotValue extends TSnapshot>(
|
|
84
|
-
snapshot: TSnapshotValue,
|
|
85
|
-
expectedRevision: number,
|
|
86
|
-
validator: TSnapshotValidator<TSnapshotValue>,
|
|
87
|
-
): TSnapshotValue {
|
|
88
|
-
validator(snapshot);
|
|
89
|
-
validateExpectedRevision(expectedRevision);
|
|
90
|
-
if (snapshot.revision !== expectedRevision + 1) {
|
|
91
|
-
throw new FlexHarnessStoreFormatError(
|
|
92
|
-
`Snapshot revision ${snapshot.revision} must equal expected revision ${expectedRevision} plus one.`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
return cloneSerializable(snapshot);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function providerKey(storageKey: string, sessionId: string): string {
|
|
99
|
-
validateIdentifier(storageKey, 'storageKey');
|
|
100
|
-
validateIdentifier(sessionId, 'sessionId');
|
|
101
|
-
return JSON.stringify([storageKey, sessionId]);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function sameProjectManagementGeneration(
|
|
105
|
-
left: TFlexProjectManagementRecord,
|
|
106
|
-
right: TFlexProjectManagementRecord,
|
|
107
|
-
): boolean {
|
|
108
|
-
return left.sessionGenerationId === right.sessionGenerationId
|
|
109
|
-
&& left.sessionGenerationSequence === right.sessionGenerationSequence;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function canInitializeProjectManagementGeneration(
|
|
113
|
-
current: TFlexProjectManagementRecord,
|
|
114
|
-
next: TFlexProjectManagementRecord,
|
|
115
|
-
expectedRevision: number,
|
|
116
|
-
): boolean {
|
|
117
|
-
return 'deletedAt' in current
|
|
118
|
-
&& expectedRevision === 0
|
|
119
|
-
&& next.sessionGenerationId !== current.sessionGenerationId
|
|
120
|
-
&& next.sessionGenerationSequence > current.sessionGenerationSequence;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function assertProjectionSession(snapshot: TFlexProjectionSnapshot, sessionId: string): void {
|
|
124
|
-
for (const message of snapshot.messages) {
|
|
125
|
-
if (message.sessionId !== sessionId) {
|
|
126
|
-
throw new FlexHarnessStoreFormatError(
|
|
127
|
-
`Projection message "${message.messageId}" does not belong to session "${sessionId}".`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
for (const terminal of snapshot.stagedTerminals) {
|
|
132
|
-
if (
|
|
133
|
-
terminal.userMessage.sessionId !== sessionId
|
|
134
|
-
|| terminal.assistantMessage.sessionId !== sessionId
|
|
135
|
-
) {
|
|
136
|
-
throw new FlexHarnessStoreFormatError(
|
|
137
|
-
`Staged terminal "${terminal.runId}" does not belong to session "${sessionId}".`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
45
|
+
import {
|
|
46
|
+
assertProjectionSession,
|
|
47
|
+
assertProjectManagementSaveAdmitted,
|
|
48
|
+
assertSameArchiveEvents,
|
|
49
|
+
isProjectManagementTombstoneAdmitted,
|
|
50
|
+
providerKey,
|
|
51
|
+
validateExpectedRevision,
|
|
52
|
+
validateIdentifier,
|
|
53
|
+
validateProjectManagementWriteContext,
|
|
54
|
+
validateSnapshotSave,
|
|
55
|
+
} from './utils.storerules.js';
|
|
142
56
|
|
|
143
57
|
class InMemoryScopeStore implements IFlexScopeStore {
|
|
144
58
|
private readonly snapshots = new Map<string, IFlexScopeSnapshot>();
|
|
@@ -253,15 +167,7 @@ export class InMemoryFlexProjectManagementStore implements IFlexProjectManagemen
|
|
|
253
167
|
expectedRevision,
|
|
254
168
|
assertFlexProjectManagementSnapshot,
|
|
255
169
|
);
|
|
256
|
-
|
|
257
|
-
const actualRevision = current?.revision ?? 0;
|
|
258
|
-
if (current && !sameProjectManagementGeneration(current, validated)) {
|
|
259
|
-
if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
|
|
260
|
-
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
261
|
-
}
|
|
262
|
-
} else if ((current && 'deletedAt' in current) || actualRevision !== expectedRevision) {
|
|
263
|
-
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
264
|
-
}
|
|
170
|
+
assertProjectManagementSaveAdmitted(key, this.snapshots.get(key), validated, expectedRevision);
|
|
265
171
|
this.snapshots.set(key, validated);
|
|
266
172
|
}
|
|
267
173
|
|
|
@@ -277,19 +183,8 @@ export class InMemoryFlexProjectManagementStore implements IFlexProjectManagemen
|
|
|
277
183
|
expectedRevision,
|
|
278
184
|
assertFlexProjectManagementTombstone,
|
|
279
185
|
);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (current && sameProjectManagementGeneration(current, validated)) {
|
|
283
|
-
if ('deletedAt' in current) return;
|
|
284
|
-
if (actualRevision !== expectedRevision) {
|
|
285
|
-
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
286
|
-
}
|
|
287
|
-
} else if (current) {
|
|
288
|
-
if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
|
|
289
|
-
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
290
|
-
}
|
|
291
|
-
} else if (actualRevision !== expectedRevision) {
|
|
292
|
-
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
186
|
+
if (!isProjectManagementTombstoneAdmitted(key, this.snapshots.get(key), validated, expectedRevision)) {
|
|
187
|
+
return;
|
|
293
188
|
}
|
|
294
189
|
this.snapshots.set(key, validated);
|
|
295
190
|
}
|
|
@@ -346,12 +241,7 @@ class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
346
241
|
const validated = validateAgentEventArchive(normalized, this.sessionId, archive.archiveId);
|
|
347
242
|
const existing = this.archives.get(archive.archiveId);
|
|
348
243
|
if (existing) {
|
|
349
|
-
|
|
350
|
-
JSON.stringify(existing.events.map((event) => event.id))
|
|
351
|
-
!== JSON.stringify(validated.events.map((event) => event.id))
|
|
352
|
-
) {
|
|
353
|
-
throw new Error(`Archive "${archive.archiveId}" already exists with different events.`);
|
|
354
|
-
}
|
|
244
|
+
assertSameArchiveEvents(existing, validated);
|
|
355
245
|
return;
|
|
356
246
|
}
|
|
357
247
|
this.archives.set(archive.archiveId, cloneSerializable(validated));
|
|
@@ -1007,22 +897,12 @@ class JsonProjectManagementStore implements IFlexProjectManagementStore {
|
|
|
1007
897
|
assertFlexProjectManagementRecord(value);
|
|
1008
898
|
return value;
|
|
1009
899
|
});
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
actualRevision,
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
} else if ((current && 'deletedAt' in current) || actualRevision !== expectedRevision) {
|
|
1020
|
-
throw new FlexHarnessStoreConflictError(
|
|
1021
|
-
providerKey(storageKey, sessionId),
|
|
1022
|
-
expectedRevision,
|
|
1023
|
-
actualRevision,
|
|
1024
|
-
);
|
|
1025
|
-
}
|
|
900
|
+
assertProjectManagementSaveAdmitted(
|
|
901
|
+
providerKey(storageKey, sessionId),
|
|
902
|
+
current,
|
|
903
|
+
validated,
|
|
904
|
+
expectedRevision,
|
|
905
|
+
);
|
|
1026
906
|
await this.root.write(filePath, validated);
|
|
1027
907
|
}),
|
|
1028
908
|
);
|
|
@@ -1046,31 +926,12 @@ class JsonProjectManagementStore implements IFlexProjectManagementStore {
|
|
|
1046
926
|
assertFlexProjectManagementRecord(value);
|
|
1047
927
|
return value;
|
|
1048
928
|
});
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
expectedRevision,
|
|
1056
|
-
actualRevision,
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
} else if (current) {
|
|
1060
|
-
if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
|
|
1061
|
-
throw new FlexHarnessStoreConflictError(
|
|
1062
|
-
providerKey(storageKey, sessionId),
|
|
1063
|
-
expectedRevision,
|
|
1064
|
-
actualRevision,
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
} else if (actualRevision !== expectedRevision) {
|
|
1068
|
-
throw new FlexHarnessStoreConflictError(
|
|
1069
|
-
providerKey(storageKey, sessionId),
|
|
1070
|
-
expectedRevision,
|
|
1071
|
-
actualRevision,
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
929
|
+
if (!isProjectManagementTombstoneAdmitted(
|
|
930
|
+
providerKey(storageKey, sessionId),
|
|
931
|
+
current,
|
|
932
|
+
validated,
|
|
933
|
+
expectedRevision,
|
|
934
|
+
)) return;
|
|
1074
935
|
await this.root.write(filePath, validated);
|
|
1075
936
|
}),
|
|
1076
937
|
);
|
|
@@ -1163,12 +1024,7 @@ class JsonAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
|
1163
1024
|
(value) => validateAgentEventArchive(value, this.sessionId, archive.archiveId),
|
|
1164
1025
|
);
|
|
1165
1026
|
if (current) {
|
|
1166
|
-
|
|
1167
|
-
JSON.stringify(current.events.map((event) => event.id))
|
|
1168
|
-
!== JSON.stringify(validated.events.map((event) => event.id))
|
|
1169
|
-
) {
|
|
1170
|
-
throw new Error(`Archive "${archive.archiveId}" already exists with different events.`);
|
|
1171
|
-
}
|
|
1027
|
+
assertSameArchiveEvents(current, validated);
|
|
1172
1028
|
return;
|
|
1173
1029
|
}
|
|
1174
1030
|
await this.root.write(filePath, validated);
|
package/ts/errors.ts
CHANGED
|
@@ -173,13 +173,19 @@ export class FlexHarnessStoreFormatError extends FlexHarnessError {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* What a store was doing when it could not confirm the outcome: `write` and `delete` name a
|
|
178
|
+
* database record, `delete-file` and `delete-directory` a file-system path.
|
|
179
|
+
*/
|
|
180
|
+
export type TFlexStoreCommitOperation = 'write' | 'delete' | 'delete-file' | 'delete-directory';
|
|
181
|
+
|
|
176
182
|
export class FlexHarnessStoreCommitUncertainError extends FlexHarnessError {
|
|
177
183
|
public readonly path: string;
|
|
178
|
-
public readonly operation:
|
|
184
|
+
public readonly operation: TFlexStoreCommitOperation;
|
|
179
185
|
|
|
180
186
|
constructor(
|
|
181
187
|
path: string,
|
|
182
|
-
operation:
|
|
188
|
+
operation: TFlexStoreCommitOperation,
|
|
183
189
|
options: ErrorOptions,
|
|
184
190
|
) {
|
|
185
191
|
super(
|
package/ts/readme.md
CHANGED
|
@@ -61,10 +61,12 @@ or provider SDKs.
|
|
|
61
61
|
| `/chat` | Portable streaming `ChatSession`, history and usage | None |
|
|
62
62
|
| `/chat/cli` | Ink/React terminal chat | `ink ink-text-input react`; TypeScript: `@types/react` |
|
|
63
63
|
| `/chat/web` | Lit `flexchat-window`, `flexchat-message`, `flexchat-input` | `lit` |
|
|
64
|
+
| `/chat/harness` | `FlexHarnessTranscript` and intents for the `@design.estate/dees-catalog` harness components, browser-safe | `@design.estate/dees-catalog` (types only) |
|
|
64
65
|
| `/kvm` | Browser control, terminal framing and OCR observation | `puppeteer` |
|
|
65
66
|
| `/documents` | PDF and image documents, `extractTextFromPdf` | `@push.rocks/smartpdf`; for image documents `sharp` |
|
|
66
67
|
| `/mcp` | MCP clients and AI SDK tool conversion | `@modelcontextprotocol/sdk` |
|
|
67
68
|
| `/migration` | Existing versioned harness migrations | None |
|
|
69
|
+
| `/stores/nosqldb` | `NoSqlFlexHarnessStores`: cross-process CAS stores on `@lossless.org/client/nosqldb` | `@lossless.org/client` |
|
|
68
70
|
|
|
69
71
|
The integrations in the last column use optional peer dependencies. Install them
|
|
70
72
|
only for the entrypoints your project imports, for example:
|
|
@@ -1068,6 +1070,8 @@ After every harness using a `JsonFileFlexHarnessStores` instance has been dispos
|
|
|
1068
1070
|
|
|
1069
1071
|
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.
|
|
1070
1072
|
|
|
1073
|
+
`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.
|
|
1074
|
+
|
|
1071
1075
|
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.
|
|
1072
1076
|
|
|
1073
1077
|
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.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessStoreConflictError,
|
|
4
|
+
FlexHarnessStoreFormatError,
|
|
5
|
+
FlexHarnessValidationError,
|
|
6
|
+
} from './errors.js';
|
|
7
|
+
import type {
|
|
8
|
+
IFlexPermissionSnapshot,
|
|
9
|
+
IFlexProjectManagementSnapshot,
|
|
10
|
+
IFlexProjectManagementTombstone,
|
|
11
|
+
IFlexProjectManagementWriteContext,
|
|
12
|
+
IFlexScopeSnapshot,
|
|
13
|
+
TFlexProjectionSnapshot,
|
|
14
|
+
TFlexProjectManagementRecord,
|
|
15
|
+
} from './interfaces.js';
|
|
16
|
+
import { assertJsonSerializable, cloneSerializable } from './utils.json.js';
|
|
17
|
+
import { assertExactKeys, assertRecord } from './utils.storecodecs.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Store-independent admission rules every `IFlexHarnessStores` implementation applies: argument
|
|
21
|
+
* validation, the next-revision rule, exact session keys, and the project-management generation
|
|
22
|
+
* fence. Backends own only how they read, compare and write.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type TSnapshot =
|
|
26
|
+
| IFlexScopeSnapshot
|
|
27
|
+
| TFlexProjectionSnapshot
|
|
28
|
+
| IFlexPermissionSnapshot
|
|
29
|
+
| TFlexProjectManagementRecord;
|
|
30
|
+
type TSnapshotValidator<TSnapshotValue extends TSnapshot> = (
|
|
31
|
+
value: unknown,
|
|
32
|
+
) => asserts value is TSnapshotValue;
|
|
33
|
+
|
|
34
|
+
export function validateIdentifier(value: string, name: string): void {
|
|
35
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
36
|
+
throw new FlexHarnessValidationError(`${name} must be a non-empty string.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateExpectedRevision(expectedRevision: number): void {
|
|
41
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
|
|
42
|
+
throw new FlexHarnessValidationError('expectedRevision must be a non-negative integer.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateProjectManagementWriteContext(
|
|
47
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
48
|
+
): void {
|
|
49
|
+
assertJsonSerializable(writeContext, '$writeContext');
|
|
50
|
+
assertRecord(writeContext, 'writeContext');
|
|
51
|
+
assertExactKeys(writeContext, ['actor', 'runId', 'agent', 'toolCallId'], 'writeContext');
|
|
52
|
+
if (writeContext.actor !== 'agent' && writeContext.actor !== 'application') {
|
|
53
|
+
throw new FlexHarnessValidationError('writeContext.actor must be agent or application.');
|
|
54
|
+
}
|
|
55
|
+
for (const key of ['runId', 'agent', 'toolCallId'] as const) {
|
|
56
|
+
if (writeContext[key] !== undefined) validateIdentifier(writeContext[key], `writeContext.${key}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateSnapshotSave<TSnapshotValue extends TSnapshot>(
|
|
61
|
+
snapshot: TSnapshotValue,
|
|
62
|
+
expectedRevision: number,
|
|
63
|
+
validator: TSnapshotValidator<TSnapshotValue>,
|
|
64
|
+
): TSnapshotValue {
|
|
65
|
+
validator(snapshot);
|
|
66
|
+
validateExpectedRevision(expectedRevision);
|
|
67
|
+
if (snapshot.revision !== expectedRevision + 1) {
|
|
68
|
+
throw new FlexHarnessStoreFormatError(
|
|
69
|
+
`Snapshot revision ${snapshot.revision} must equal expected revision ${expectedRevision} plus one.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return cloneSerializable(snapshot);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function providerKey(storageKey: string, sessionId: string): string {
|
|
76
|
+
validateIdentifier(storageKey, 'storageKey');
|
|
77
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
78
|
+
return JSON.stringify([storageKey, sessionId]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function assertProjectionSession(snapshot: TFlexProjectionSnapshot, sessionId: string): void {
|
|
82
|
+
for (const message of snapshot.messages) {
|
|
83
|
+
if (message.sessionId !== sessionId) {
|
|
84
|
+
throw new FlexHarnessStoreFormatError(
|
|
85
|
+
`Projection message "${message.messageId}" does not belong to session "${sessionId}".`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const terminal of snapshot.stagedTerminals) {
|
|
90
|
+
if (
|
|
91
|
+
terminal.userMessage.sessionId !== sessionId
|
|
92
|
+
|| terminal.assistantMessage.sessionId !== sessionId
|
|
93
|
+
) {
|
|
94
|
+
throw new FlexHarnessStoreFormatError(
|
|
95
|
+
`Staged terminal "${terminal.runId}" does not belong to session "${sessionId}".`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sameProjectManagementGeneration(
|
|
102
|
+
left: TFlexProjectManagementRecord,
|
|
103
|
+
right: TFlexProjectManagementRecord,
|
|
104
|
+
): boolean {
|
|
105
|
+
return left.sessionGenerationId === right.sessionGenerationId
|
|
106
|
+
&& left.sessionGenerationSequence === right.sessionGenerationSequence;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function canInitializeProjectManagementGeneration(
|
|
110
|
+
current: TFlexProjectManagementRecord,
|
|
111
|
+
next: TFlexProjectManagementRecord,
|
|
112
|
+
expectedRevision: number,
|
|
113
|
+
): boolean {
|
|
114
|
+
return 'deletedAt' in current
|
|
115
|
+
&& expectedRevision === 0
|
|
116
|
+
&& next.sessionGenerationId !== current.sessionGenerationId
|
|
117
|
+
&& next.sessionGenerationSequence > current.sessionGenerationSequence;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Admits a live project-management save over the stored record: a CAS save within the stored
|
|
122
|
+
* generation, or a newer generation that replaces a tombstone at expected revision 0.
|
|
123
|
+
*/
|
|
124
|
+
export function assertProjectManagementSaveAdmitted(
|
|
125
|
+
key: string,
|
|
126
|
+
current: TFlexProjectManagementRecord | undefined,
|
|
127
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
128
|
+
expectedRevision: number,
|
|
129
|
+
): void {
|
|
130
|
+
const actualRevision = current?.revision ?? 0;
|
|
131
|
+
if (current && !sameProjectManagementGeneration(current, snapshot)) {
|
|
132
|
+
if (!canInitializeProjectManagementGeneration(current, snapshot, expectedRevision)) {
|
|
133
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
134
|
+
}
|
|
135
|
+
} else if ((current && 'deletedAt' in current) || actualRevision !== expectedRevision) {
|
|
136
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Admits a project-management tombstone over the stored record. Returns false when the same
|
|
142
|
+
* generation is already tombstoned, which makes a repeated cleanup idempotent.
|
|
143
|
+
*/
|
|
144
|
+
export function isProjectManagementTombstoneAdmitted(
|
|
145
|
+
key: string,
|
|
146
|
+
current: TFlexProjectManagementRecord | undefined,
|
|
147
|
+
tombstone: IFlexProjectManagementTombstone,
|
|
148
|
+
expectedRevision: number,
|
|
149
|
+
): boolean {
|
|
150
|
+
const actualRevision = current?.revision ?? 0;
|
|
151
|
+
if (current && sameProjectManagementGeneration(current, tombstone)) {
|
|
152
|
+
if ('deletedAt' in current) return false;
|
|
153
|
+
if (actualRevision !== expectedRevision) {
|
|
154
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
155
|
+
}
|
|
156
|
+
} else if (current) {
|
|
157
|
+
if (!canInitializeProjectManagementGeneration(current, tombstone, expectedRevision)) {
|
|
158
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
159
|
+
}
|
|
160
|
+
} else if (actualRevision !== expectedRevision) {
|
|
161
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** An archive is written once; repeating it with the same events is a no-op. */
|
|
167
|
+
export function assertSameArchiveEvents(
|
|
168
|
+
existing: plugins.IAgentEventArchiveV2,
|
|
169
|
+
next: plugins.IAgentEventArchiveV2,
|
|
170
|
+
): void {
|
|
171
|
+
if (
|
|
172
|
+
JSON.stringify(existing.events.map((event) => event.id))
|
|
173
|
+
!== JSON.stringify(next.events.map((event) => event.id))
|
|
174
|
+
) {
|
|
175
|
+
throw new Error(`Archive "${next.archiveId}" already exists with different events.`);
|
|
176
|
+
}
|
|
177
|
+
}
|