@modelprofile.com/flexharness 3.7.0 → 4.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/changelog.md +21 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +65 -2
- package/dist_ts/classes.flexharness.js +1401 -222
- package/dist_ts/classes.stores.d.ts +10 -1
- package/dist_ts/classes.stores.js +164 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +184 -6
- package/dist_ts/interfaces.js +16 -1
- package/dist_ts/utils.json.d.ts +3 -2
- package/dist_ts/utils.json.js +278 -12
- package/dist_ts/utils.projectmanagement.d.ts +5 -0
- package/dist_ts/utils.projectmanagement.js +141 -0
- package/dist_ts_migration/v3_legacyflexharness.js +1 -1
- package/package.json +1 -1
- package/readme.hints.md +7 -4
- package/readme.md +207 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +2024 -255
- package/ts/classes.stores.ts +265 -6
- package/ts/index.ts +6 -0
- package/ts/interfaces.ts +254 -6
- package/ts/utils.json.ts +321 -13
- package/ts/utils.projectmanagement.ts +226 -0
- package/ts_migration/v3_legacyflexharness.ts +1 -1
package/ts/classes.stores.ts
CHANGED
|
@@ -10,12 +10,17 @@ import type {
|
|
|
10
10
|
IFlexHarnessStores,
|
|
11
11
|
IFlexPermissionSnapshot,
|
|
12
12
|
IFlexPermissionStore,
|
|
13
|
-
|
|
13
|
+
IFlexProjectManagementSnapshot,
|
|
14
|
+
IFlexProjectManagementStore,
|
|
15
|
+
IFlexProjectManagementTombstone,
|
|
16
|
+
IFlexProjectManagementWriteContext,
|
|
17
|
+
IFlexProjectionSnapshotV3,
|
|
14
18
|
IFlexProjectionStore,
|
|
15
19
|
IFlexScopeSnapshot,
|
|
16
20
|
IFlexScopeStore,
|
|
17
21
|
IFlexToolJobStoreProvider,
|
|
18
22
|
TFlexProjectionSnapshot,
|
|
23
|
+
TFlexProjectManagementRecord,
|
|
19
24
|
} from './interfaces.js';
|
|
20
25
|
import {
|
|
21
26
|
assertFlexPermissionSnapshot,
|
|
@@ -24,8 +29,17 @@ import {
|
|
|
24
29
|
assertJsonSerializable,
|
|
25
30
|
cloneSerializable,
|
|
26
31
|
} from './utils.json.js';
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
import {
|
|
33
|
+
assertFlexProjectManagementRecord,
|
|
34
|
+
assertFlexProjectManagementSnapshot,
|
|
35
|
+
assertFlexProjectManagementTombstone,
|
|
36
|
+
} from './utils.projectmanagement.js';
|
|
37
|
+
|
|
38
|
+
type TSnapshot =
|
|
39
|
+
| IFlexScopeSnapshot
|
|
40
|
+
| TFlexProjectionSnapshot
|
|
41
|
+
| IFlexPermissionSnapshot
|
|
42
|
+
| TFlexProjectManagementRecord;
|
|
29
43
|
type TSnapshotValidator<TSnapshotValue extends TSnapshot> = (
|
|
30
44
|
value: unknown,
|
|
31
45
|
) => asserts value is TSnapshotValue;
|
|
@@ -42,6 +56,20 @@ function validateExpectedRevision(expectedRevision: number): void {
|
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
58
|
|
|
59
|
+
function validateProjectManagementWriteContext(
|
|
60
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
61
|
+
): void {
|
|
62
|
+
assertJsonSerializable(writeContext, '$writeContext');
|
|
63
|
+
assertRecord(writeContext, 'writeContext');
|
|
64
|
+
assertExactKeys(writeContext, ['actor', 'runId', 'agent', 'toolCallId'], 'writeContext');
|
|
65
|
+
if (writeContext.actor !== 'agent' && writeContext.actor !== 'application') {
|
|
66
|
+
throw new FlexHarnessValidationError('writeContext.actor must be agent or application.');
|
|
67
|
+
}
|
|
68
|
+
for (const key of ['runId', 'agent', 'toolCallId'] as const) {
|
|
69
|
+
if (writeContext[key] !== undefined) validateIdentifier(writeContext[key], `writeContext.${key}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
function validateSnapshotSave<TSnapshotValue extends TSnapshot>(
|
|
46
74
|
snapshot: TSnapshotValue,
|
|
47
75
|
expectedRevision: number,
|
|
@@ -63,6 +91,25 @@ function providerKey(storageKey: string, sessionId: string): string {
|
|
|
63
91
|
return JSON.stringify([storageKey, sessionId]);
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
function sameProjectManagementGeneration(
|
|
95
|
+
left: TFlexProjectManagementRecord,
|
|
96
|
+
right: TFlexProjectManagementRecord,
|
|
97
|
+
): boolean {
|
|
98
|
+
return left.sessionGenerationId === right.sessionGenerationId
|
|
99
|
+
&& left.sessionGenerationSequence === right.sessionGenerationSequence;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function canInitializeProjectManagementGeneration(
|
|
103
|
+
current: TFlexProjectManagementRecord,
|
|
104
|
+
next: TFlexProjectManagementRecord,
|
|
105
|
+
expectedRevision: number,
|
|
106
|
+
): boolean {
|
|
107
|
+
return 'deletedAt' in current
|
|
108
|
+
&& expectedRevision === 0
|
|
109
|
+
&& next.sessionGenerationId !== current.sessionGenerationId
|
|
110
|
+
&& next.sessionGenerationSequence > current.sessionGenerationSequence;
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
function assertProjectionSession(snapshot: TFlexProjectionSnapshot, sessionId: string): void {
|
|
67
114
|
for (const message of snapshot.messages) {
|
|
68
115
|
if (message.sessionId !== sessionId) {
|
|
@@ -365,7 +412,7 @@ class InMemoryProjectionStore implements IFlexProjectionStore {
|
|
|
365
412
|
public async save(
|
|
366
413
|
storageKey: string,
|
|
367
414
|
sessionId: string,
|
|
368
|
-
snapshot:
|
|
415
|
+
snapshot: IFlexProjectionSnapshotV3,
|
|
369
416
|
expectedRevision: number,
|
|
370
417
|
): Promise<void> {
|
|
371
418
|
const key = providerKey(storageKey, sessionId);
|
|
@@ -414,6 +461,81 @@ class InMemoryPermissionStore implements IFlexPermissionStore {
|
|
|
414
461
|
}
|
|
415
462
|
}
|
|
416
463
|
|
|
464
|
+
export class InMemoryFlexProjectManagementStore implements IFlexProjectManagementStore {
|
|
465
|
+
private readonly snapshots = new Map<string, TFlexProjectManagementRecord>();
|
|
466
|
+
|
|
467
|
+
public async load(
|
|
468
|
+
storageKey: string,
|
|
469
|
+
sessionId: string,
|
|
470
|
+
): Promise<TFlexProjectManagementRecord | undefined> {
|
|
471
|
+
const snapshot = this.snapshots.get(providerKey(storageKey, sessionId));
|
|
472
|
+
return snapshot ? cloneSerializable(snapshot) : undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
public async save(
|
|
476
|
+
storageKey: string,
|
|
477
|
+
sessionId: string,
|
|
478
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
479
|
+
expectedRevision: number,
|
|
480
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
481
|
+
): Promise<void> {
|
|
482
|
+
const key = providerKey(storageKey, sessionId);
|
|
483
|
+
validateProjectManagementWriteContext(writeContext);
|
|
484
|
+
const validated = validateSnapshotSave(
|
|
485
|
+
snapshot,
|
|
486
|
+
expectedRevision,
|
|
487
|
+
assertFlexProjectManagementSnapshot,
|
|
488
|
+
);
|
|
489
|
+
const current = this.snapshots.get(key);
|
|
490
|
+
const actualRevision = current?.revision ?? 0;
|
|
491
|
+
if (current && !sameProjectManagementGeneration(current, validated)) {
|
|
492
|
+
if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
|
|
493
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
494
|
+
}
|
|
495
|
+
} else if ((current && 'deletedAt' in current) || actualRevision !== expectedRevision) {
|
|
496
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
497
|
+
}
|
|
498
|
+
this.snapshots.set(key, validated);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
public async tombstoneSession(
|
|
502
|
+
storageKey: string,
|
|
503
|
+
sessionId: string,
|
|
504
|
+
tombstone: IFlexProjectManagementTombstone,
|
|
505
|
+
expectedRevision: number,
|
|
506
|
+
): Promise<void> {
|
|
507
|
+
const key = providerKey(storageKey, sessionId);
|
|
508
|
+
const validated = validateSnapshotSave(
|
|
509
|
+
tombstone,
|
|
510
|
+
expectedRevision,
|
|
511
|
+
assertFlexProjectManagementTombstone,
|
|
512
|
+
);
|
|
513
|
+
const current = this.snapshots.get(key);
|
|
514
|
+
const actualRevision = current?.revision ?? 0;
|
|
515
|
+
if (current && sameProjectManagementGeneration(current, validated)) {
|
|
516
|
+
if ('deletedAt' in current) return;
|
|
517
|
+
if (actualRevision !== expectedRevision) {
|
|
518
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
519
|
+
}
|
|
520
|
+
} else if (current) {
|
|
521
|
+
if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
|
|
522
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
523
|
+
}
|
|
524
|
+
} else if (actualRevision !== expectedRevision) {
|
|
525
|
+
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
526
|
+
}
|
|
527
|
+
this.snapshots.set(key, validated);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
public async purgeNamespace(storageKey: string): Promise<void> {
|
|
531
|
+
validateIdentifier(storageKey, 'storageKey');
|
|
532
|
+
for (const key of this.snapshots.keys()) {
|
|
533
|
+
const [recordStorageKey] = JSON.parse(key) as [string, string];
|
|
534
|
+
if (recordStorageKey === storageKey) this.snapshots.delete(key);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
417
539
|
class InMemoryAgentEventStore implements plugins.IAgentEventStoreV2 {
|
|
418
540
|
public readonly eventSchemaVersion = 2 as const;
|
|
419
541
|
private snapshot: plugins.IAgentEventSnapshotV2 | undefined;
|
|
@@ -563,9 +685,18 @@ export class InMemoryFlexHarnessStores implements IFlexHarnessStores {
|
|
|
563
685
|
public readonly permissions: IFlexPermissionStore = new InMemoryPermissionStore();
|
|
564
686
|
public readonly agentEvents: IFlexAgentEventStoreProvider = new InMemoryAgentEventStoreProvider();
|
|
565
687
|
public readonly jobs: IFlexToolJobStoreProvider = new InMemoryToolJobStoreProvider();
|
|
688
|
+
public readonly projectManagement = new InMemoryFlexProjectManagementStore();
|
|
566
689
|
}
|
|
567
690
|
|
|
568
|
-
const jsonDomains = [
|
|
691
|
+
const jsonDomains = [
|
|
692
|
+
'scopes',
|
|
693
|
+
'projections',
|
|
694
|
+
'permissions',
|
|
695
|
+
'projectManagement',
|
|
696
|
+
'events',
|
|
697
|
+
'archives',
|
|
698
|
+
'jobs',
|
|
699
|
+
] as const;
|
|
569
700
|
type TJsonDomain = (typeof jsonDomains)[number];
|
|
570
701
|
|
|
571
702
|
interface IDirectorySyncAttempt {
|
|
@@ -621,6 +752,11 @@ class JsonFileRoot {
|
|
|
621
752
|
);
|
|
622
753
|
}
|
|
623
754
|
|
|
755
|
+
public sessionNamespacePath(domain: TJsonDomain, storageKey: string): string {
|
|
756
|
+
validateIdentifier(storageKey, 'storageKey');
|
|
757
|
+
return plugins.path.join(this.directory, domain, this.digest(storageKey));
|
|
758
|
+
}
|
|
759
|
+
|
|
624
760
|
public archivePath(storageKey: string, sessionId: string, archiveId: string): string {
|
|
625
761
|
validateIdentifier(archiveId, 'archiveId');
|
|
626
762
|
return plugins.path.join(
|
|
@@ -988,7 +1124,7 @@ class JsonProjectionStore implements IFlexProjectionStore {
|
|
|
988
1124
|
public async save(
|
|
989
1125
|
storageKey: string,
|
|
990
1126
|
sessionId: string,
|
|
991
|
-
snapshot:
|
|
1127
|
+
snapshot: IFlexProjectionSnapshotV3,
|
|
992
1128
|
expectedRevision: number,
|
|
993
1129
|
): Promise<void> {
|
|
994
1130
|
const validated = validateSnapshotSave(snapshot, expectedRevision, assertFlexProjectionSnapshot);
|
|
@@ -1058,6 +1194,127 @@ class JsonPermissionStore implements IFlexPermissionStore {
|
|
|
1058
1194
|
}
|
|
1059
1195
|
}
|
|
1060
1196
|
|
|
1197
|
+
class JsonProjectManagementStore implements IFlexProjectManagementStore {
|
|
1198
|
+
constructor(private readonly root: JsonFileRoot) {}
|
|
1199
|
+
|
|
1200
|
+
private inNamespaceQueue<T>(storageKey: string, operation: () => Promise<T>): Promise<T> {
|
|
1201
|
+
return JsonFileRoot.inQueue(
|
|
1202
|
+
`project-management:${this.root.sessionNamespacePath('projectManagement', storageKey)}`,
|
|
1203
|
+
operation,
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
public async load(
|
|
1208
|
+
storageKey: string,
|
|
1209
|
+
sessionId: string,
|
|
1210
|
+
): Promise<TFlexProjectManagementRecord | undefined> {
|
|
1211
|
+
const filePath = this.root.sessionPath('projectManagement', storageKey, sessionId);
|
|
1212
|
+
return this.inNamespaceQueue(storageKey, () =>
|
|
1213
|
+
JsonFileRoot.inQueue(filePath, async () => {
|
|
1214
|
+
const snapshot = await this.root.read(filePath, (value) => {
|
|
1215
|
+
assertFlexProjectManagementRecord(value);
|
|
1216
|
+
return value;
|
|
1217
|
+
});
|
|
1218
|
+
return snapshot ? cloneSerializable(snapshot) : undefined;
|
|
1219
|
+
})
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
public async save(
|
|
1224
|
+
storageKey: string,
|
|
1225
|
+
sessionId: string,
|
|
1226
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
1227
|
+
expectedRevision: number,
|
|
1228
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
1229
|
+
): Promise<void> {
|
|
1230
|
+
validateProjectManagementWriteContext(writeContext);
|
|
1231
|
+
const validated = validateSnapshotSave(
|
|
1232
|
+
snapshot,
|
|
1233
|
+
expectedRevision,
|
|
1234
|
+
assertFlexProjectManagementSnapshot,
|
|
1235
|
+
);
|
|
1236
|
+
const filePath = this.root.sessionPath('projectManagement', storageKey, sessionId);
|
|
1237
|
+
await this.inNamespaceQueue(storageKey, () =>
|
|
1238
|
+
JsonFileRoot.inQueue(filePath, async () => {
|
|
1239
|
+
const current = await this.root.read(filePath, (value) => {
|
|
1240
|
+
assertFlexProjectManagementRecord(value);
|
|
1241
|
+
return value;
|
|
1242
|
+
});
|
|
1243
|
+
const actualRevision = current?.revision ?? 0;
|
|
1244
|
+
if (current && !sameProjectManagementGeneration(current, validated)) {
|
|
1245
|
+
if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
|
|
1246
|
+
throw new FlexHarnessStoreConflictError(
|
|
1247
|
+
providerKey(storageKey, sessionId),
|
|
1248
|
+
expectedRevision,
|
|
1249
|
+
actualRevision,
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
} else if ((current && 'deletedAt' in current) || actualRevision !== expectedRevision) {
|
|
1253
|
+
throw new FlexHarnessStoreConflictError(
|
|
1254
|
+
providerKey(storageKey, sessionId),
|
|
1255
|
+
expectedRevision,
|
|
1256
|
+
actualRevision,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
await this.root.write(filePath, validated);
|
|
1260
|
+
}),
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
public async tombstoneSession(
|
|
1265
|
+
storageKey: string,
|
|
1266
|
+
sessionId: string,
|
|
1267
|
+
tombstone: IFlexProjectManagementTombstone,
|
|
1268
|
+
expectedRevision: number,
|
|
1269
|
+
): Promise<void> {
|
|
1270
|
+
const validated = validateSnapshotSave(
|
|
1271
|
+
tombstone,
|
|
1272
|
+
expectedRevision,
|
|
1273
|
+
assertFlexProjectManagementTombstone,
|
|
1274
|
+
);
|
|
1275
|
+
const filePath = this.root.sessionPath('projectManagement', storageKey, sessionId);
|
|
1276
|
+
await this.inNamespaceQueue(storageKey, () =>
|
|
1277
|
+
JsonFileRoot.inQueue(filePath, async () => {
|
|
1278
|
+
const current = await this.root.read(filePath, (value) => {
|
|
1279
|
+
assertFlexProjectManagementRecord(value);
|
|
1280
|
+
return value;
|
|
1281
|
+
});
|
|
1282
|
+
const actualRevision = current?.revision ?? 0;
|
|
1283
|
+
if (current && sameProjectManagementGeneration(current, validated)) {
|
|
1284
|
+
if ('deletedAt' in current) return;
|
|
1285
|
+
if (actualRevision !== expectedRevision) {
|
|
1286
|
+
throw new FlexHarnessStoreConflictError(
|
|
1287
|
+
providerKey(storageKey, sessionId),
|
|
1288
|
+
expectedRevision,
|
|
1289
|
+
actualRevision,
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
} else if (current) {
|
|
1293
|
+
if (!canInitializeProjectManagementGeneration(current, validated, expectedRevision)) {
|
|
1294
|
+
throw new FlexHarnessStoreConflictError(
|
|
1295
|
+
providerKey(storageKey, sessionId),
|
|
1296
|
+
expectedRevision,
|
|
1297
|
+
actualRevision,
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
} else if (actualRevision !== expectedRevision) {
|
|
1301
|
+
throw new FlexHarnessStoreConflictError(
|
|
1302
|
+
providerKey(storageKey, sessionId),
|
|
1303
|
+
expectedRevision,
|
|
1304
|
+
actualRevision,
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
await this.root.write(filePath, validated);
|
|
1308
|
+
}),
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
public async purgeNamespace(storageKey: string): Promise<void> {
|
|
1313
|
+
const directory = this.root.sessionNamespacePath('projectManagement', storageKey);
|
|
1314
|
+
await this.inNamespaceQueue(storageKey, () => this.root.deleteDirectory(directory));
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1061
1318
|
async function clearJsonAgentEventSession(
|
|
1062
1319
|
root: JsonFileRoot,
|
|
1063
1320
|
storageKey: string,
|
|
@@ -1305,6 +1562,7 @@ export class JsonFileFlexHarnessStores implements IFlexHarnessStores {
|
|
|
1305
1562
|
public readonly permissions: IFlexPermissionStore;
|
|
1306
1563
|
public readonly agentEvents: IFlexAgentEventStoreProvider;
|
|
1307
1564
|
public readonly jobs: IFlexToolJobStoreProvider;
|
|
1565
|
+
public readonly projectManagement: IFlexProjectManagementStore;
|
|
1308
1566
|
private readonly root: JsonFileRoot;
|
|
1309
1567
|
|
|
1310
1568
|
constructor(options: IJsonFileFlexHarnessStoresOptions) {
|
|
@@ -1315,6 +1573,7 @@ export class JsonFileFlexHarnessStores implements IFlexHarnessStores {
|
|
|
1315
1573
|
this.scopes = new JsonScopeStore(this.root);
|
|
1316
1574
|
this.projections = new JsonProjectionStore(this.root);
|
|
1317
1575
|
this.permissions = new JsonPermissionStore(this.root);
|
|
1576
|
+
this.projectManagement = new JsonProjectManagementStore(this.root);
|
|
1318
1577
|
this.agentEvents = new JsonAgentEventStoreProvider(this.root);
|
|
1319
1578
|
this.jobs = new JsonToolJobStoreProvider(this.root);
|
|
1320
1579
|
}
|
package/ts/index.ts
CHANGED
|
@@ -3,6 +3,12 @@ export * from './classes.stores.js';
|
|
|
3
3
|
export * from './errors.js';
|
|
4
4
|
export * from './interfaces.js';
|
|
5
5
|
export { normalizeJsonValue } from './utils.json.js';
|
|
6
|
+
export {
|
|
7
|
+
assertFlexProjectManagementSnapshot,
|
|
8
|
+
assertFlexProjectManagementRecord,
|
|
9
|
+
assertFlexProjectManagementTombstone,
|
|
10
|
+
createEmptyFlexProjectManagementSnapshot,
|
|
11
|
+
} from './utils.projectmanagement.js';
|
|
6
12
|
export {
|
|
7
13
|
FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
|
|
8
14
|
parseSlashCommand,
|