@modelprofile.com/flexharness 1.0.1 → 2.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.
@@ -3,6 +3,7 @@ import {
3
3
  FlexHarnessAbortError,
4
4
  FlexHarnessCallbackOverflowError,
5
5
  FlexHarnessClosedError,
6
+ FlexHarnessExternalError,
6
7
  FlexHarnessNotFoundError,
7
8
  FlexHarnessPermissionRejectedError,
8
9
  FlexHarnessPermissionStateError,
@@ -16,14 +17,18 @@ import type {
16
17
  IFlexAttachmentMessagePart,
17
18
  IFlexCallbackLimits,
18
19
  IFlexCreateSessionOptions,
20
+ IFlexErrorInfo,
19
21
  IFlexHarnessOptions,
20
22
  IFlexHarnessSnapshot,
21
23
  IFlexMessage,
24
+ IFlexMessagePage,
25
+ IFlexMessagePageOptions,
22
26
  IFlexModelIdentity,
23
27
  IFlexPartChangedEvent,
24
28
  IFlexPermissionRequest,
25
29
  IFlexPermissionRequestInput,
26
30
  IFlexPromptOptions,
31
+ IFlexPromptAdmission,
27
32
  IFlexPromptResult,
28
33
  IFlexResolvedModel,
29
34
  IFlexResolvedScope,
@@ -39,6 +44,8 @@ import type {
39
44
  TFlexAgentToolCallFinishEvent,
40
45
  TFlexAgentToolSet,
41
46
  TFlexAttachmentSource,
47
+ TFlexExternalErrorProjector,
48
+ TFlexExternalErrorSource,
42
49
  TFlexHarnessEvent,
43
50
  TFlexHarnessEventListener,
44
51
  TFlexMessagePart,
@@ -84,6 +91,10 @@ interface IActiveRun {
84
91
  userMessageId: string;
85
92
  assistantMessageId: string;
86
93
  controller: AbortController;
94
+ admission: Promise<void>;
95
+ resolveAdmission: () => void;
96
+ rejectAdmission: (error: unknown) => void;
97
+ admissionSettled: boolean;
87
98
  ownerCancellation?: FlexHarnessAbortError;
88
99
  internalFailure?: unknown;
89
100
  finalizer: Promise<IFlexPromptResult>;
@@ -117,6 +128,13 @@ interface IFinalMutationResult {
117
128
  terminalParts: TFlexMessagePart[];
118
129
  }
119
130
 
131
+ interface IRunResultProjection {
132
+ text: string;
133
+ steps: number;
134
+ finishReason: string;
135
+ usage: IFlexUsage;
136
+ }
137
+
120
138
  type TEventDetails = Record<string, unknown> & {
121
139
  type: TFlexHarnessEvent['type'];
122
140
  };
@@ -132,11 +150,34 @@ interface IResolverOutcome<T> {
132
150
  error?: unknown;
133
151
  }
134
152
 
153
+ interface IFlexMessageCursor {
154
+ version: 1;
155
+ namespace: string;
156
+ sessionId: string;
157
+ anchorMessageId: string;
158
+ }
159
+
135
160
  const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
136
161
  maxEvents: 10_000,
137
162
  maxOutputBytes: 1024 * 1024,
138
163
  maxParts: 2_000,
139
164
  };
165
+ const maxMessagePageSize = 50;
166
+ const maxMessagePageCursorBytes = 4096;
167
+ const maxTransferIdentifierBytes = 512;
168
+ const maxTransferMetadataBytes = 2048;
169
+ const maxTransferTextBytes = 96 * 1024;
170
+ const maxTransferMessageBytes = 480 * 1024;
171
+ const maxTransferPageBytes = 512 * 1024;
172
+ const maxProjectedErrorNameBytes = 128;
173
+ const maxProjectedErrorMessageBytes = 2048;
174
+ const maxProjectedErrorCodeBytes = 128;
175
+ const externalErrorFallback: IFlexErrorInfo = Object.freeze({
176
+ name: 'FlexHarnessExternalError',
177
+ message: 'The model operation failed.',
178
+ code: 'FLEX_EXTERNAL_ERROR',
179
+ });
180
+ const maxDetachedCleanupErrors = 100;
140
181
 
141
182
  function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
142
183
  return Boolean(
@@ -149,15 +190,21 @@ function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
149
190
  async function* normalizeAsyncIterable(
150
191
  iterable: AsyncIterable<unknown>,
151
192
  limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
193
+ projectError: (error: unknown) => Error,
152
194
  ): AsyncGenerator<unknown> {
153
- for await (const value of iterable) {
154
- yield normalizeJsonValue(value, limits);
195
+ try {
196
+ for await (const value of iterable) {
197
+ yield normalizeJsonValue(value, limits);
198
+ }
199
+ } catch (error) {
200
+ throw projectError(error);
155
201
  }
156
202
  }
157
203
 
158
204
  function wrapToolSet(
159
205
  tools: TFlexAgentToolSet,
160
206
  limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
207
+ projectError: (error: unknown) => Error,
161
208
  ): TFlexAgentToolSet {
162
209
  const wrapped: Record<string, unknown> = {};
163
210
  for (const [name, tool] of Object.entries(tools)) {
@@ -170,11 +217,31 @@ function wrapToolSet(
170
217
  wrapped[name] = {
171
218
  ...toolRecord,
172
219
  execute(input: unknown, options: unknown): unknown {
173
- const output = execute.call(tool, input, options);
174
- if (isAsyncIterable(output)) {
175
- return normalizeAsyncIterable(output, limits);
220
+ let output: unknown;
221
+ try {
222
+ output = execute.call(tool, input, options);
223
+ } catch (error) {
224
+ throw projectError(error);
225
+ }
226
+ try {
227
+ if (isAsyncIterable(output)) {
228
+ return normalizeAsyncIterable(output, limits, projectError);
229
+ }
230
+ } catch (error) {
231
+ throw projectError(error);
176
232
  }
177
- return Promise.resolve(output).then((value) => normalizeJsonValue(value, limits));
233
+ return Promise.resolve(output).then(
234
+ (value) => {
235
+ try {
236
+ return normalizeJsonValue(value, limits);
237
+ } catch (error) {
238
+ throw projectError(error);
239
+ }
240
+ },
241
+ (error: unknown) => {
242
+ throw projectError(error);
243
+ },
244
+ );
178
245
  },
179
246
  };
180
247
  }
@@ -185,25 +252,216 @@ function publicSnapshot<T>(value: T): T {
185
252
  return deepFreeze(cloneSerializable(value));
186
253
  }
187
254
 
255
+ function jsonBytes(value: unknown): number {
256
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
257
+ }
258
+
259
+ function truncateUtf8(value: string, maxBytes: number): string {
260
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
261
+ const suffix = ' [truncated]';
262
+ const suffixBytes = Buffer.byteLength(suffix, 'utf8');
263
+ let truncated = Buffer.from(value, 'utf8')
264
+ .subarray(0, Math.max(0, maxBytes - suffixBytes))
265
+ .toString('utf8')
266
+ .replace(/\uFFFD$/u, '');
267
+ while (Buffer.byteLength(`${truncated}${suffix}`, 'utf8') > maxBytes) {
268
+ truncated = truncated.slice(0, -1);
269
+ }
270
+ return `${truncated}${suffix}`;
271
+ }
272
+
273
+ function requireTransferIdentifier(value: string, field: string): void {
274
+ if (Buffer.byteLength(value, 'utf8') > maxTransferIdentifierBytes) {
275
+ throw new FlexHarnessValidationError(`Stored ${field} exceeds the transfer limit.`);
276
+ }
277
+ }
278
+
279
+ function createBoundedTransferMessage(message: IFlexMessage): IFlexMessage {
280
+ const projected = cloneSerializable(message);
281
+ requireTransferIdentifier(projected.messageId, 'messageId');
282
+ requireTransferIdentifier(projected.sessionId, 'sessionId');
283
+ requireTransferIdentifier(projected.runId, 'runId');
284
+ projected.createdAt = truncateUtf8(projected.createdAt, 128);
285
+ if (projected.completedAt !== undefined) {
286
+ projected.completedAt = truncateUtf8(projected.completedAt, 128);
287
+ }
288
+ if (projected.error !== undefined) {
289
+ projected.error = truncateUtf8(projected.error, maxTransferMetadataBytes);
290
+ }
291
+ if (projected.model) {
292
+ projected.model.provider = truncateUtf8(projected.model.provider, maxTransferIdentifierBytes);
293
+ projected.model.model = truncateUtf8(projected.model.model, maxTransferIdentifierBytes);
294
+ if (projected.model.displayName !== undefined) {
295
+ projected.model.displayName = truncateUtf8(
296
+ projected.model.displayName,
297
+ maxTransferMetadataBytes,
298
+ );
299
+ }
300
+ if (projected.model.variant !== undefined) {
301
+ projected.model.variant = truncateUtf8(projected.model.variant, 128);
302
+ }
303
+ }
304
+ for (const part of projected.parts) {
305
+ requireTransferIdentifier(part.partId, 'partId');
306
+ if (part.type === 'text' || part.type === 'reasoning') {
307
+ part.text = truncateUtf8(part.text, maxTransferTextBytes);
308
+ } else if (part.type === 'tool') {
309
+ part.toolCallId = truncateUtf8(part.toolCallId, maxTransferIdentifierBytes);
310
+ part.toolName = truncateUtf8(part.toolName, maxTransferIdentifierBytes);
311
+ if (part.error !== undefined) {
312
+ part.error = truncateUtf8(part.error, maxTransferMetadataBytes);
313
+ }
314
+ } else {
315
+ if (part.mediaType !== undefined) {
316
+ part.mediaType = truncateUtf8(part.mediaType, maxTransferIdentifierBytes);
317
+ }
318
+ if (part.name !== undefined) {
319
+ part.name = truncateUtf8(part.name, maxTransferMetadataBytes);
320
+ }
321
+ }
322
+ }
323
+ if (jsonBytes(projected) > maxTransferMessageBytes) {
324
+ projected.parts = [{
325
+ partId: 'transfer-elided',
326
+ type: 'text',
327
+ text: '[elided: message exceeds the transfer budget]',
328
+ }];
329
+ }
330
+ if (jsonBytes(projected) > maxTransferMessageBytes) {
331
+ throw new FlexHarnessValidationError('Stored message metadata exceeds the transfer limit.');
332
+ }
333
+ return projected;
334
+ }
335
+
336
+ function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
337
+ if (
338
+ !value
339
+ || typeof value !== 'object'
340
+ || Array.isArray(value)
341
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
342
+ ) {
343
+ throw new FlexHarnessValidationError('External error projection is invalid.');
344
+ }
345
+ const descriptors = Object.getOwnPropertyDescriptors(value);
346
+ const keys = Reflect.ownKeys(descriptors);
347
+ const unsupportedKey = keys.find(
348
+ (key) => typeof key !== 'string' || (key !== 'name' && key !== 'message' && key !== 'code'),
349
+ );
350
+ const name = descriptors.name?.value;
351
+ const message = descriptors.message?.value;
352
+ const code = descriptors.code?.value;
353
+ if (
354
+ unsupportedKey
355
+ || descriptors.name?.get !== undefined
356
+ || descriptors.name?.set !== undefined
357
+ || descriptors.message?.get !== undefined
358
+ || descriptors.message?.set !== undefined
359
+ || descriptors.code?.get !== undefined
360
+ || descriptors.code?.set !== undefined
361
+ || typeof name !== 'string'
362
+ || name.length === 0
363
+ || Buffer.byteLength(name, 'utf8') > maxProjectedErrorNameBytes
364
+ || typeof message !== 'string'
365
+ || message.length === 0
366
+ || Buffer.byteLength(message, 'utf8') > maxProjectedErrorMessageBytes
367
+ || (code !== undefined
368
+ && (
369
+ typeof code !== 'string'
370
+ || code.length === 0
371
+ || Buffer.byteLength(code, 'utf8') > maxProjectedErrorCodeBytes
372
+ ))
373
+ ) {
374
+ throw new FlexHarnessValidationError('External error projection is invalid.');
375
+ }
376
+ return Object.freeze({ name, message, ...(code === undefined ? {} : { code }) });
377
+ }
378
+
188
379
  function validateIdentifier(value: string, name: string): void {
189
380
  if (typeof value !== 'string' || value.length === 0) {
190
381
  throw new FlexHarnessValidationError(`${name} must be a non-empty string.`);
191
382
  }
192
383
  }
193
384
 
194
- function validateModelIdentity(identity: IFlexModelIdentity): void {
195
- validateIdentifier(identity.provider, 'model identity provider');
196
- validateIdentifier(identity.model, 'model identity model');
197
- if (identity.displayName !== undefined) {
198
- validateIdentifier(identity.displayName, 'model identity displayName');
385
+ function normalizeModelIdentity(identity: IFlexModelIdentity): IFlexModelIdentity {
386
+ const provider = identity.provider;
387
+ const model = identity.model;
388
+ const displayName = identity.displayName;
389
+ const variant = identity.variant;
390
+ const normalized: IFlexModelIdentity = {
391
+ provider,
392
+ model,
393
+ ...(displayName === undefined ? {} : { displayName }),
394
+ ...(variant === undefined ? {} : { variant }),
395
+ };
396
+ validateIdentifier(normalized.provider, 'model identity provider');
397
+ validateIdentifier(normalized.model, 'model identity model');
398
+ if (normalized.displayName !== undefined) {
399
+ validateIdentifier(normalized.displayName, 'model identity displayName');
400
+ }
401
+ if (normalized.variant !== undefined) {
402
+ validateIdentifier(normalized.variant, 'model identity variant');
403
+ }
404
+ if (
405
+ Buffer.byteLength(normalized.provider, 'utf8') > maxTransferIdentifierBytes
406
+ || Buffer.byteLength(normalized.model, 'utf8') > maxTransferIdentifierBytes
407
+ || (normalized.displayName !== undefined
408
+ && Buffer.byteLength(normalized.displayName, 'utf8') > maxTransferMetadataBytes)
409
+ || (normalized.variant !== undefined && Buffer.byteLength(normalized.variant, 'utf8') > 128)
410
+ ) {
411
+ throw new FlexHarnessValidationError('Model identity exceeds its transfer limit.');
199
412
  }
413
+ return normalized;
414
+ }
415
+
416
+ function normalizeResolvedModel(resolvedModel: IFlexResolvedModel): IFlexResolvedModel {
417
+ const model = resolvedModel.model;
418
+ const identity = normalizeModelIdentity(resolvedModel.identity);
419
+ const system = resolvedModel.system;
420
+ const providerOptions = resolvedModel.providerOptions;
421
+ const cache = resolvedModel.cache;
422
+ const maxSteps = resolvedModel.maxSteps;
423
+ return {
424
+ model,
425
+ identity,
426
+ ...(system === undefined ? {} : { system }),
427
+ ...(providerOptions === undefined ? {} : { providerOptions }),
428
+ ...(cache === undefined ? {} : { cache }),
429
+ ...(maxSteps === undefined ? {} : { maxSteps }),
430
+ };
431
+ }
432
+
433
+ function normalizeRunResult(result: TFlexAgentRunResult): IRunResultProjection {
434
+ const normalized: IRunResultProjection = {
435
+ text: result.text,
436
+ steps: result.steps,
437
+ finishReason: result.finishReason,
438
+ usage: {
439
+ inputTokens: result.usage.inputTokens,
440
+ outputTokens: result.usage.outputTokens,
441
+ totalTokens: result.usage.totalTokens,
442
+ cacheReadTokens: result.usage.cacheReadTokens,
443
+ cacheWriteTokens: result.usage.cacheWriteTokens,
444
+ },
445
+ };
446
+ if (
447
+ typeof normalized.text !== 'string'
448
+ || typeof normalized.finishReason !== 'string'
449
+ || !Number.isSafeInteger(normalized.steps)
450
+ || normalized.steps < 0
451
+ || Object.values(normalized.usage).some((value) => !Number.isFinite(value) || value < 0)
452
+ ) {
453
+ throw new FlexHarnessValidationError('Agent runner result is invalid.');
454
+ }
455
+ return normalized;
200
456
  }
201
457
 
202
458
  function combineErrors(errors: unknown[]): unknown {
203
459
  if (errors.length === 1) {
204
460
  return errors[0];
205
461
  }
206
- return new FlexHarnessRunError(errors);
462
+ const combined = new FlexHarnessRunError(errors);
463
+ Object.freeze(combined.errors);
464
+ return Object.freeze(combined);
207
465
  }
208
466
 
209
467
  function isAbortError(error: unknown): boolean {
@@ -214,7 +472,7 @@ function isAbortError(error: unknown): boolean {
214
472
  );
215
473
  }
216
474
 
217
- function makeUsage(result: TFlexAgentRunResult): IFlexUsage {
475
+ function makeUsage(result: IRunResultProjection): IFlexUsage {
218
476
  return {
219
477
  inputTokens: result.usage.inputTokens,
220
478
  outputTokens: result.usage.outputTokens,
@@ -329,9 +587,12 @@ export class FlexHarness<TScope = unknown> {
329
587
  NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>
330
588
  >;
331
589
  private readonly callbackLimits: Required<IFlexCallbackLimits>;
590
+ private readonly externalErrorProjector?: TFlexExternalErrorProjector;
332
591
  private readonly stateLoads = new Map<string, Promise<IStorageState>>();
333
592
  private readonly listeners = new Set<TFlexHarnessEventListener>();
334
593
  private readonly detachedToolCleanups = new Set<Promise<void>>();
594
+ private readonly detachedToolCleanupErrors: Error[] = [];
595
+ private readonly trustedInternalErrors = new WeakSet<object>();
335
596
  private sequence = 0;
336
597
  private closed = false;
337
598
  private disposePromise?: Promise<void>;
@@ -344,6 +605,7 @@ export class FlexHarness<TScope = unknown> {
344
605
  this.runner = options.runner ?? plugins.runAgent;
345
606
  this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
346
607
  this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
608
+ this.externalErrorProjector = options.externalErrorProjector;
347
609
  }
348
610
 
349
611
  public async listSessions(scopeId: string): Promise<IFlexSession[]> {
@@ -453,12 +715,122 @@ export class FlexHarness<TScope = unknown> {
453
715
  return publicSnapshot(this.requireSession(state, sessionId).messages);
454
716
  }
455
717
 
718
+ public async listMessagePage(
719
+ scopeId: string,
720
+ sessionId: string,
721
+ options: IFlexMessagePageOptions = {},
722
+ ): Promise<IFlexMessagePage> {
723
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
724
+ throw new FlexHarnessValidationError('Message page options must be a plain object.');
725
+ }
726
+ const unsupportedKey = Object.keys(options).find(
727
+ (key) => key !== 'limit' && key !== 'before',
728
+ );
729
+ if (unsupportedKey) {
730
+ throw new FlexHarnessValidationError(
731
+ `Message page options do not support "${unsupportedKey}".`,
732
+ );
733
+ }
734
+ const limit = options.limit ?? maxMessagePageSize;
735
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > maxMessagePageSize) {
736
+ throw new FlexHarnessValidationError(
737
+ `Message page limit must be an integer from 1 through ${maxMessagePageSize}.`,
738
+ );
739
+ }
740
+ if (
741
+ options.before !== undefined
742
+ && (
743
+ typeof options.before !== 'string'
744
+ || options.before.length === 0
745
+ || Buffer.byteLength(options.before, 'utf8') > maxMessagePageCursorBytes
746
+ )
747
+ ) {
748
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
749
+ }
750
+ validateIdentifier(sessionId, 'sessionId');
751
+ requireTransferIdentifier(sessionId, 'sessionId');
752
+ const { state } = await this.resolveState(scopeId);
753
+ await state.saveTail;
754
+ const stored = this.requireSession(state, sessionId);
755
+ const namespace = this.messageCursorNamespace(state.storageKey);
756
+ let end = stored.messages.length;
757
+ if (options.before !== undefined) {
758
+ const cursor = this.parseMessageCursor(options.before);
759
+ if (cursor.namespace !== namespace || cursor.sessionId !== sessionId) {
760
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
761
+ }
762
+ const anchorIndex = stored.messages.findIndex(
763
+ (message) => message.messageId === cursor.anchorMessageId,
764
+ );
765
+ if (anchorIndex < 0) {
766
+ throw new FlexHarnessValidationError('Message page cursor is stale.');
767
+ }
768
+ end = anchorIndex;
769
+ }
770
+
771
+ let start = end;
772
+ let messages: IFlexMessage[] = [];
773
+ for (let index = end - 1; index >= 0 && messages.length < limit; index--) {
774
+ const candidate = createBoundedTransferMessage(stored.messages[index]);
775
+ const candidateMessages = [candidate, ...messages];
776
+ const nextCursor = index > 0
777
+ ? this.createMessageCursor(namespace, sessionId, candidate.messageId)
778
+ : undefined;
779
+ const candidatePage: IFlexMessagePage = {
780
+ messages: candidateMessages,
781
+ ...(nextCursor === undefined ? {} : { nextCursor }),
782
+ };
783
+ if (jsonBytes(candidatePage) > maxTransferPageBytes) break;
784
+ start = index;
785
+ messages = candidateMessages;
786
+ }
787
+ if (messages.length === 0 && end > 0) {
788
+ throw new FlexHarnessValidationError('Stored message exceeds the page transfer limit.');
789
+ }
790
+ const nextCursor = start > 0
791
+ ? this.createMessageCursor(namespace, sessionId, messages[0].messageId)
792
+ : undefined;
793
+ const page: IFlexMessagePage = {
794
+ messages,
795
+ ...(nextCursor === undefined ? {} : { nextCursor }),
796
+ };
797
+ if (jsonBytes(page) > maxTransferPageBytes) {
798
+ throw new FlexHarnessValidationError('Message page exceeds the transfer limit.');
799
+ }
800
+ return publicSnapshot(page);
801
+ }
802
+
803
+ public async getMessage(
804
+ scopeId: string,
805
+ sessionId: string,
806
+ messageId: string,
807
+ ): Promise<IFlexMessage> {
808
+ validateIdentifier(sessionId, 'sessionId');
809
+ requireTransferIdentifier(sessionId, 'sessionId');
810
+ validateIdentifier(messageId, 'messageId');
811
+ requireTransferIdentifier(messageId, 'messageId');
812
+ const { state } = await this.resolveState(scopeId);
813
+ await state.saveTail;
814
+ const message = this.requireMessage(this.requireSession(state, sessionId), messageId);
815
+ return publicSnapshot(createBoundedTransferMessage(message));
816
+ }
817
+
456
818
  public async prompt(
457
819
  scopeId: string,
458
820
  sessionId: string,
459
821
  prompt: TFlexPrompt,
460
822
  options: IFlexPromptOptions = {},
461
823
  ): Promise<IFlexPromptResult> {
824
+ const admission = await this.startPrompt(scopeId, sessionId, prompt, options);
825
+ return admission.completion;
826
+ }
827
+
828
+ public async startPrompt(
829
+ scopeId: string,
830
+ sessionId: string,
831
+ prompt: TFlexPrompt,
832
+ options: IFlexPromptOptions = {},
833
+ ): Promise<IFlexPromptAdmission> {
462
834
  const normalizedPrompt = normalizeFlexPrompt(prompt);
463
835
  if (options.maxSteps !== undefined && (!Number.isSafeInteger(options.maxSteps) || options.maxSteps < 1)) {
464
836
  throw new FlexHarnessValidationError('maxSteps must be a positive integer.');
@@ -471,6 +843,19 @@ export class FlexHarness<TScope = unknown> {
471
843
  throw new FlexHarnessSessionBusyError(sessionId);
472
844
  }
473
845
 
846
+ let resolveAdmission!: () => void;
847
+ let rejectAdmission!: (error: unknown) => void;
848
+ const admission = new Promise<void>((resolve, reject) => {
849
+ resolveAdmission = resolve;
850
+ rejectAdmission = reject;
851
+ });
852
+ void admission.catch(() => undefined);
853
+ let resolveCompletion!: (result: IFlexPromptResult) => void;
854
+ let rejectCompletion!: (error: unknown) => void;
855
+ const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
856
+ resolveCompletion = resolve;
857
+ rejectCompletion = reject;
858
+ });
474
859
  const run: IActiveRun = {
475
860
  scopeId,
476
861
  sessionId,
@@ -478,7 +863,11 @@ export class FlexHarness<TScope = unknown> {
478
863
  userMessageId: plugins.crypto.randomUUID(),
479
864
  assistantMessageId: plugins.crypto.randomUUID(),
480
865
  controller: new AbortController(),
481
- finalizer: Promise.reject(new Error('Run finalizer was not initialized.')),
866
+ admission,
867
+ resolveAdmission,
868
+ rejectAdmission,
869
+ admissionSettled: false,
870
+ finalizer: completion,
482
871
  callbackEventCount: 0,
483
872
  callbackOutputBytes: 0,
484
873
  callbackParts: [],
@@ -491,17 +880,21 @@ export class FlexHarness<TScope = unknown> {
491
880
  };
492
881
  void run.finalizer.catch(() => undefined);
493
882
  resolved.state.activeRuns.set(sessionId, run);
494
- run.finalizer = this.executeRun(resolved, run, normalizedPrompt, options);
495
- return run.finalizer;
883
+ void this.executeRun(resolved, run, normalizedPrompt, options).then(
884
+ resolveCompletion,
885
+ rejectCompletion,
886
+ );
887
+ await run.admission;
888
+ return Object.freeze({ runId: run.runId, completion: run.finalizer });
496
889
  }
497
890
 
498
- public async abort(scopeId: string, sessionId: string, reason?: string): Promise<boolean> {
891
+ public async abort(scopeId: string, sessionId: string, _reason?: string): Promise<boolean> {
499
892
  const { state } = await this.resolveState(scopeId);
500
893
  const run = state.activeRuns.get(sessionId);
501
894
  if (!run || run.phase === 'committing') {
502
895
  return false;
503
896
  }
504
- const error = new FlexHarnessAbortError(reason ?? 'The run was aborted by the caller.');
897
+ const error = Object.freeze(new FlexHarnessAbortError());
505
898
  if (run.internalFailure === undefined) run.ownerCancellation ??= error;
506
899
  this.rejectRunPermissions(state, run, error);
507
900
  run.controller.abort(error);
@@ -572,7 +965,9 @@ export class FlexHarness<TScope = unknown> {
572
965
  for (const run of state.activeRuns.values()) {
573
966
  finalizers.push(run.finalizer);
574
967
  if (run.phase === 'committing') continue;
575
- const error = new FlexHarnessAbortError('The run was aborted because FlexHarness was disposed.');
968
+ const error = Object.freeze(
969
+ new FlexHarnessAbortError('The run was aborted because FlexHarness was disposed.'),
970
+ );
576
971
  if (run.internalFailure === undefined) run.ownerCancellation ??= error;
577
972
  this.rejectRunPermissions(state, run, error);
578
973
  run.controller.abort(error);
@@ -581,13 +976,15 @@ export class FlexHarness<TScope = unknown> {
581
976
  this.rejectPending(
582
977
  state,
583
978
  pending,
584
- new FlexHarnessAbortError('Permission was rejected because FlexHarness was disposed.'),
979
+ Object.freeze(
980
+ new FlexHarnessAbortError('Permission was rejected because FlexHarness was disposed.'),
981
+ ),
585
982
  );
586
983
  }
587
984
  }
588
985
  const results = await Promise.allSettled(finalizers);
589
986
  await Promise.all(states.map((state) => state.saveTail));
590
- const detachedCleanupResults = await Promise.allSettled([...this.detachedToolCleanups]);
987
+ await Promise.all([...this.detachedToolCleanups]);
591
988
  this.listeners.clear();
592
989
  this.stateLoads.clear();
593
990
  this.detachedToolCleanups.clear();
@@ -607,9 +1004,8 @@ export class FlexHarness<TScope = unknown> {
607
1004
  unexpectedErrors.push(result.reason);
608
1005
  }
609
1006
  }
610
- for (const result of detachedCleanupResults) {
611
- if (result.status === 'rejected') unexpectedErrors.push(result.reason);
612
- }
1007
+ unexpectedErrors.push(...this.detachedToolCleanupErrors);
1008
+ this.detachedToolCleanupErrors.length = 0;
613
1009
  if (unexpectedErrors.length > 0) {
614
1010
  throw combineErrors(unexpectedErrors);
615
1011
  }
@@ -623,7 +1019,7 @@ export class FlexHarness<TScope = unknown> {
623
1019
  ): Promise<IFlexPromptResult> {
624
1020
  let modelResolution: IFlexResolvedModel | undefined;
625
1021
  let toolHandle: IFlexToolHandle | undefined;
626
- let result: TFlexAgentRunResult | undefined;
1022
+ let result: IRunResultProjection | undefined;
627
1023
  let serializedResultMessages: TFlexAgentModelMessage[] | undefined;
628
1024
  let originalError: unknown;
629
1025
  let reserved = false;
@@ -649,6 +1045,7 @@ export class FlexHarness<TScope = unknown> {
649
1045
  messageId: run.assistantMessageId,
650
1046
  message: reservation.assistantMessage,
651
1047
  });
1048
+ this.resolveRunAdmission(run);
652
1049
 
653
1050
  const modelOutcome = Promise.resolve()
654
1051
  .then(() => this.modelResolver.resolveModel({
@@ -660,8 +1057,16 @@ export class FlexHarness<TScope = unknown> {
660
1057
  signal: run.controller.signal,
661
1058
  }))
662
1059
  .then(
663
- (value): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: true, value }),
664
- (error: unknown): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: false, error }),
1060
+ (value): IResolverOutcome<IFlexResolvedModel> => ({
1061
+ source: 'model',
1062
+ success: true,
1063
+ value,
1064
+ }),
1065
+ (error: unknown): IResolverOutcome<IFlexResolvedModel> => ({
1066
+ source: 'model',
1067
+ success: false,
1068
+ error,
1069
+ }),
665
1070
  );
666
1071
  const toolOutcome = Promise.resolve()
667
1072
  .then(() =>
@@ -678,8 +1083,16 @@ export class FlexHarness<TScope = unknown> {
678
1083
  : undefined,
679
1084
  )
680
1085
  .then(
681
- (value): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: true, value }),
682
- (error: unknown): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: false, error }),
1086
+ (value): IResolverOutcome<IFlexToolHandle | undefined> => ({
1087
+ source: 'tools',
1088
+ success: true,
1089
+ value,
1090
+ }),
1091
+ (error: unknown): IResolverOutcome<IFlexToolHandle | undefined> => ({
1092
+ source: 'tools',
1093
+ success: false,
1094
+ error,
1095
+ }),
683
1096
  );
684
1097
  let resolveAbort!: (outcome: IResolverOutcome<never>) => void;
685
1098
  const abortOutcome = new Promise<IResolverOutcome<never>>((resolve) => {
@@ -698,27 +1111,41 @@ export class FlexHarness<TScope = unknown> {
698
1111
  const firstResolver = await Promise.race([modelOutcome, toolOutcome, abortOutcome]);
699
1112
  if (!firstResolver.success) {
700
1113
  run.controller.signal.removeEventListener('abort', onResolverAbort);
1114
+ const firstError = firstResolver.source === 'abort'
1115
+ ? firstResolver.error
1116
+ : this.projectExternalError(
1117
+ run,
1118
+ firstResolver.error,
1119
+ firstResolver.source === 'model' ? 'modelResolver' : 'toolProvider',
1120
+ );
701
1121
  if (firstResolver.source !== 'abort') {
702
- this.abortRunInternally(run, firstResolver.error);
1122
+ this.abortRunInternally(run, firstError);
703
1123
  }
704
1124
  if (firstResolver.source !== 'tools') {
705
- this.observeDetachedToolProvider(toolOutcome);
1125
+ this.observeDetachedToolProvider(toolOutcome, run);
706
1126
  }
707
- throw firstResolver.error;
1127
+ throw firstError;
708
1128
  }
709
1129
 
710
- if (firstResolver.source === 'model') {
711
- modelResolution = firstResolver.value as IFlexResolvedModel;
712
- try {
713
- validateModelIdentity(modelResolution.identity);
714
- } catch (error) {
715
- run.controller.signal.removeEventListener('abort', onResolverAbort);
716
- this.abortRunInternally(run, error);
717
- this.observeDetachedToolProvider(toolOutcome);
718
- throw error;
1130
+ try {
1131
+ if (firstResolver.source === 'model') {
1132
+ modelResolution = normalizeResolvedModel(firstResolver.value as IFlexResolvedModel);
1133
+ } else {
1134
+ toolHandle = this.normalizeToolHandle(
1135
+ run,
1136
+ firstResolver.value as IFlexToolHandle | undefined,
1137
+ );
719
1138
  }
720
- } else {
721
- toolHandle = firstResolver.value as IFlexToolHandle | undefined;
1139
+ } catch (error) {
1140
+ const resolverError = firstResolver.source === 'model'
1141
+ ? this.projectExternalError(run, error, 'modelResolver')
1142
+ : error;
1143
+ run.controller.signal.removeEventListener('abort', onResolverAbort);
1144
+ this.abortRunInternally(run, resolverError);
1145
+ if (firstResolver.source === 'model') {
1146
+ this.observeDetachedToolProvider(toolOutcome, run);
1147
+ }
1148
+ throw resolverError;
722
1149
  }
723
1150
 
724
1151
  const secondResolver = await Promise.race([
@@ -727,29 +1154,53 @@ export class FlexHarness<TScope = unknown> {
727
1154
  ]);
728
1155
  run.controller.signal.removeEventListener('abort', onResolverAbort);
729
1156
  if (!secondResolver.success) {
1157
+ const secondError = secondResolver.source === 'abort'
1158
+ ? secondResolver.error
1159
+ : this.projectExternalError(
1160
+ run,
1161
+ secondResolver.error,
1162
+ secondResolver.source === 'model' ? 'modelResolver' : 'toolProvider',
1163
+ );
730
1164
  if (secondResolver.source !== 'abort') {
731
- this.abortRunInternally(run, secondResolver.error);
1165
+ this.abortRunInternally(run, secondError);
732
1166
  }
733
1167
  if (firstResolver.source === 'model' && secondResolver.source === 'abort') {
734
- this.observeDetachedToolProvider(toolOutcome);
1168
+ this.observeDetachedToolProvider(toolOutcome, run);
735
1169
  }
736
- throw secondResolver.error;
1170
+ throw secondError;
737
1171
  }
738
- if (secondResolver.source === 'model') {
739
- modelResolution = secondResolver.value as IFlexResolvedModel;
740
- try {
741
- validateModelIdentity(modelResolution.identity);
742
- } catch (error) {
743
- this.abortRunInternally(run, error);
744
- throw error;
1172
+ try {
1173
+ if (secondResolver.source === 'model') {
1174
+ modelResolution = normalizeResolvedModel(secondResolver.value as IFlexResolvedModel);
1175
+ } else {
1176
+ toolHandle = this.normalizeToolHandle(
1177
+ run,
1178
+ secondResolver.value as IFlexToolHandle | undefined,
1179
+ );
745
1180
  }
746
- } else {
747
- toolHandle = secondResolver.value as IFlexToolHandle | undefined;
1181
+ } catch (error) {
1182
+ const resolverError = secondResolver.source === 'model'
1183
+ ? this.projectExternalError(run, error, 'modelResolver')
1184
+ : error;
1185
+ this.abortRunInternally(run, resolverError);
1186
+ throw resolverError;
748
1187
  }
749
1188
  if (run.controller.signal.aborted) {
750
1189
  throw run.controller.signal.reason;
751
1190
  }
752
1191
 
1192
+ let tools: TFlexAgentToolSet | undefined;
1193
+ try {
1194
+ tools = toolHandle
1195
+ ? wrapToolSet(
1196
+ toolHandle.tools,
1197
+ this.toolOutputLimits,
1198
+ (error) => this.projectExternalError(run, error, 'toolExecution'),
1199
+ )
1200
+ : undefined;
1201
+ } catch (error) {
1202
+ throw this.projectExternalError(run, error, 'toolProvider');
1203
+ }
753
1204
  const runnerOptions: Parameters<typeof this.runner>[0] = {
754
1205
  model: modelResolution!.model,
755
1206
  prompt: prompt.agentPrompt,
@@ -759,7 +1210,7 @@ export class FlexHarness<TScope = unknown> {
759
1210
  ...(options.system ?? modelResolution!.system
760
1211
  ? { system: options.system ?? modelResolution!.system }
761
1212
  : {}),
762
- ...(toolHandle ? { tools: wrapToolSet(toolHandle.tools, this.toolOutputLimits) } : {}),
1213
+ ...(tools ? { tools } : {}),
763
1214
  ...(modelResolution!.providerOptions
764
1215
  ? { providerOptions: modelResolution!.providerOptions }
765
1216
  : {}),
@@ -775,16 +1226,22 @@ export class FlexHarness<TScope = unknown> {
775
1226
  onToolCallFinish: (event) => this.onToolFinish(run, event),
776
1227
  };
777
1228
  run.phase = 'running';
778
- result = await this.runner(runnerOptions);
1229
+ try {
1230
+ const runnerResult = await this.runner(runnerOptions);
1231
+ serializedResultMessages = serializeAgentMessages(runnerResult.messages);
1232
+ result = normalizeRunResult(runnerResult);
1233
+ } catch (error) {
1234
+ throw this.projectExternalError(run, error, 'runner');
1235
+ }
779
1236
  if (run.callbackError) {
780
1237
  throw run.callbackError;
781
1238
  }
782
1239
  if (run.controller.signal.aborted) {
783
1240
  throw run.controller.signal.reason;
784
1241
  }
785
- serializedResultMessages = serializeAgentMessages(result.messages);
786
1242
  } catch (error) {
787
- originalError = error ?? new Error('The run failed without an error value.');
1243
+ const runError = error ?? new Error('The run failed without an error value.');
1244
+ originalError = reserved ? this.projectExternalError(run, runError, 'runner') : runError;
788
1245
  }
789
1246
  run.callbacksClosed = true;
790
1247
  run.phase = 'closing';
@@ -793,15 +1250,18 @@ export class FlexHarness<TScope = unknown> {
793
1250
  ? run.callbackError
794
1251
  : combineErrors([originalError, run.callbackError]);
795
1252
  }
1253
+ if (!reserved) {
1254
+ const admissionError = this.projectExternalError(run, originalError, 'persistence');
1255
+ this.rejectRunAdmission(run, admissionError);
1256
+ if (resolved.state.activeRuns.get(run.sessionId) === run) {
1257
+ resolved.state.activeRuns.delete(run.sessionId);
1258
+ }
1259
+ throw admissionError;
1260
+ }
796
1261
  if (originalError !== undefined && run.ownerCancellation === undefined) {
797
1262
  run.internalFailure ??= originalError;
798
1263
  }
799
1264
 
800
- if (!reserved) {
801
- resolved.state.activeRuns.delete(run.sessionId);
802
- throw originalError;
803
- }
804
-
805
1265
  this.rejectRunPermissions(
806
1266
  resolved.state,
807
1267
  run,
@@ -810,7 +1270,11 @@ export class FlexHarness<TScope = unknown> {
810
1270
  new FlexHarnessAbortError('The run ended before permission was resolved.'),
811
1271
  );
812
1272
  const closeResult = await Promise.allSettled([
813
- Promise.resolve().then(() => toolHandle?.close?.()),
1273
+ Promise.resolve()
1274
+ .then(() => toolHandle?.close?.())
1275
+ .catch((error) => {
1276
+ throw this.projectExternalError(run, error, 'toolCleanup');
1277
+ }),
814
1278
  ]).then(([settled]) => settled);
815
1279
  const errorsBeforePersistence: unknown[] = [];
816
1280
  if (originalError !== undefined) errorsBeforePersistence.push(originalError);
@@ -848,7 +1312,12 @@ export class FlexHarness<TScope = unknown> {
848
1312
  )]).then(([settled]) => settled);
849
1313
 
850
1314
  if (persistenceResult.status === 'rejected') {
851
- const allErrors = [...errorsBeforePersistence, persistenceResult.reason];
1315
+ const persistenceError = this.projectExternalError(
1316
+ run,
1317
+ persistenceResult.reason,
1318
+ 'persistence',
1319
+ );
1320
+ const allErrors = [...errorsBeforePersistence, persistenceError];
852
1321
  const combinedError = combineErrors(allErrors);
853
1322
  const fallback = this.applyRunFinalState(
854
1323
  resolved.state,
@@ -860,14 +1329,18 @@ export class FlexHarness<TScope = unknown> {
860
1329
  combinedError,
861
1330
  cancelled,
862
1331
  );
863
- resolved.state.activeRuns.delete(run.sessionId);
1332
+ if (resolved.state.activeRuns.get(run.sessionId) === run) {
1333
+ resolved.state.activeRuns.delete(run.sessionId);
1334
+ }
864
1335
  this.emitFinalMutationEvents(run, fallback);
865
1336
  this.emitFinalRunEvent(run, fallback.assistantMessage, combinedError, cancelled);
866
1337
  throw combinedError;
867
1338
  }
868
1339
 
869
1340
  const finalized = persistenceResult.value;
870
- resolved.state.activeRuns.delete(run.sessionId);
1341
+ if (resolved.state.activeRuns.get(run.sessionId) === run) {
1342
+ resolved.state.activeRuns.delete(run.sessionId);
1343
+ }
871
1344
  this.emitFinalRunEvent(run, finalized.assistantMessage, terminalError, cancelled);
872
1345
  if (terminalError !== undefined) throw terminalError;
873
1346
  return {
@@ -945,7 +1418,7 @@ export class FlexHarness<TScope = unknown> {
945
1418
  run: IActiveRun,
946
1419
  prompt: INormalizedFlexPrompt,
947
1420
  model: IFlexResolvedModel | undefined,
948
- result: TFlexAgentRunResult | undefined,
1421
+ result: IRunResultProjection | undefined,
949
1422
  serializedResultMessages: TFlexAgentModelMessage[] | undefined,
950
1423
  originalError: unknown,
951
1424
  cancelled: boolean,
@@ -971,7 +1444,7 @@ export class FlexHarness<TScope = unknown> {
971
1444
  run: IActiveRun,
972
1445
  prompt: INormalizedFlexPrompt,
973
1446
  model: IFlexResolvedModel | undefined,
974
- result: TFlexAgentRunResult | undefined,
1447
+ result: IRunResultProjection | undefined,
975
1448
  serializedResultMessages: TFlexAgentModelMessage[] | undefined,
976
1449
  originalError: unknown,
977
1450
  cancelled: boolean,
@@ -1190,16 +1663,19 @@ export class FlexHarness<TScope = unknown> {
1190
1663
  const output = event.success
1191
1664
  ? normalizeJsonValue(event.output, this.toolOutputLimits)
1192
1665
  : undefined;
1666
+ const projectedError = event.success
1667
+ ? undefined
1668
+ : this.projectExternalError(run, event.error, 'toolCallback');
1193
1669
  const byteLength = event.success
1194
1670
  ? Buffer.byteLength(JSON.stringify(output))
1195
- : Buffer.byteLength(event.error);
1671
+ : Buffer.byteLength(projectedError!.message);
1196
1672
  if (!this.reserveCallbackCapacity(run, 1, byteLength, 0)) return;
1197
1673
  if (event.success) {
1198
1674
  part.status = 'completed';
1199
1675
  part.output = output!;
1200
1676
  } else {
1201
1677
  part.status = 'failed';
1202
- part.error = event.error;
1678
+ part.error = projectedError!.message;
1203
1679
  }
1204
1680
  this.emitPartEvent(run, 'part.completed', part);
1205
1681
  }
@@ -1218,8 +1694,10 @@ export class FlexHarness<TScope = unknown> {
1218
1694
  nextOutputBytes > this.callbackLimits.maxOutputBytes ||
1219
1695
  nextParts > this.callbackLimits.maxParts
1220
1696
  ) {
1221
- const error = new FlexHarnessCallbackOverflowError(
1222
- `Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
1697
+ const error = Object.freeze(
1698
+ new FlexHarnessCallbackOverflowError(
1699
+ `Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
1700
+ ),
1223
1701
  );
1224
1702
  run.callbackError = error;
1225
1703
  this.abortRunInternally(run, error);
@@ -1308,7 +1786,7 @@ export class FlexHarness<TScope = unknown> {
1308
1786
  state.pendingPermissions.delete(request.permissionId);
1309
1787
  run.pendingPermissionIds.delete(request.permissionId);
1310
1788
  run.controller.signal.removeEventListener('abort', abortListener);
1311
- throw error;
1789
+ throw this.projectExternalError(run, error, 'persistence');
1312
1790
  }
1313
1791
  if (pending.settled) {
1314
1792
  return permissionPromise;
@@ -1361,13 +1839,14 @@ export class FlexHarness<TScope = unknown> {
1361
1839
  stored.session.updatedAt = new Date().toISOString();
1362
1840
  });
1363
1841
  } catch (error) {
1842
+ const persistenceError = this.projectExternalError(pending.run, error, 'persistence');
1364
1843
  pending.responding = false;
1365
1844
  if (pending.abortReason !== undefined) {
1366
1845
  const abortReason = pending.abortReason;
1367
1846
  this.forceRejectPending(state, pending, abortReason);
1368
- throw combineErrors([abortReason, error]);
1847
+ throw combineErrors([abortReason, persistenceError]);
1369
1848
  }
1370
- throw error;
1849
+ throw persistenceError;
1371
1850
  }
1372
1851
 
1373
1852
  if (pending.abortReason !== undefined) {
@@ -1381,12 +1860,21 @@ export class FlexHarness<TScope = unknown> {
1381
1860
  })
1382
1861
  : Promise.resolve();
1383
1862
  pending.responding = false;
1384
- this.forceRejectPending(state, pending, abortReason);
1385
1863
  try {
1386
1864
  await rollbackPromise;
1387
1865
  } catch (rollbackError) {
1388
- throw combineErrors([abortReason, rollbackError]);
1866
+ if (addedRememberKey && rememberKey) {
1867
+ state.sessions
1868
+ .get(pending.request.sessionId)
1869
+ ?.rememberedPermissionKeys.delete(rememberKey);
1870
+ }
1871
+ this.forceRejectPending(state, pending, abortReason);
1872
+ throw combineErrors([
1873
+ abortReason,
1874
+ this.projectExternalError(pending.run, rollbackError, 'persistence'),
1875
+ ]);
1389
1876
  }
1877
+ this.forceRejectPending(state, pending, abortReason);
1390
1878
  throw abortReason;
1391
1879
  }
1392
1880
 
@@ -1402,7 +1890,10 @@ export class FlexHarness<TScope = unknown> {
1402
1890
  decision,
1403
1891
  });
1404
1892
  if (decision === 'reject') {
1405
- pending.reject(new FlexHarnessPermissionRejectedError(pending.request.permissionId));
1893
+ const error = this.trustInternalError(
1894
+ new FlexHarnessPermissionRejectedError(pending.request.permissionId),
1895
+ );
1896
+ pending.reject(error);
1406
1897
  } else {
1407
1898
  pending.resolve();
1408
1899
  }
@@ -1460,19 +1951,102 @@ export class FlexHarness<TScope = unknown> {
1460
1951
  }
1461
1952
  }
1462
1953
 
1954
+ private projectExternalError(
1955
+ run: IActiveRun,
1956
+ error: unknown,
1957
+ source: TFlexExternalErrorSource,
1958
+ ): Error {
1959
+ if ((typeof error === 'object' && error !== null) || typeof error === 'function') {
1960
+ if (
1961
+ error === run.ownerCancellation
1962
+ || error === run.callbackError
1963
+ || this.trustedInternalErrors.has(error)
1964
+ ) {
1965
+ return error as Error;
1966
+ }
1967
+ }
1968
+ let info = externalErrorFallback;
1969
+ if (this.externalErrorProjector) {
1970
+ try {
1971
+ info = validateProjectedErrorInfo(this.externalErrorProjector(error, {
1972
+ source,
1973
+ scopeId: run.scopeId,
1974
+ sessionId: run.sessionId,
1975
+ runId: run.runId,
1976
+ }));
1977
+ } catch {
1978
+ info = externalErrorFallback;
1979
+ }
1980
+ }
1981
+ return this.trustInternalError(new FlexHarnessExternalError(info));
1982
+ }
1983
+
1984
+ private trustInternalError<TError extends Error>(error: TError): TError {
1985
+ Object.freeze(error);
1986
+ this.trustedInternalErrors.add(error);
1987
+ return error;
1988
+ }
1989
+
1990
+ private normalizeToolHandle(
1991
+ run: IActiveRun,
1992
+ toolHandle: IFlexToolHandle | undefined,
1993
+ ): IFlexToolHandle | undefined {
1994
+ if (toolHandle === undefined) return undefined;
1995
+ let close: IFlexToolHandle['close'];
1996
+ try {
1997
+ close = toolHandle.close;
1998
+ if (close !== undefined && typeof close !== 'function') {
1999
+ throw new FlexHarnessValidationError('Tool handle close must be a function.');
2000
+ }
2001
+ } catch (error) {
2002
+ throw this.projectExternalError(run, error, 'toolProvider');
2003
+ }
2004
+ const closeHandle = close === undefined ? undefined : () => close.call(toolHandle);
2005
+ try {
2006
+ const tools = toolHandle.tools;
2007
+ return {
2008
+ tools,
2009
+ ...(closeHandle === undefined ? {} : { close: closeHandle }),
2010
+ };
2011
+ } catch (error) {
2012
+ if (closeHandle) this.trackDetachedToolCleanup(closeHandle, run);
2013
+ throw this.projectExternalError(run, error, 'toolProvider');
2014
+ }
2015
+ }
2016
+
2017
+ private trackDetachedToolCleanup(
2018
+ cleanup: () => Promise<void> | void,
2019
+ run: IActiveRun,
2020
+ ): void {
2021
+ let tracked!: Promise<void>;
2022
+ tracked = Promise.resolve()
2023
+ .then(cleanup)
2024
+ .catch((error) => {
2025
+ if (this.detachedToolCleanupErrors.length < maxDetachedCleanupErrors) {
2026
+ this.detachedToolCleanupErrors.push(
2027
+ this.projectExternalError(run, error, 'toolCleanup'),
2028
+ );
2029
+ }
2030
+ })
2031
+ .finally(() => {
2032
+ this.detachedToolCleanups.delete(tracked);
2033
+ });
2034
+ this.detachedToolCleanups.add(tracked);
2035
+ }
2036
+
1463
2037
  private observeDetachedToolProvider(
1464
2038
  outcomePromise: Promise<IResolverOutcome<IFlexToolHandle | undefined>>,
2039
+ run: IActiveRun,
1465
2040
  ): void {
1466
- const cleanup = outcomePromise.then(async (outcome) => {
1467
- if (outcome.success && outcome.source === 'tools' && outcome.value?.close) {
1468
- await Promise.resolve().then(() => outcome.value!.close!());
2041
+ this.trackDetachedToolCleanup(async () => {
2042
+ const outcome = await outcomePromise;
2043
+ if (!outcome.success || outcome.source !== 'tools' || !outcome.value) return;
2044
+ const close = outcome.value.close;
2045
+ if (close !== undefined && typeof close !== 'function') {
2046
+ throw new FlexHarnessValidationError('Tool handle close must be a function.');
1469
2047
  }
1470
- });
1471
- this.detachedToolCleanups.add(cleanup);
1472
- void cleanup.then(
1473
- () => this.detachedToolCleanups.delete(cleanup),
1474
- () => undefined,
1475
- );
2048
+ await close?.call(outcome.value);
2049
+ }, run);
1476
2050
  }
1477
2051
 
1478
2052
  private emitEvent(scopeId: string, sessionId: string, details: TEventDetails): void {
@@ -1615,6 +2189,76 @@ export class FlexHarness<TScope = unknown> {
1615
2189
  };
1616
2190
  }
1617
2191
 
2192
+ private messageCursorNamespace(storageKey: string): string {
2193
+ return plugins.crypto.createHash('sha256').update(storageKey).digest('base64url');
2194
+ }
2195
+
2196
+ private createMessageCursor(
2197
+ namespace: string,
2198
+ sessionId: string,
2199
+ anchorMessageId: string,
2200
+ ): string {
2201
+ const cursor = Buffer.from(JSON.stringify({
2202
+ version: 1,
2203
+ namespace,
2204
+ sessionId,
2205
+ anchorMessageId,
2206
+ } satisfies IFlexMessageCursor)).toString('base64url');
2207
+ if (Buffer.byteLength(cursor, 'utf8') > maxMessagePageCursorBytes) {
2208
+ throw new FlexHarnessValidationError('Message page cursor exceeds its transfer limit.');
2209
+ }
2210
+ return cursor;
2211
+ }
2212
+
2213
+ private parseMessageCursor(cursor: string): IFlexMessageCursor {
2214
+ let parsed: unknown;
2215
+ try {
2216
+ const decoded = Buffer.from(cursor, 'base64url');
2217
+ if (decoded.toString('base64url') !== cursor) {
2218
+ throw new Error('non-canonical cursor');
2219
+ }
2220
+ parsed = JSON.parse(decoded.toString('utf8'));
2221
+ } catch {
2222
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
2223
+ }
2224
+ if (
2225
+ !parsed
2226
+ || typeof parsed !== 'object'
2227
+ || Array.isArray(parsed)
2228
+ || Object.keys(parsed).length !== 4
2229
+ || (parsed as Partial<IFlexMessageCursor>).version !== 1
2230
+ || typeof (parsed as Partial<IFlexMessageCursor>).namespace !== 'string'
2231
+ || typeof (parsed as Partial<IFlexMessageCursor>).sessionId !== 'string'
2232
+ || typeof (parsed as Partial<IFlexMessageCursor>).anchorMessageId !== 'string'
2233
+ ) {
2234
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
2235
+ }
2236
+ const result = parsed as IFlexMessageCursor;
2237
+ if (
2238
+ result.namespace.length === 0
2239
+ || result.sessionId.length === 0
2240
+ || result.anchorMessageId.length === 0
2241
+ || Buffer.byteLength(result.namespace, 'utf8') > 128
2242
+ || Buffer.byteLength(result.sessionId, 'utf8') > maxTransferIdentifierBytes
2243
+ || Buffer.byteLength(result.anchorMessageId, 'utf8') > maxTransferIdentifierBytes
2244
+ ) {
2245
+ throw new FlexHarnessValidationError('Message page cursor is invalid.');
2246
+ }
2247
+ return result;
2248
+ }
2249
+
2250
+ private resolveRunAdmission(run: IActiveRun): void {
2251
+ if (run.admissionSettled) return;
2252
+ run.admissionSettled = true;
2253
+ run.resolveAdmission();
2254
+ }
2255
+
2256
+ private rejectRunAdmission(run: IActiveRun, error: unknown): void {
2257
+ if (run.admissionSettled) return;
2258
+ run.admissionSettled = true;
2259
+ run.rejectAdmission(error);
2260
+ }
2261
+
1618
2262
  private requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
1619
2263
  validateIdentifier(sessionId, 'sessionId');
1620
2264
  const session = state.sessions.get(sessionId);