@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.
@@ -10,12 +10,17 @@ import type {
10
10
  IFlexHarnessStores,
11
11
  IFlexPermissionSnapshot,
12
12
  IFlexPermissionStore,
13
- IFlexProjectionSnapshotV2,
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
- type TSnapshot = IFlexScopeSnapshot | TFlexProjectionSnapshot | IFlexPermissionSnapshot;
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: IFlexProjectionSnapshotV2,
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 = ['scopes', 'projections', 'permissions', 'events', 'archives', 'jobs'] as const;
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: IFlexProjectionSnapshotV2,
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,