@modelprofile.com/flexharness 5.1.0 → 5.3.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
@@ -155,6 +155,7 @@ export interface IFlexSession {
155
155
 
156
156
  export interface IFlexCreateSessionOptions {
157
157
  sessionId?: string;
158
+ sessionGenerationId?: string;
158
159
  title?: string;
159
160
  }
160
161
 
@@ -342,7 +343,7 @@ export interface IFlexScopeResolver<TScope> {
342
343
  resolveScope(scopeId: string): Promise<IFlexResolvedScope<TScope>> | IFlexResolvedScope<TScope>;
343
344
  }
344
345
 
345
- export interface IFlexModelResolverContext<TScope> {
346
+ export interface IFlexModelResolverContext<TScope> extends IFlexSessionGeneration {
346
347
  scopeId: string;
347
348
  scope: TScope;
348
349
  sessionId: string;
@@ -376,7 +377,8 @@ export interface IFlexPermissionRequestInput {
376
377
  metadata?: TJsonValue;
377
378
  }
378
379
 
379
- export interface IFlexPermissionRequest extends IFlexPermissionRequestInput {
380
+ export interface IFlexPermissionRequest
381
+ extends IFlexPermissionRequestInput, IFlexSessionGeneration {
380
382
  permissionId: string;
381
383
  scopeId: string;
382
384
  sessionId: string;
@@ -573,12 +575,27 @@ export interface IFlexPermissionSnapshot {
573
575
 
574
576
  export const FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION = 1 as const;
575
577
  export const FLEX_SESSION_GENERATION_ID_MAX_BYTES = 128;
578
+ export const FLEX_SESSION_GENERATION_COHORT_MAX_ENTRIES = 2048;
576
579
 
577
580
  export interface IFlexSessionGeneration {
578
581
  sessionGenerationId: string;
579
582
  sessionGenerationSequence: number;
580
583
  }
581
584
 
585
+ export interface IFlexSessionGenerationCohortEntry extends IFlexSessionGeneration {
586
+ sessionId: string;
587
+ }
588
+
589
+ export interface IFlexDeleteSessionGenerationCohortInput {
590
+ root: IFlexSessionGenerationCohortEntry;
591
+ /** Exact authorized entries ordered by strictly ascending sessionId. */
592
+ authorizedCohort: readonly IFlexSessionGenerationCohortEntry[];
593
+ }
594
+
595
+ export interface IFlexDeleteSessionGenerationCohortResult {
596
+ readonly matched: boolean;
597
+ }
598
+
582
599
  export const FLEX_PROJECT_MANAGEMENT_LIMITS = Object.freeze({
583
600
  maxGoalBytes: 8 * 1024,
584
601
  maxScratchpadBytes: 128 * 1024,
@@ -927,6 +944,7 @@ export interface IFlexRedoSessionResult {
927
944
  export type TFlexExternalErrorSource =
928
945
  | 'modelResolver'
929
946
  | 'toolProvider'
947
+ | 'delegatedRunAdmissionProvider'
930
948
  | 'toolExecution'
931
949
  | 'toolCallback'
932
950
  | 'toolCleanup'
@@ -947,6 +965,37 @@ export type TFlexExternalErrorProjector = (
947
965
  context: IFlexExternalErrorContext,
948
966
  ) => IFlexErrorInfo;
949
967
 
968
+ export interface IFlexDelegatedRunAdmissionContext<TScope> extends IFlexSessionGeneration {
969
+ readonly scopeId: string;
970
+ readonly scope: TScope;
971
+ readonly storageKey: string;
972
+ readonly sessionId: string;
973
+ readonly sessionGenerationId: string;
974
+ readonly sessionGenerationSequence: number;
975
+ readonly queueId: string;
976
+ readonly runId: string;
977
+ readonly parentSessionId: string;
978
+ readonly parentSessionGenerationId: string;
979
+ readonly parentSessionGenerationSequence: number;
980
+ readonly parentQueueId: string;
981
+ readonly parentRunId: string;
982
+ readonly parentToolCallId: string;
983
+ readonly agent: string;
984
+ readonly depth: number;
985
+ readonly signal: AbortSignal;
986
+ }
987
+
988
+ export interface IFlexDelegatedRunAdmissionLease {
989
+ /** Must be idempotent and safe to retry after a rejected close attempt. */
990
+ close(): Promise<void> | void;
991
+ }
992
+
993
+ export interface IFlexDelegatedRunAdmissionProvider<TScope> {
994
+ acquireDelegatedRunAdmission(
995
+ context: IFlexDelegatedRunAdmissionContext<TScope>,
996
+ ): Promise<IFlexDelegatedRunAdmissionLease> | IFlexDelegatedRunAdmissionLease;
997
+ }
998
+
950
999
  export interface IFlexExecutionContextProviderContext<TScope> {
951
1000
  scopeId: string;
952
1001
  scope: TScope;
@@ -1018,6 +1067,7 @@ export interface IFlexHarnessOptions<TScope> {
1018
1067
  toolProvider?: IFlexToolProvider<TScope>;
1019
1068
  resourceToolProviderResolver?: IFlexResourceToolProviderResolver<TScope>;
1020
1069
  executionContextProvider?: IFlexExecutionContextProvider<TScope>;
1070
+ delegatedRunAdmissionProvider?: IFlexDelegatedRunAdmissionProvider<TScope>;
1021
1071
  stores?: IFlexHarnessStores;
1022
1072
  agentSessionPolicy?: IFlexAgentSessionPolicy<TScope>;
1023
1073
  toolOutputLimits?: IFlexJsonLimits;
@@ -1070,7 +1120,7 @@ export interface IFlexEventArchiveMetadata {
1070
1120
  eventCount: number;
1071
1121
  }
1072
1122
 
1073
- export interface IFlexEventBase {
1123
+ export interface IFlexEventBase extends IFlexSessionGeneration {
1074
1124
  readonly eventId: string;
1075
1125
  readonly sequence: number;
1076
1126
  readonly timestamp: string;
@@ -1,9 +1,15 @@
1
1
  import { FlexHarnessValidationError } from './errors.js';
2
- import { FLEX_PROJECT_MANAGEMENT_LIMITS } from './interfaces.js';
2
+ import {
3
+ FLEX_PROJECT_MANAGEMENT_LIMITS,
4
+ FLEX_SESSION_GENERATION_COHORT_MAX_ENTRIES,
5
+ FLEX_SESSION_GENERATION_ID_MAX_BYTES,
6
+ } from './interfaces.js';
3
7
  import type {
4
8
  IFlexCreateProjectTaskInput,
9
+ IFlexDeleteSessionGenerationCohortInput,
5
10
  IFlexErrorInfo,
6
11
  IFlexPromptOptions,
12
+ IFlexSessionGenerationCohortEntry,
7
13
  IFlexSlashCommandExecutionOptions,
8
14
  IFlexUpdateProjectTaskInput,
9
15
  IFlexUpdateSessionOptions,
@@ -14,6 +20,7 @@ import type {
14
20
  const maxProjectedErrorNameBytes = 128;
15
21
  const maxProjectedErrorMessageBytes = 2048;
16
22
  const maxProjectedErrorCodeBytes = 128;
23
+ const controlCharacterPattern = /[\u0000-\u001f\u007f]/u;
17
24
 
18
25
  export function validateIdentifier(value: string, name: string): void {
19
26
  if (typeof value !== 'string' || value.length === 0) {
@@ -38,6 +45,123 @@ export function validateUtf8String(
38
45
  }
39
46
  }
40
47
 
48
+ export function validateSessionGenerationId(
49
+ value: unknown,
50
+ name = 'sessionGenerationId',
51
+ ): asserts value is string {
52
+ if (
53
+ typeof value !== 'string'
54
+ || !value.trim()
55
+ || Buffer.byteLength(value, 'utf8') > FLEX_SESSION_GENERATION_ID_MAX_BYTES
56
+ || controlCharacterPattern.test(value)
57
+ ) {
58
+ throw new FlexHarnessValidationError(
59
+ `${name} must be a non-empty string without control characters of at most ${FLEX_SESSION_GENERATION_ID_MAX_BYTES} UTF-8 bytes.`,
60
+ );
61
+ }
62
+ }
63
+
64
+ function validateSessionGenerationSequence(value: unknown, name: string): asserts value is number {
65
+ if (!Number.isSafeInteger(value) || Number(value) < 1) {
66
+ throw new FlexHarnessValidationError(`${name} must be a positive integer.`);
67
+ }
68
+ }
69
+
70
+ function validateSessionGenerationCohortEntry(
71
+ value: unknown,
72
+ name: string,
73
+ ): Readonly<IFlexSessionGenerationCohortEntry> {
74
+ if (
75
+ !value
76
+ || typeof value !== 'object'
77
+ || Array.isArray(value)
78
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
79
+ ) {
80
+ throw new FlexHarnessValidationError(`${name} must be a plain object.`);
81
+ }
82
+ const entry = value as Record<string, unknown>;
83
+ const keys = Reflect.ownKeys(entry);
84
+ if (
85
+ keys.length !== 3
86
+ || keys.some((key) => typeof key !== 'string'
87
+ || !['sessionId', 'sessionGenerationId', 'sessionGenerationSequence'].includes(key))
88
+ ) {
89
+ throw new FlexHarnessValidationError(`${name} must use the exact session generation shape.`);
90
+ }
91
+ validateIdentifier(entry.sessionId as string, `${name}.sessionId`);
92
+ validateSessionGenerationId(entry.sessionGenerationId, `${name}.sessionGenerationId`);
93
+ validateSessionGenerationSequence(
94
+ entry.sessionGenerationSequence,
95
+ `${name}.sessionGenerationSequence`,
96
+ );
97
+ return Object.freeze({
98
+ sessionId: entry.sessionId as string,
99
+ sessionGenerationId: entry.sessionGenerationId,
100
+ sessionGenerationSequence: entry.sessionGenerationSequence,
101
+ });
102
+ }
103
+
104
+ export function validateDeleteSessionGenerationCohortInput(
105
+ value: IFlexDeleteSessionGenerationCohortInput,
106
+ ): {
107
+ root: Readonly<IFlexSessionGenerationCohortEntry>;
108
+ authorizedBySessionId: ReadonlyMap<string, Readonly<IFlexSessionGenerationCohortEntry>>;
109
+ } {
110
+ if (
111
+ !value
112
+ || typeof value !== 'object'
113
+ || Array.isArray(value)
114
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
115
+ ) {
116
+ throw new FlexHarnessValidationError(
117
+ 'deleteSessionGenerationCohort input must be a plain object.',
118
+ );
119
+ }
120
+ const keys = Reflect.ownKeys(value);
121
+ if (
122
+ keys.length !== 2
123
+ || keys.some((key) => typeof key !== 'string'
124
+ || !['root', 'authorizedCohort'].includes(key))
125
+ ) {
126
+ throw new FlexHarnessValidationError(
127
+ 'deleteSessionGenerationCohort input must use the exact root and authorizedCohort shape.',
128
+ );
129
+ }
130
+ const root = validateSessionGenerationCohortEntry(value.root, 'root');
131
+ if (!Array.isArray(value.authorizedCohort)) {
132
+ throw new FlexHarnessValidationError('authorizedCohort must be an array.');
133
+ }
134
+ if (value.authorizedCohort.length > FLEX_SESSION_GENERATION_COHORT_MAX_ENTRIES) {
135
+ throw new FlexHarnessValidationError(
136
+ `authorizedCohort exceeds ${FLEX_SESSION_GENERATION_COHORT_MAX_ENTRIES} entries.`,
137
+ );
138
+ }
139
+ const authorizedBySessionId = new Map<
140
+ string,
141
+ Readonly<IFlexSessionGenerationCohortEntry>
142
+ >();
143
+ let previousSessionId: string | undefined;
144
+ for (const [index, rawEntry] of value.authorizedCohort.entries()) {
145
+ const entry = validateSessionGenerationCohortEntry(
146
+ rawEntry,
147
+ `authorizedCohort[${index}]`,
148
+ );
149
+ if (authorizedBySessionId.has(entry.sessionId)) {
150
+ throw new FlexHarnessValidationError(
151
+ `authorizedCohort contains duplicate sessionId "${entry.sessionId}".`,
152
+ );
153
+ }
154
+ if (previousSessionId !== undefined && previousSessionId >= entry.sessionId) {
155
+ throw new FlexHarnessValidationError(
156
+ 'authorizedCohort must be ordered by strictly ascending sessionId.',
157
+ );
158
+ }
159
+ authorizedBySessionId.set(entry.sessionId, entry);
160
+ previousSessionId = entry.sessionId;
161
+ }
162
+ return { root, authorizedBySessionId };
163
+ }
164
+
41
165
  export function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
42
166
  if (
43
167
  !value