@modelprofile.com/flexharness 3.4.0 → 3.6.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/ts/interfaces.ts CHANGED
@@ -90,6 +90,8 @@ export interface IFlexToolMessagePart {
90
90
  input: TJsonValue;
91
91
  output?: TJsonValue;
92
92
  error?: string;
93
+ childSessionId?: string;
94
+ model?: IFlexModelIdentity;
93
95
  }
94
96
 
95
97
  export interface IFlexAttachmentMessagePart {
@@ -139,6 +141,11 @@ export interface IFlexSession {
139
141
  archivedAt?: string;
140
142
  status: TFlexActivityStatus;
141
143
  activity: IFlexSessionActivity;
144
+ parentSessionId?: string;
145
+ parentRunId?: string;
146
+ parentToolCallId?: string;
147
+ agent?: string;
148
+ depth?: number;
142
149
  }
143
150
 
144
151
  export interface IFlexCreateSessionOptions {
@@ -233,6 +240,8 @@ export interface IFlexModelResolverContext<TScope> {
233
240
  sessionId: string;
234
241
  runId: string;
235
242
  modelHint?: string;
243
+ readonly parentSessionId?: string;
244
+ readonly agent?: string;
236
245
  signal: AbortSignal;
237
246
  }
238
247
 
@@ -274,6 +283,8 @@ export interface IFlexToolProviderContext<TScope> {
274
283
  scope: TScope;
275
284
  sessionId: string;
276
285
  runId: string;
286
+ readonly parentSessionId?: string;
287
+ readonly agent?: string;
277
288
  signal: AbortSignal;
278
289
  requestPermission(request: IFlexPermissionRequestInput): Promise<void>;
279
290
  }
@@ -289,9 +300,35 @@ export interface IFlexToolProvider<TScope> {
289
300
  ): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
290
301
  }
291
302
 
303
+ export interface IFlexResourceToolProviderResolverContext<TScope> {
304
+ scopeId: string;
305
+ scope: TScope;
306
+ sessionId: string;
307
+ runId: string;
308
+ readonly parentSessionId?: string;
309
+ readonly agent?: string;
310
+ signal: AbortSignal;
311
+ }
312
+
313
+ export interface IFlexResourceToolProviderDescriptor<TScope> {
314
+ resourceId: string;
315
+ attachmentRevision: number;
316
+ provider: IFlexToolProvider<TScope>;
317
+ }
318
+
319
+ export interface IFlexResourceToolProviderResolver<TScope> {
320
+ resolveResourceToolProviders(
321
+ context: IFlexResourceToolProviderResolverContext<TScope>,
322
+ ): Promise<readonly IFlexResourceToolProviderDescriptor<TScope>[]>
323
+ | readonly IFlexResourceToolProviderDescriptor<TScope>[];
324
+ }
325
+
292
326
  export interface IFlexSessionTombstone {
293
327
  sessionId: string;
294
328
  deletedAt: string;
329
+ rootSessionId?: string;
330
+ depth?: number;
331
+ parentSessionId?: string;
295
332
  }
296
333
 
297
334
  export interface IFlexScopeSnapshot {
@@ -468,10 +505,19 @@ export interface IFlexAgentSessionPolicy<TScope = unknown> {
468
505
  maxContextOverflowRetries?: number;
469
506
  }
470
507
 
508
+ export interface IFlexSubagentDefinition {
509
+ name: string;
510
+ description: string;
511
+ modelHint?: string;
512
+ system?: string;
513
+ maxSteps?: number;
514
+ }
515
+
471
516
  export interface IFlexHarnessOptions<TScope> {
472
517
  scopeResolver: IFlexScopeResolver<TScope>;
473
518
  modelResolver: IFlexModelResolver<TScope>;
474
519
  toolProvider?: IFlexToolProvider<TScope>;
520
+ resourceToolProviderResolver?: IFlexResourceToolProviderResolver<TScope>;
475
521
  executionContextProvider?: IFlexExecutionContextProvider<TScope>;
476
522
  stores?: IFlexHarnessStores;
477
523
  agentSessionPolicy?: IFlexAgentSessionPolicy<TScope>;
@@ -479,6 +525,9 @@ export interface IFlexHarnessOptions<TScope> {
479
525
  callbackLimits?: IFlexCallbackLimits;
480
526
  promptQueueLimits?: IFlexPromptQueueLimits;
481
527
  externalErrorProjector?: TFlexExternalErrorProjector;
528
+ subagents?: IFlexSubagentDefinition[];
529
+ maxSubagentDepth?: number;
530
+ maxSubagentCallsPerRun?: number;
482
531
  }
483
532
 
484
533
  export interface IFlexUncertainToolExecution {
@@ -555,7 +604,7 @@ export interface IFlexMessageChangedEvent extends IFlexEventBase {
555
604
  }
556
605
 
557
606
  export interface IFlexPartChangedEvent extends IFlexEventBase {
558
- readonly type: 'part.started' | 'part.delta' | 'part.completed';
607
+ readonly type: 'part.started' | 'part.delta' | 'part.updated' | 'part.completed';
559
608
  readonly runId: string;
560
609
  readonly messageId: string;
561
610
  /** Zero-based position in the session's authoritative message sequence. */
package/ts/plugins.ts CHANGED
@@ -15,8 +15,10 @@ import {
15
15
  createAgentEvent,
16
16
  getAgentGenerationTransactions,
17
17
  modelMessagesToAgentEvents,
18
+ tool,
18
19
  validateAgentEventArchiveV2,
19
20
  validateAgentEventSnapshotV2,
21
+ z,
20
22
  } from '@push.rocks/smartagent';
21
23
 
22
24
  export {
@@ -27,8 +29,10 @@ export {
27
29
  createAgentEvent,
28
30
  getAgentGenerationTransactions,
29
31
  modelMessagesToAgentEvents,
32
+ tool,
30
33
  validateAgentEventArchiveV2,
31
34
  validateAgentEventSnapshotV2,
35
+ z,
32
36
  };
33
37
  export type {
34
38
  IAgentEventArchiveV2,
package/ts/utils.json.ts CHANGED
@@ -422,7 +422,7 @@ const activityStatuses = [
422
422
  'cancelled',
423
423
  ] as const;
424
424
 
425
- function validateSession(value: unknown, path: string): string {
425
+ function validateSession(value: unknown, path: string): IValidatedSessionIdentity {
426
426
  const session = requireRecord(value, path);
427
427
  requireOnlyKeys(
428
428
  session,
@@ -435,6 +435,11 @@ function validateSession(value: unknown, path: string): string {
435
435
  'archivedAt',
436
436
  'status',
437
437
  'activity',
438
+ 'parentSessionId',
439
+ 'parentRunId',
440
+ 'parentToolCallId',
441
+ 'agent',
442
+ 'depth',
438
443
  ],
439
444
  path,
440
445
  );
@@ -456,7 +461,41 @@ function validateSession(value: unknown, path: string): string {
456
461
  requireOptionalString(activity.startedAt, `${path}.activity.startedAt`);
457
462
  requireOptionalString(activity.completedAt, `${path}.activity.completedAt`);
458
463
  requireOptionalString(activity.error, `${path}.activity.error`);
459
- return sessionId;
464
+ const relationship = [
465
+ session.parentSessionId,
466
+ session.parentRunId,
467
+ session.parentToolCallId,
468
+ session.agent,
469
+ ];
470
+ const relationshipCount = relationship.filter((entry) => entry !== undefined).length;
471
+ if (relationshipCount !== 0 && relationshipCount !== relationship.length) {
472
+ throw new FlexHarnessStoreFormatError(`${path} has partial subagent relationship fields.`);
473
+ }
474
+ for (const [key, entry] of [
475
+ ['parentSessionId', session.parentSessionId],
476
+ ['parentRunId', session.parentRunId],
477
+ ['parentToolCallId', session.parentToolCallId],
478
+ ['agent', session.agent],
479
+ ] as const) requireOptionalString(entry, `${path}.${key}`);
480
+ if (session.depth !== undefined) requireNonNegativeInteger(session.depth, `${path}.depth`);
481
+ if (relationshipCount === 0 && session.depth !== undefined && session.depth !== 0) {
482
+ throw new FlexHarnessStoreFormatError(`${path}.depth must be 0 for a root session.`);
483
+ }
484
+ if (relationshipCount > 0 && (!Number.isSafeInteger(session.depth) || Number(session.depth) < 1)) {
485
+ throw new FlexHarnessStoreFormatError(`${path}.depth must be positive for a child session.`);
486
+ }
487
+ return {
488
+ sessionId,
489
+ ...(relationshipCount === 0
490
+ ? {}
491
+ : {
492
+ parentSessionId: session.parentSessionId as string,
493
+ parentRunId: session.parentRunId as string,
494
+ parentToolCallId: session.parentToolCallId as string,
495
+ agent: session.agent as string,
496
+ }),
497
+ ...(session.depth === undefined ? {} : { depth: session.depth as number }),
498
+ };
460
499
  }
461
500
 
462
501
  function validateModel(value: unknown, path: string): void {
@@ -490,6 +529,15 @@ interface IValidatedMessageIdentity {
490
529
  status: 'streaming' | 'completed' | 'failed' | 'cancelled';
491
530
  }
492
531
 
532
+ interface IValidatedSessionIdentity {
533
+ sessionId: string;
534
+ parentSessionId?: string;
535
+ parentRunId?: string;
536
+ parentToolCallId?: string;
537
+ agent?: string;
538
+ depth?: number;
539
+ }
540
+
493
541
  function validateMessage(value: unknown, path: string): IValidatedMessageIdentity {
494
542
  const message = requireRecord(value, path);
495
543
  requireOnlyKeys(
@@ -551,7 +599,18 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
551
599
  } else if (part.type === 'tool') {
552
600
  requireOnlyKeys(
553
601
  part,
554
- ['partId', 'type', 'toolCallId', 'toolName', 'status', 'input', 'output', 'error'],
602
+ [
603
+ 'partId',
604
+ 'type',
605
+ 'toolCallId',
606
+ 'toolName',
607
+ 'status',
608
+ 'input',
609
+ 'output',
610
+ 'error',
611
+ 'childSessionId',
612
+ 'model',
613
+ ],
555
614
  partPath,
556
615
  );
557
616
  requireString(part.toolCallId, `${partPath}.toolCallId`);
@@ -563,6 +622,8 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
563
622
  throw new FlexHarnessStoreFormatError(`${partPath}.input is required.`);
564
623
  }
565
624
  requireOptionalString(part.error, `${partPath}.error`);
625
+ requireOptionalString(part.childSessionId, `${partPath}.childSessionId`);
626
+ if (part.model !== undefined) validateModel(part.model, `${partPath}.model`);
566
627
  } else if (part.type === 'attachment') {
567
628
  requireOnlyKeys(
568
629
  part,
@@ -650,23 +711,91 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
650
711
  throw new FlexHarnessStoreFormatError('Snapshot sessions must be an array.');
651
712
  }
652
713
  const sessionIds = new Set<string>();
714
+ const sessions = new Map<string, IValidatedSessionIdentity>();
653
715
  for (let index = 0; index < snapshot.sessions.length; index++) {
654
- const sessionId = validateSession(snapshot.sessions[index], `$snapshot.sessions[${index}]`);
655
- if (sessionIds.has(sessionId)) {
656
- throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate session "${sessionId}".`);
716
+ const session = validateSession(snapshot.sessions[index], `$snapshot.sessions[${index}]`);
717
+ if (sessionIds.has(session.sessionId)) {
718
+ throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate session "${session.sessionId}".`);
719
+ }
720
+ sessionIds.add(session.sessionId);
721
+ sessions.set(session.sessionId, session);
722
+ }
723
+ const origins = new Set<string>();
724
+ for (const session of sessions.values()) {
725
+ if (!session.parentSessionId) continue;
726
+ const parent = sessions.get(session.parentSessionId);
727
+ if (!parent) {
728
+ throw new FlexHarnessStoreFormatError(
729
+ `Snapshot child session "${session.sessionId}" has no live parent "${session.parentSessionId}".`,
730
+ );
731
+ }
732
+ const expectedDepth = (parent.depth ?? 0) + 1;
733
+ if (session.depth !== expectedDepth) {
734
+ throw new FlexHarnessStoreFormatError(
735
+ `Snapshot child session "${session.sessionId}" has inconsistent depth.`,
736
+ );
737
+ }
738
+ const origin = JSON.stringify([
739
+ session.parentSessionId,
740
+ session.parentRunId,
741
+ session.parentToolCallId,
742
+ ]);
743
+ if (origins.has(origin)) {
744
+ throw new FlexHarnessStoreFormatError('Snapshot contains duplicate subagent invocation origins.');
745
+ }
746
+ origins.add(origin);
747
+ const ancestors = new Set([session.sessionId]);
748
+ let current: IValidatedSessionIdentity | undefined = session;
749
+ while (current.parentSessionId) {
750
+ if (ancestors.has(current.parentSessionId)) {
751
+ throw new FlexHarnessStoreFormatError('Snapshot session relationships contain a cycle.');
752
+ }
753
+ ancestors.add(current.parentSessionId);
754
+ current = sessions.get(current.parentSessionId);
755
+ if (!current) break;
657
756
  }
658
- sessionIds.add(sessionId);
659
757
  }
660
758
  if (!Array.isArray(snapshot.tombstones)) {
661
759
  throw new FlexHarnessStoreFormatError('Snapshot tombstones must be an array.');
662
760
  }
663
761
  const tombstoneIds = new Set<string>();
762
+ const tombstones = new Map<string, {
763
+ sessionId: string;
764
+ rootSessionId?: string;
765
+ depth?: number;
766
+ parentSessionId?: string;
767
+ }>();
664
768
  for (let index = 0; index < snapshot.tombstones.length; index++) {
665
769
  const path = `$snapshot.tombstones[${index}]`;
666
770
  const tombstone = requireRecord(snapshot.tombstones[index], path);
667
- requireOnlyKeys(tombstone, ['sessionId', 'deletedAt'], path);
771
+ requireOnlyKeys(
772
+ tombstone,
773
+ ['sessionId', 'deletedAt', 'rootSessionId', 'depth', 'parentSessionId'],
774
+ path,
775
+ );
668
776
  const sessionId = requireString(tombstone.sessionId, `${path}.sessionId`);
669
777
  requireString(tombstone.deletedAt, `${path}.deletedAt`);
778
+ const parentSessionId = tombstone.parentSessionId === undefined
779
+ ? undefined
780
+ : requireString(tombstone.parentSessionId, `${path}.parentSessionId`);
781
+ if (parentSessionId === sessionId) {
782
+ throw new FlexHarnessStoreFormatError(`${path} cannot be its own parent.`);
783
+ }
784
+ const hasRoot = tombstone.rootSessionId !== undefined;
785
+ const hasDepth = tombstone.depth !== undefined;
786
+ if (hasRoot !== hasDepth) {
787
+ throw new FlexHarnessStoreFormatError(`${path} has partial tombstone group fields.`);
788
+ }
789
+ if (hasRoot) {
790
+ requireString(tombstone.rootSessionId, `${path}.rootSessionId`);
791
+ requireNonNegativeInteger(tombstone.depth, `${path}.depth`);
792
+ if (tombstone.depth === 0 && tombstone.rootSessionId !== sessionId) {
793
+ throw new FlexHarnessStoreFormatError(`${path} has an invalid tombstone group root.`);
794
+ }
795
+ if (tombstone.depth !== 0 && tombstone.rootSessionId === sessionId) {
796
+ throw new FlexHarnessStoreFormatError(`${path} has an invalid tombstone group depth.`);
797
+ }
798
+ }
670
799
  if (tombstoneIds.has(sessionId)) {
671
800
  throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate tombstone "${sessionId}".`);
672
801
  }
@@ -676,6 +805,71 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
676
805
  );
677
806
  }
678
807
  tombstoneIds.add(sessionId);
808
+ tombstones.set(sessionId, {
809
+ sessionId,
810
+ ...(hasRoot ? { rootSessionId: tombstone.rootSessionId as string } : {}),
811
+ ...(hasDepth ? { depth: tombstone.depth as number } : {}),
812
+ ...(parentSessionId === undefined ? {} : { parentSessionId }),
813
+ });
814
+ }
815
+ for (const [index, tombstone] of snapshot.tombstones.entries()) {
816
+ if (tombstone.rootSessionId === undefined) continue;
817
+ const root = tombstones.get(tombstone.rootSessionId);
818
+ if (!root || root.rootSessionId !== root.sessionId || root.depth !== 0) {
819
+ throw new FlexHarnessStoreFormatError(
820
+ `$snapshot.tombstones[${index}] references an invalid cleanup group root.`,
821
+ );
822
+ }
823
+ }
824
+ for (const tombstone of tombstones.values()) {
825
+ if (tombstone.depth !== undefined && tombstone.depth > 0 && !tombstone.parentSessionId) {
826
+ throw new FlexHarnessStoreFormatError(
827
+ `Snapshot grouped tombstone "${tombstone.sessionId}" must retain its parent.`,
828
+ );
829
+ }
830
+ if (!tombstone.parentSessionId) continue;
831
+ const parentSession = sessions.get(tombstone.parentSessionId);
832
+ const parentTombstone = tombstones.get(tombstone.parentSessionId);
833
+ if (!parentSession && !parentTombstone) {
834
+ throw new FlexHarnessStoreFormatError(
835
+ `Snapshot tombstone "${tombstone.sessionId}" has no parent "${tombstone.parentSessionId}".`,
836
+ );
837
+ }
838
+ if (tombstone.depth !== undefined && tombstone.depth > 0) {
839
+ if (
840
+ !parentTombstone
841
+ || parentTombstone.rootSessionId !== tombstone.rootSessionId
842
+ || parentTombstone.depth !== tombstone.depth - 1
843
+ ) {
844
+ throw new FlexHarnessStoreFormatError(
845
+ `Snapshot tombstone "${tombstone.sessionId}" has inconsistent group ancestry.`,
846
+ );
847
+ }
848
+ }
849
+ const ancestors = new Set([tombstone.sessionId]);
850
+ let currentId: string | undefined = tombstone.sessionId;
851
+ while (currentId) {
852
+ const currentParent: string | undefined = tombstones.get(currentId)?.parentSessionId
853
+ ?? sessions.get(currentId)?.parentSessionId;
854
+ if (!currentParent) break;
855
+ if (ancestors.has(currentParent)) {
856
+ throw new FlexHarnessStoreFormatError('Snapshot session and tombstone relationships contain a cycle.');
857
+ }
858
+ ancestors.add(currentParent);
859
+ currentId = currentParent;
860
+ }
861
+ }
862
+ const tombstoned = tombstoneIds;
863
+ for (const session of sessions.values()) {
864
+ let ancestorId = session.parentSessionId;
865
+ while (ancestorId) {
866
+ if (tombstoned.has(ancestorId)) {
867
+ throw new FlexHarnessStoreFormatError(
868
+ `Snapshot live session "${session.sessionId}" descends from a tombstoned ancestor.`,
869
+ );
870
+ }
871
+ ancestorId = sessions.get(ancestorId)?.parentSessionId;
872
+ }
679
873
  }
680
874
  }
681
875