@modelprofile.com/flexharness 1.0.1 → 2.1.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +30 -2
- package/dist_ts/classes.flexharness.js +799 -135
- package/dist_ts/errors.d.ts +3 -0
- package/dist_ts/errors.js +7 -1
- package/dist_ts/interfaces.d.ts +22 -0
- package/dist_ts/utils.json.js +7 -6
- package/package.json +2 -2
- package/readme.hints.md +2 -0
- package/readme.md +36 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +1014 -175
- package/ts/errors.ts +7 -0
- package/ts/interfaces.ts +38 -0
- package/ts/utils.json.ts +6 -5
|
@@ -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,
|
|
@@ -75,15 +82,22 @@ interface IStorageState {
|
|
|
75
82
|
activeRuns: Map<string, IActiveRun>;
|
|
76
83
|
pendingPermissions: Map<string, IPendingPermission>;
|
|
77
84
|
saveTail: Promise<void>;
|
|
85
|
+
lifecycle: 'active' | 'retiring' | 'retired';
|
|
86
|
+
detachedToolCleanupErrors: Error[];
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
interface IActiveRun {
|
|
90
|
+
state: IStorageState;
|
|
81
91
|
scopeId: string;
|
|
82
92
|
sessionId: string;
|
|
83
93
|
runId: string;
|
|
84
94
|
userMessageId: string;
|
|
85
95
|
assistantMessageId: string;
|
|
86
96
|
controller: AbortController;
|
|
97
|
+
admission: Promise<void>;
|
|
98
|
+
resolveAdmission: () => void;
|
|
99
|
+
rejectAdmission: (error: unknown) => void;
|
|
100
|
+
admissionSettled: boolean;
|
|
87
101
|
ownerCancellation?: FlexHarnessAbortError;
|
|
88
102
|
internalFailure?: unknown;
|
|
89
103
|
finalizer: Promise<IFlexPromptResult>;
|
|
@@ -111,12 +125,30 @@ interface IPendingPermission {
|
|
|
111
125
|
abortListener: () => void;
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
interface IDetachedToolCleanup {
|
|
129
|
+
state: IStorageState;
|
|
130
|
+
completion: Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface IScopeAdmissionState {
|
|
134
|
+
generation: number;
|
|
135
|
+
inFlightResolvers: number;
|
|
136
|
+
retiring: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
interface IFinalMutationResult {
|
|
115
140
|
userMessage: IFlexMessage;
|
|
116
141
|
assistantMessage: IFlexMessage;
|
|
117
142
|
terminalParts: TFlexMessagePart[];
|
|
118
143
|
}
|
|
119
144
|
|
|
145
|
+
interface IRunResultProjection {
|
|
146
|
+
text: string;
|
|
147
|
+
steps: number;
|
|
148
|
+
finishReason: string;
|
|
149
|
+
usage: IFlexUsage;
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
type TEventDetails = Record<string, unknown> & {
|
|
121
153
|
type: TFlexHarnessEvent['type'];
|
|
122
154
|
};
|
|
@@ -132,11 +164,35 @@ interface IResolverOutcome<T> {
|
|
|
132
164
|
error?: unknown;
|
|
133
165
|
}
|
|
134
166
|
|
|
167
|
+
interface IFlexMessageCursor {
|
|
168
|
+
version: 1;
|
|
169
|
+
namespace: string;
|
|
170
|
+
sessionId: string;
|
|
171
|
+
anchorMessageId: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
135
174
|
const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
136
175
|
maxEvents: 10_000,
|
|
137
176
|
maxOutputBytes: 1024 * 1024,
|
|
138
177
|
maxParts: 2_000,
|
|
139
178
|
};
|
|
179
|
+
const maxMessagePageSize = 50;
|
|
180
|
+
const maxMessagePageCursorBytes = 4096;
|
|
181
|
+
const maxTransferIdentifierBytes = 512;
|
|
182
|
+
const maxTransferMetadataBytes = 2048;
|
|
183
|
+
const maxTransferTextBytes = 96 * 1024;
|
|
184
|
+
const maxTransferMessageBytes = 480 * 1024;
|
|
185
|
+
const maxTransferPageBytes = 512 * 1024;
|
|
186
|
+
const maxProjectedErrorNameBytes = 128;
|
|
187
|
+
const maxProjectedErrorMessageBytes = 2048;
|
|
188
|
+
const maxProjectedErrorCodeBytes = 128;
|
|
189
|
+
const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
190
|
+
name: 'FlexHarnessExternalError',
|
|
191
|
+
message: 'The model operation failed.',
|
|
192
|
+
code: 'FLEX_EXTERNAL_ERROR',
|
|
193
|
+
});
|
|
194
|
+
const maxDetachedCleanupErrors = 100;
|
|
195
|
+
const scopeRetirementMessage = 'The scope is being retired.';
|
|
140
196
|
|
|
141
197
|
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
142
198
|
return Boolean(
|
|
@@ -149,15 +205,21 @@ function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
|
149
205
|
async function* normalizeAsyncIterable(
|
|
150
206
|
iterable: AsyncIterable<unknown>,
|
|
151
207
|
limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
|
|
208
|
+
projectError: (error: unknown) => Error,
|
|
152
209
|
): AsyncGenerator<unknown> {
|
|
153
|
-
|
|
154
|
-
|
|
210
|
+
try {
|
|
211
|
+
for await (const value of iterable) {
|
|
212
|
+
yield normalizeJsonValue(value, limits);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw projectError(error);
|
|
155
216
|
}
|
|
156
217
|
}
|
|
157
218
|
|
|
158
219
|
function wrapToolSet(
|
|
159
220
|
tools: TFlexAgentToolSet,
|
|
160
221
|
limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
|
|
222
|
+
projectError: (error: unknown) => Error,
|
|
161
223
|
): TFlexAgentToolSet {
|
|
162
224
|
const wrapped: Record<string, unknown> = {};
|
|
163
225
|
for (const [name, tool] of Object.entries(tools)) {
|
|
@@ -170,11 +232,31 @@ function wrapToolSet(
|
|
|
170
232
|
wrapped[name] = {
|
|
171
233
|
...toolRecord,
|
|
172
234
|
execute(input: unknown, options: unknown): unknown {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
235
|
+
let output: unknown;
|
|
236
|
+
try {
|
|
237
|
+
output = execute.call(tool, input, options);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw projectError(error);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
if (isAsyncIterable(output)) {
|
|
243
|
+
return normalizeAsyncIterable(output, limits, projectError);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw projectError(error);
|
|
176
247
|
}
|
|
177
|
-
return Promise.resolve(output).then(
|
|
248
|
+
return Promise.resolve(output).then(
|
|
249
|
+
(value) => {
|
|
250
|
+
try {
|
|
251
|
+
return normalizeJsonValue(value, limits);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw projectError(error);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
(error: unknown) => {
|
|
257
|
+
throw projectError(error);
|
|
258
|
+
},
|
|
259
|
+
);
|
|
178
260
|
},
|
|
179
261
|
};
|
|
180
262
|
}
|
|
@@ -185,25 +267,216 @@ function publicSnapshot<T>(value: T): T {
|
|
|
185
267
|
return deepFreeze(cloneSerializable(value));
|
|
186
268
|
}
|
|
187
269
|
|
|
270
|
+
function jsonBytes(value: unknown): number {
|
|
271
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function truncateUtf8(value: string, maxBytes: number): string {
|
|
275
|
+
if (Buffer.byteLength(value, 'utf8') <= maxBytes) return value;
|
|
276
|
+
const suffix = ' [truncated]';
|
|
277
|
+
const suffixBytes = Buffer.byteLength(suffix, 'utf8');
|
|
278
|
+
let truncated = Buffer.from(value, 'utf8')
|
|
279
|
+
.subarray(0, Math.max(0, maxBytes - suffixBytes))
|
|
280
|
+
.toString('utf8')
|
|
281
|
+
.replace(/\uFFFD$/u, '');
|
|
282
|
+
while (Buffer.byteLength(`${truncated}${suffix}`, 'utf8') > maxBytes) {
|
|
283
|
+
truncated = truncated.slice(0, -1);
|
|
284
|
+
}
|
|
285
|
+
return `${truncated}${suffix}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function requireTransferIdentifier(value: string, field: string): void {
|
|
289
|
+
if (Buffer.byteLength(value, 'utf8') > maxTransferIdentifierBytes) {
|
|
290
|
+
throw new FlexHarnessValidationError(`Stored ${field} exceeds the transfer limit.`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function createBoundedTransferMessage(message: IFlexMessage): IFlexMessage {
|
|
295
|
+
const projected = cloneSerializable(message);
|
|
296
|
+
requireTransferIdentifier(projected.messageId, 'messageId');
|
|
297
|
+
requireTransferIdentifier(projected.sessionId, 'sessionId');
|
|
298
|
+
requireTransferIdentifier(projected.runId, 'runId');
|
|
299
|
+
projected.createdAt = truncateUtf8(projected.createdAt, 128);
|
|
300
|
+
if (projected.completedAt !== undefined) {
|
|
301
|
+
projected.completedAt = truncateUtf8(projected.completedAt, 128);
|
|
302
|
+
}
|
|
303
|
+
if (projected.error !== undefined) {
|
|
304
|
+
projected.error = truncateUtf8(projected.error, maxTransferMetadataBytes);
|
|
305
|
+
}
|
|
306
|
+
if (projected.model) {
|
|
307
|
+
projected.model.provider = truncateUtf8(projected.model.provider, maxTransferIdentifierBytes);
|
|
308
|
+
projected.model.model = truncateUtf8(projected.model.model, maxTransferIdentifierBytes);
|
|
309
|
+
if (projected.model.displayName !== undefined) {
|
|
310
|
+
projected.model.displayName = truncateUtf8(
|
|
311
|
+
projected.model.displayName,
|
|
312
|
+
maxTransferMetadataBytes,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (projected.model.variant !== undefined) {
|
|
316
|
+
projected.model.variant = truncateUtf8(projected.model.variant, 128);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const part of projected.parts) {
|
|
320
|
+
requireTransferIdentifier(part.partId, 'partId');
|
|
321
|
+
if (part.type === 'text' || part.type === 'reasoning') {
|
|
322
|
+
part.text = truncateUtf8(part.text, maxTransferTextBytes);
|
|
323
|
+
} else if (part.type === 'tool') {
|
|
324
|
+
part.toolCallId = truncateUtf8(part.toolCallId, maxTransferIdentifierBytes);
|
|
325
|
+
part.toolName = truncateUtf8(part.toolName, maxTransferIdentifierBytes);
|
|
326
|
+
if (part.error !== undefined) {
|
|
327
|
+
part.error = truncateUtf8(part.error, maxTransferMetadataBytes);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
if (part.mediaType !== undefined) {
|
|
331
|
+
part.mediaType = truncateUtf8(part.mediaType, maxTransferIdentifierBytes);
|
|
332
|
+
}
|
|
333
|
+
if (part.name !== undefined) {
|
|
334
|
+
part.name = truncateUtf8(part.name, maxTransferMetadataBytes);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (jsonBytes(projected) > maxTransferMessageBytes) {
|
|
339
|
+
projected.parts = [{
|
|
340
|
+
partId: 'transfer-elided',
|
|
341
|
+
type: 'text',
|
|
342
|
+
text: '[elided: message exceeds the transfer budget]',
|
|
343
|
+
}];
|
|
344
|
+
}
|
|
345
|
+
if (jsonBytes(projected) > maxTransferMessageBytes) {
|
|
346
|
+
throw new FlexHarnessValidationError('Stored message metadata exceeds the transfer limit.');
|
|
347
|
+
}
|
|
348
|
+
return projected;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
|
|
352
|
+
if (
|
|
353
|
+
!value
|
|
354
|
+
|| typeof value !== 'object'
|
|
355
|
+
|| Array.isArray(value)
|
|
356
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
357
|
+
) {
|
|
358
|
+
throw new FlexHarnessValidationError('External error projection is invalid.');
|
|
359
|
+
}
|
|
360
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
361
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
362
|
+
const unsupportedKey = keys.find(
|
|
363
|
+
(key) => typeof key !== 'string' || (key !== 'name' && key !== 'message' && key !== 'code'),
|
|
364
|
+
);
|
|
365
|
+
const name = descriptors.name?.value;
|
|
366
|
+
const message = descriptors.message?.value;
|
|
367
|
+
const code = descriptors.code?.value;
|
|
368
|
+
if (
|
|
369
|
+
unsupportedKey
|
|
370
|
+
|| descriptors.name?.get !== undefined
|
|
371
|
+
|| descriptors.name?.set !== undefined
|
|
372
|
+
|| descriptors.message?.get !== undefined
|
|
373
|
+
|| descriptors.message?.set !== undefined
|
|
374
|
+
|| descriptors.code?.get !== undefined
|
|
375
|
+
|| descriptors.code?.set !== undefined
|
|
376
|
+
|| typeof name !== 'string'
|
|
377
|
+
|| name.length === 0
|
|
378
|
+
|| Buffer.byteLength(name, 'utf8') > maxProjectedErrorNameBytes
|
|
379
|
+
|| typeof message !== 'string'
|
|
380
|
+
|| message.length === 0
|
|
381
|
+
|| Buffer.byteLength(message, 'utf8') > maxProjectedErrorMessageBytes
|
|
382
|
+
|| (code !== undefined
|
|
383
|
+
&& (
|
|
384
|
+
typeof code !== 'string'
|
|
385
|
+
|| code.length === 0
|
|
386
|
+
|| Buffer.byteLength(code, 'utf8') > maxProjectedErrorCodeBytes
|
|
387
|
+
))
|
|
388
|
+
) {
|
|
389
|
+
throw new FlexHarnessValidationError('External error projection is invalid.');
|
|
390
|
+
}
|
|
391
|
+
return Object.freeze({ name, message, ...(code === undefined ? {} : { code }) });
|
|
392
|
+
}
|
|
393
|
+
|
|
188
394
|
function validateIdentifier(value: string, name: string): void {
|
|
189
395
|
if (typeof value !== 'string' || value.length === 0) {
|
|
190
396
|
throw new FlexHarnessValidationError(`${name} must be a non-empty string.`);
|
|
191
397
|
}
|
|
192
398
|
}
|
|
193
399
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
400
|
+
function normalizeModelIdentity(identity: IFlexModelIdentity): IFlexModelIdentity {
|
|
401
|
+
const provider = identity.provider;
|
|
402
|
+
const model = identity.model;
|
|
403
|
+
const displayName = identity.displayName;
|
|
404
|
+
const variant = identity.variant;
|
|
405
|
+
const normalized: IFlexModelIdentity = {
|
|
406
|
+
provider,
|
|
407
|
+
model,
|
|
408
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
409
|
+
...(variant === undefined ? {} : { variant }),
|
|
410
|
+
};
|
|
411
|
+
validateIdentifier(normalized.provider, 'model identity provider');
|
|
412
|
+
validateIdentifier(normalized.model, 'model identity model');
|
|
413
|
+
if (normalized.displayName !== undefined) {
|
|
414
|
+
validateIdentifier(normalized.displayName, 'model identity displayName');
|
|
415
|
+
}
|
|
416
|
+
if (normalized.variant !== undefined) {
|
|
417
|
+
validateIdentifier(normalized.variant, 'model identity variant');
|
|
418
|
+
}
|
|
419
|
+
if (
|
|
420
|
+
Buffer.byteLength(normalized.provider, 'utf8') > maxTransferIdentifierBytes
|
|
421
|
+
|| Buffer.byteLength(normalized.model, 'utf8') > maxTransferIdentifierBytes
|
|
422
|
+
|| (normalized.displayName !== undefined
|
|
423
|
+
&& Buffer.byteLength(normalized.displayName, 'utf8') > maxTransferMetadataBytes)
|
|
424
|
+
|| (normalized.variant !== undefined && Buffer.byteLength(normalized.variant, 'utf8') > 128)
|
|
425
|
+
) {
|
|
426
|
+
throw new FlexHarnessValidationError('Model identity exceeds its transfer limit.');
|
|
427
|
+
}
|
|
428
|
+
return normalized;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function normalizeResolvedModel(resolvedModel: IFlexResolvedModel): IFlexResolvedModel {
|
|
432
|
+
const model = resolvedModel.model;
|
|
433
|
+
const identity = normalizeModelIdentity(resolvedModel.identity);
|
|
434
|
+
const system = resolvedModel.system;
|
|
435
|
+
const providerOptions = resolvedModel.providerOptions;
|
|
436
|
+
const cache = resolvedModel.cache;
|
|
437
|
+
const maxSteps = resolvedModel.maxSteps;
|
|
438
|
+
return {
|
|
439
|
+
model,
|
|
440
|
+
identity,
|
|
441
|
+
...(system === undefined ? {} : { system }),
|
|
442
|
+
...(providerOptions === undefined ? {} : { providerOptions }),
|
|
443
|
+
...(cache === undefined ? {} : { cache }),
|
|
444
|
+
...(maxSteps === undefined ? {} : { maxSteps }),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function normalizeRunResult(result: TFlexAgentRunResult): IRunResultProjection {
|
|
449
|
+
const normalized: IRunResultProjection = {
|
|
450
|
+
text: result.text,
|
|
451
|
+
steps: result.steps,
|
|
452
|
+
finishReason: result.finishReason,
|
|
453
|
+
usage: {
|
|
454
|
+
inputTokens: result.usage.inputTokens,
|
|
455
|
+
outputTokens: result.usage.outputTokens,
|
|
456
|
+
totalTokens: result.usage.totalTokens,
|
|
457
|
+
cacheReadTokens: result.usage.cacheReadTokens,
|
|
458
|
+
cacheWriteTokens: result.usage.cacheWriteTokens,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
if (
|
|
462
|
+
typeof normalized.text !== 'string'
|
|
463
|
+
|| typeof normalized.finishReason !== 'string'
|
|
464
|
+
|| !Number.isSafeInteger(normalized.steps)
|
|
465
|
+
|| normalized.steps < 0
|
|
466
|
+
|| Object.values(normalized.usage).some((value) => !Number.isFinite(value) || value < 0)
|
|
467
|
+
) {
|
|
468
|
+
throw new FlexHarnessValidationError('Agent runner result is invalid.');
|
|
199
469
|
}
|
|
470
|
+
return normalized;
|
|
200
471
|
}
|
|
201
472
|
|
|
202
473
|
function combineErrors(errors: unknown[]): unknown {
|
|
203
474
|
if (errors.length === 1) {
|
|
204
475
|
return errors[0];
|
|
205
476
|
}
|
|
206
|
-
|
|
477
|
+
const combined = new FlexHarnessRunError(errors);
|
|
478
|
+
Object.freeze(combined.errors);
|
|
479
|
+
return Object.freeze(combined);
|
|
207
480
|
}
|
|
208
481
|
|
|
209
482
|
function isAbortError(error: unknown): boolean {
|
|
@@ -214,7 +487,7 @@ function isAbortError(error: unknown): boolean {
|
|
|
214
487
|
);
|
|
215
488
|
}
|
|
216
489
|
|
|
217
|
-
function makeUsage(result:
|
|
490
|
+
function makeUsage(result: IRunResultProjection): IFlexUsage {
|
|
218
491
|
return {
|
|
219
492
|
inputTokens: result.usage.inputTokens,
|
|
220
493
|
outputTokens: result.usage.outputTokens,
|
|
@@ -329,9 +602,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
329
602
|
NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>
|
|
330
603
|
>;
|
|
331
604
|
private readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
605
|
+
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
332
606
|
private readonly stateLoads = new Map<string, Promise<IStorageState>>();
|
|
607
|
+
private readonly scopeAdmissions = new Map<string, IScopeAdmissionState>();
|
|
608
|
+
private readonly scopeRetirements = new Map<string, Promise<void>>();
|
|
609
|
+
private readonly storageDrains = new Map<string, Promise<void>>();
|
|
333
610
|
private readonly listeners = new Set<TFlexHarnessEventListener>();
|
|
334
|
-
private readonly detachedToolCleanups = new Set<
|
|
611
|
+
private readonly detachedToolCleanups = new Set<IDetachedToolCleanup>();
|
|
612
|
+
private readonly trustedInternalErrors = new WeakSet<object>();
|
|
335
613
|
private sequence = 0;
|
|
336
614
|
private closed = false;
|
|
337
615
|
private disposePromise?: Promise<void>;
|
|
@@ -344,11 +622,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
344
622
|
this.runner = options.runner ?? plugins.runAgent;
|
|
345
623
|
this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
|
|
346
624
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
625
|
+
this.externalErrorProjector = options.externalErrorProjector;
|
|
347
626
|
}
|
|
348
627
|
|
|
349
628
|
public async listSessions(scopeId: string): Promise<IFlexSession[]> {
|
|
350
629
|
const { state } = await this.resolveState(scopeId);
|
|
351
|
-
await state
|
|
630
|
+
await this.waitForReadableState(state);
|
|
352
631
|
return publicSnapshot(
|
|
353
632
|
[...state.sessions.values()]
|
|
354
633
|
.map((entry) => entry.session)
|
|
@@ -397,7 +676,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
397
676
|
|
|
398
677
|
public async getSession(scopeId: string, sessionId: string): Promise<IFlexSession> {
|
|
399
678
|
const { state } = await this.resolveState(scopeId);
|
|
400
|
-
await state
|
|
679
|
+
await this.waitForReadableState(state);
|
|
401
680
|
return publicSnapshot(this.requireSession(state, sessionId).session);
|
|
402
681
|
}
|
|
403
682
|
|
|
@@ -449,16 +728,126 @@ export class FlexHarness<TScope = unknown> {
|
|
|
449
728
|
|
|
450
729
|
public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
|
|
451
730
|
const { state } = await this.resolveState(scopeId);
|
|
452
|
-
await state
|
|
731
|
+
await this.waitForReadableState(state);
|
|
453
732
|
return publicSnapshot(this.requireSession(state, sessionId).messages);
|
|
454
733
|
}
|
|
455
734
|
|
|
735
|
+
public async listMessagePage(
|
|
736
|
+
scopeId: string,
|
|
737
|
+
sessionId: string,
|
|
738
|
+
options: IFlexMessagePageOptions = {},
|
|
739
|
+
): Promise<IFlexMessagePage> {
|
|
740
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
741
|
+
throw new FlexHarnessValidationError('Message page options must be a plain object.');
|
|
742
|
+
}
|
|
743
|
+
const unsupportedKey = Object.keys(options).find(
|
|
744
|
+
(key) => key !== 'limit' && key !== 'before',
|
|
745
|
+
);
|
|
746
|
+
if (unsupportedKey) {
|
|
747
|
+
throw new FlexHarnessValidationError(
|
|
748
|
+
`Message page options do not support "${unsupportedKey}".`,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const limit = options.limit ?? maxMessagePageSize;
|
|
752
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maxMessagePageSize) {
|
|
753
|
+
throw new FlexHarnessValidationError(
|
|
754
|
+
`Message page limit must be an integer from 1 through ${maxMessagePageSize}.`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
if (
|
|
758
|
+
options.before !== undefined
|
|
759
|
+
&& (
|
|
760
|
+
typeof options.before !== 'string'
|
|
761
|
+
|| options.before.length === 0
|
|
762
|
+
|| Buffer.byteLength(options.before, 'utf8') > maxMessagePageCursorBytes
|
|
763
|
+
)
|
|
764
|
+
) {
|
|
765
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
766
|
+
}
|
|
767
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
768
|
+
requireTransferIdentifier(sessionId, 'sessionId');
|
|
769
|
+
const { state } = await this.resolveState(scopeId);
|
|
770
|
+
await this.waitForReadableState(state);
|
|
771
|
+
const stored = this.requireSession(state, sessionId);
|
|
772
|
+
const namespace = this.messageCursorNamespace(state.storageKey);
|
|
773
|
+
let end = stored.messages.length;
|
|
774
|
+
if (options.before !== undefined) {
|
|
775
|
+
const cursor = this.parseMessageCursor(options.before);
|
|
776
|
+
if (cursor.namespace !== namespace || cursor.sessionId !== sessionId) {
|
|
777
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
778
|
+
}
|
|
779
|
+
const anchorIndex = stored.messages.findIndex(
|
|
780
|
+
(message) => message.messageId === cursor.anchorMessageId,
|
|
781
|
+
);
|
|
782
|
+
if (anchorIndex < 0) {
|
|
783
|
+
throw new FlexHarnessValidationError('Message page cursor is stale.');
|
|
784
|
+
}
|
|
785
|
+
end = anchorIndex;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
let start = end;
|
|
789
|
+
let messages: IFlexMessage[] = [];
|
|
790
|
+
for (let index = end - 1; index >= 0 && messages.length < limit; index--) {
|
|
791
|
+
const candidate = createBoundedTransferMessage(stored.messages[index]);
|
|
792
|
+
const candidateMessages = [candidate, ...messages];
|
|
793
|
+
const nextCursor = index > 0
|
|
794
|
+
? this.createMessageCursor(namespace, sessionId, candidate.messageId)
|
|
795
|
+
: undefined;
|
|
796
|
+
const candidatePage: IFlexMessagePage = {
|
|
797
|
+
messages: candidateMessages,
|
|
798
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
799
|
+
};
|
|
800
|
+
if (jsonBytes(candidatePage) > maxTransferPageBytes) break;
|
|
801
|
+
start = index;
|
|
802
|
+
messages = candidateMessages;
|
|
803
|
+
}
|
|
804
|
+
if (messages.length === 0 && end > 0) {
|
|
805
|
+
throw new FlexHarnessValidationError('Stored message exceeds the page transfer limit.');
|
|
806
|
+
}
|
|
807
|
+
const nextCursor = start > 0
|
|
808
|
+
? this.createMessageCursor(namespace, sessionId, messages[0].messageId)
|
|
809
|
+
: undefined;
|
|
810
|
+
const page: IFlexMessagePage = {
|
|
811
|
+
messages,
|
|
812
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
813
|
+
};
|
|
814
|
+
if (jsonBytes(page) > maxTransferPageBytes) {
|
|
815
|
+
throw new FlexHarnessValidationError('Message page exceeds the transfer limit.');
|
|
816
|
+
}
|
|
817
|
+
return publicSnapshot(page);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
public async getMessage(
|
|
821
|
+
scopeId: string,
|
|
822
|
+
sessionId: string,
|
|
823
|
+
messageId: string,
|
|
824
|
+
): Promise<IFlexMessage> {
|
|
825
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
826
|
+
requireTransferIdentifier(sessionId, 'sessionId');
|
|
827
|
+
validateIdentifier(messageId, 'messageId');
|
|
828
|
+
requireTransferIdentifier(messageId, 'messageId');
|
|
829
|
+
const { state } = await this.resolveState(scopeId);
|
|
830
|
+
await this.waitForReadableState(state);
|
|
831
|
+
const message = this.requireMessage(this.requireSession(state, sessionId), messageId);
|
|
832
|
+
return publicSnapshot(createBoundedTransferMessage(message));
|
|
833
|
+
}
|
|
834
|
+
|
|
456
835
|
public async prompt(
|
|
457
836
|
scopeId: string,
|
|
458
837
|
sessionId: string,
|
|
459
838
|
prompt: TFlexPrompt,
|
|
460
839
|
options: IFlexPromptOptions = {},
|
|
461
840
|
): Promise<IFlexPromptResult> {
|
|
841
|
+
const admission = await this.startPrompt(scopeId, sessionId, prompt, options);
|
|
842
|
+
return admission.completion;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
public async startPrompt(
|
|
846
|
+
scopeId: string,
|
|
847
|
+
sessionId: string,
|
|
848
|
+
prompt: TFlexPrompt,
|
|
849
|
+
options: IFlexPromptOptions = {},
|
|
850
|
+
): Promise<IFlexPromptAdmission> {
|
|
462
851
|
const normalizedPrompt = normalizeFlexPrompt(prompt);
|
|
463
852
|
if (options.maxSteps !== undefined && (!Number.isSafeInteger(options.maxSteps) || options.maxSteps < 1)) {
|
|
464
853
|
throw new FlexHarnessValidationError('maxSteps must be a positive integer.');
|
|
@@ -466,19 +855,38 @@ export class FlexHarness<TScope = unknown> {
|
|
|
466
855
|
const resolved = await this.resolveState(scopeId);
|
|
467
856
|
await resolved.state.saveTail;
|
|
468
857
|
this.assertOpen();
|
|
858
|
+
this.assertStateAcceptingWork(resolved.state);
|
|
469
859
|
this.requireSession(resolved.state, sessionId);
|
|
470
860
|
if (resolved.state.activeRuns.has(sessionId)) {
|
|
471
861
|
throw new FlexHarnessSessionBusyError(sessionId);
|
|
472
862
|
}
|
|
473
863
|
|
|
864
|
+
let resolveAdmission!: () => void;
|
|
865
|
+
let rejectAdmission!: (error: unknown) => void;
|
|
866
|
+
const admission = new Promise<void>((resolve, reject) => {
|
|
867
|
+
resolveAdmission = resolve;
|
|
868
|
+
rejectAdmission = reject;
|
|
869
|
+
});
|
|
870
|
+
void admission.catch(() => undefined);
|
|
871
|
+
let resolveCompletion!: (result: IFlexPromptResult) => void;
|
|
872
|
+
let rejectCompletion!: (error: unknown) => void;
|
|
873
|
+
const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
|
|
874
|
+
resolveCompletion = resolve;
|
|
875
|
+
rejectCompletion = reject;
|
|
876
|
+
});
|
|
474
877
|
const run: IActiveRun = {
|
|
878
|
+
state: resolved.state,
|
|
475
879
|
scopeId,
|
|
476
880
|
sessionId,
|
|
477
881
|
runId: plugins.crypto.randomUUID(),
|
|
478
882
|
userMessageId: plugins.crypto.randomUUID(),
|
|
479
883
|
assistantMessageId: plugins.crypto.randomUUID(),
|
|
480
884
|
controller: new AbortController(),
|
|
481
|
-
|
|
885
|
+
admission,
|
|
886
|
+
resolveAdmission,
|
|
887
|
+
rejectAdmission,
|
|
888
|
+
admissionSettled: false,
|
|
889
|
+
finalizer: completion,
|
|
482
890
|
callbackEventCount: 0,
|
|
483
891
|
callbackOutputBytes: 0,
|
|
484
892
|
callbackParts: [],
|
|
@@ -491,17 +899,21 @@ export class FlexHarness<TScope = unknown> {
|
|
|
491
899
|
};
|
|
492
900
|
void run.finalizer.catch(() => undefined);
|
|
493
901
|
resolved.state.activeRuns.set(sessionId, run);
|
|
494
|
-
|
|
495
|
-
|
|
902
|
+
void this.executeRun(resolved, run, normalizedPrompt, options).then(
|
|
903
|
+
resolveCompletion,
|
|
904
|
+
rejectCompletion,
|
|
905
|
+
);
|
|
906
|
+
await run.admission;
|
|
907
|
+
return Object.freeze({ runId: run.runId, completion: run.finalizer });
|
|
496
908
|
}
|
|
497
909
|
|
|
498
|
-
public async abort(scopeId: string, sessionId: string,
|
|
910
|
+
public async abort(scopeId: string, sessionId: string, _reason?: string): Promise<boolean> {
|
|
499
911
|
const { state } = await this.resolveState(scopeId);
|
|
500
912
|
const run = state.activeRuns.get(sessionId);
|
|
501
913
|
if (!run || run.phase === 'committing') {
|
|
502
914
|
return false;
|
|
503
915
|
}
|
|
504
|
-
const error = new FlexHarnessAbortError(
|
|
916
|
+
const error = Object.freeze(new FlexHarnessAbortError());
|
|
505
917
|
if (run.internalFailure === undefined) run.ownerCancellation ??= error;
|
|
506
918
|
this.rejectRunPermissions(state, run, error);
|
|
507
919
|
run.controller.abort(error);
|
|
@@ -513,7 +925,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
513
925
|
sessionId?: string,
|
|
514
926
|
): Promise<IFlexPermissionRequest[]> {
|
|
515
927
|
const { state } = await this.resolveState(scopeId);
|
|
516
|
-
await state
|
|
928
|
+
await this.waitForReadableState(state);
|
|
517
929
|
return publicSnapshot(
|
|
518
930
|
[...state.pendingPermissions.values()]
|
|
519
931
|
.map((pending) => pending.request)
|
|
@@ -554,6 +966,27 @@ export class FlexHarness<TScope = unknown> {
|
|
|
554
966
|
};
|
|
555
967
|
}
|
|
556
968
|
|
|
969
|
+
public retireScope(scopeId: string): Promise<void> {
|
|
970
|
+
this.assertOpen();
|
|
971
|
+
validateIdentifier(scopeId, 'scopeId');
|
|
972
|
+
const existingRetirement = this.scopeRetirements.get(scopeId);
|
|
973
|
+
if (existingRetirement) return existingRetirement;
|
|
974
|
+
|
|
975
|
+
const admission = this.getScopeAdmission(scopeId);
|
|
976
|
+
admission.generation++;
|
|
977
|
+
admission.retiring = true;
|
|
978
|
+
let retirement!: Promise<void>;
|
|
979
|
+
retirement = this.retireScopeInternal(scopeId).finally(() => {
|
|
980
|
+
if (this.scopeRetirements.get(scopeId) === retirement) {
|
|
981
|
+
this.scopeRetirements.delete(scopeId);
|
|
982
|
+
}
|
|
983
|
+
admission.retiring = false;
|
|
984
|
+
this.pruneScopeAdmission(scopeId, admission);
|
|
985
|
+
});
|
|
986
|
+
this.scopeRetirements.set(scopeId, retirement);
|
|
987
|
+
return retirement;
|
|
988
|
+
}
|
|
989
|
+
|
|
557
990
|
public async dispose(): Promise<void> {
|
|
558
991
|
if (this.disposePromise) {
|
|
559
992
|
return this.disposePromise;
|
|
@@ -564,52 +997,31 @@ export class FlexHarness<TScope = unknown> {
|
|
|
564
997
|
}
|
|
565
998
|
|
|
566
999
|
private async disposeInternal(): Promise<void> {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
for (const pending of [...state.pendingPermissions.values()]) {
|
|
581
|
-
this.rejectPending(
|
|
582
|
-
state,
|
|
583
|
-
pending,
|
|
584
|
-
new FlexHarnessAbortError('Permission was rejected because FlexHarness was disposed.'),
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
const results = await Promise.allSettled(finalizers);
|
|
589
|
-
await Promise.all(states.map((state) => state.saveTail));
|
|
590
|
-
const detachedCleanupResults = await Promise.allSettled([...this.detachedToolCleanups]);
|
|
1000
|
+
const stateLoads = [...this.stateLoads.entries()];
|
|
1001
|
+
const results = await Promise.allSettled(
|
|
1002
|
+
stateLoads.map(([storageKey, stateLoad]) =>
|
|
1003
|
+
this.drainStorage(
|
|
1004
|
+
storageKey,
|
|
1005
|
+
stateLoad,
|
|
1006
|
+
this.trustInternalError(
|
|
1007
|
+
new FlexHarnessAbortError('The run was aborted because FlexHarness was disposed.'),
|
|
1008
|
+
),
|
|
1009
|
+
),
|
|
1010
|
+
),
|
|
1011
|
+
);
|
|
1012
|
+
await Promise.all([...this.detachedToolCleanups].map((cleanup) => cleanup.completion));
|
|
591
1013
|
this.listeners.clear();
|
|
592
1014
|
this.stateLoads.clear();
|
|
1015
|
+
this.scopeAdmissions.clear();
|
|
1016
|
+
this.scopeRetirements.clear();
|
|
1017
|
+
this.storageDrains.clear();
|
|
593
1018
|
this.detachedToolCleanups.clear();
|
|
594
|
-
for (const state of states) {
|
|
595
|
-
state.sessions.clear();
|
|
596
|
-
state.activeRuns.clear();
|
|
597
|
-
state.pendingPermissions.clear();
|
|
598
|
-
}
|
|
599
1019
|
const unexpectedErrors: unknown[] = [];
|
|
600
1020
|
for (const result of results) {
|
|
601
|
-
if (result.status === '
|
|
602
|
-
|
|
603
|
-
for (const error of result.reason.errors) {
|
|
604
|
-
if (!isAbortError(error)) unexpectedErrors.push(error);
|
|
605
|
-
}
|
|
606
|
-
} else if (!isAbortError(result.reason)) {
|
|
607
|
-
unexpectedErrors.push(result.reason);
|
|
1021
|
+
if (result.status === 'rejected') {
|
|
1022
|
+
this.appendUnexpectedErrors(unexpectedErrors, result.reason);
|
|
608
1023
|
}
|
|
609
1024
|
}
|
|
610
|
-
for (const result of detachedCleanupResults) {
|
|
611
|
-
if (result.status === 'rejected') unexpectedErrors.push(result.reason);
|
|
612
|
-
}
|
|
613
1025
|
if (unexpectedErrors.length > 0) {
|
|
614
1026
|
throw combineErrors(unexpectedErrors);
|
|
615
1027
|
}
|
|
@@ -623,7 +1035,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
623
1035
|
): Promise<IFlexPromptResult> {
|
|
624
1036
|
let modelResolution: IFlexResolvedModel | undefined;
|
|
625
1037
|
let toolHandle: IFlexToolHandle | undefined;
|
|
626
|
-
let result:
|
|
1038
|
+
let result: IRunResultProjection | undefined;
|
|
627
1039
|
let serializedResultMessages: TFlexAgentModelMessage[] | undefined;
|
|
628
1040
|
let originalError: unknown;
|
|
629
1041
|
let reserved = false;
|
|
@@ -649,6 +1061,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
649
1061
|
messageId: run.assistantMessageId,
|
|
650
1062
|
message: reservation.assistantMessage,
|
|
651
1063
|
});
|
|
1064
|
+
this.resolveRunAdmission(run);
|
|
652
1065
|
|
|
653
1066
|
const modelOutcome = Promise.resolve()
|
|
654
1067
|
.then(() => this.modelResolver.resolveModel({
|
|
@@ -660,8 +1073,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
660
1073
|
signal: run.controller.signal,
|
|
661
1074
|
}))
|
|
662
1075
|
.then(
|
|
663
|
-
(value): IResolverOutcome<IFlexResolvedModel> => ({
|
|
664
|
-
|
|
1076
|
+
(value): IResolverOutcome<IFlexResolvedModel> => ({
|
|
1077
|
+
source: 'model',
|
|
1078
|
+
success: true,
|
|
1079
|
+
value,
|
|
1080
|
+
}),
|
|
1081
|
+
(error: unknown): IResolverOutcome<IFlexResolvedModel> => ({
|
|
1082
|
+
source: 'model',
|
|
1083
|
+
success: false,
|
|
1084
|
+
error,
|
|
1085
|
+
}),
|
|
665
1086
|
);
|
|
666
1087
|
const toolOutcome = Promise.resolve()
|
|
667
1088
|
.then(() =>
|
|
@@ -678,8 +1099,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
678
1099
|
: undefined,
|
|
679
1100
|
)
|
|
680
1101
|
.then(
|
|
681
|
-
(value): IResolverOutcome<IFlexToolHandle | undefined> => ({
|
|
682
|
-
|
|
1102
|
+
(value): IResolverOutcome<IFlexToolHandle | undefined> => ({
|
|
1103
|
+
source: 'tools',
|
|
1104
|
+
success: true,
|
|
1105
|
+
value,
|
|
1106
|
+
}),
|
|
1107
|
+
(error: unknown): IResolverOutcome<IFlexToolHandle | undefined> => ({
|
|
1108
|
+
source: 'tools',
|
|
1109
|
+
success: false,
|
|
1110
|
+
error,
|
|
1111
|
+
}),
|
|
683
1112
|
);
|
|
684
1113
|
let resolveAbort!: (outcome: IResolverOutcome<never>) => void;
|
|
685
1114
|
const abortOutcome = new Promise<IResolverOutcome<never>>((resolve) => {
|
|
@@ -698,27 +1127,41 @@ export class FlexHarness<TScope = unknown> {
|
|
|
698
1127
|
const firstResolver = await Promise.race([modelOutcome, toolOutcome, abortOutcome]);
|
|
699
1128
|
if (!firstResolver.success) {
|
|
700
1129
|
run.controller.signal.removeEventListener('abort', onResolverAbort);
|
|
1130
|
+
const firstError = firstResolver.source === 'abort'
|
|
1131
|
+
? firstResolver.error
|
|
1132
|
+
: this.projectExternalError(
|
|
1133
|
+
run,
|
|
1134
|
+
firstResolver.error,
|
|
1135
|
+
firstResolver.source === 'model' ? 'modelResolver' : 'toolProvider',
|
|
1136
|
+
);
|
|
701
1137
|
if (firstResolver.source !== 'abort') {
|
|
702
|
-
this.abortRunInternally(run,
|
|
1138
|
+
this.abortRunInternally(run, firstError);
|
|
703
1139
|
}
|
|
704
1140
|
if (firstResolver.source !== 'tools') {
|
|
705
|
-
this.observeDetachedToolProvider(toolOutcome);
|
|
1141
|
+
this.observeDetachedToolProvider(toolOutcome, run);
|
|
706
1142
|
}
|
|
707
|
-
throw
|
|
1143
|
+
throw firstError;
|
|
708
1144
|
}
|
|
709
1145
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
throw error;
|
|
1146
|
+
try {
|
|
1147
|
+
if (firstResolver.source === 'model') {
|
|
1148
|
+
modelResolution = normalizeResolvedModel(firstResolver.value as IFlexResolvedModel);
|
|
1149
|
+
} else {
|
|
1150
|
+
toolHandle = this.normalizeToolHandle(
|
|
1151
|
+
run,
|
|
1152
|
+
firstResolver.value as IFlexToolHandle | undefined,
|
|
1153
|
+
);
|
|
719
1154
|
}
|
|
720
|
-
}
|
|
721
|
-
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
const resolverError = firstResolver.source === 'model'
|
|
1157
|
+
? this.projectExternalError(run, error, 'modelResolver')
|
|
1158
|
+
: error;
|
|
1159
|
+
run.controller.signal.removeEventListener('abort', onResolverAbort);
|
|
1160
|
+
this.abortRunInternally(run, resolverError);
|
|
1161
|
+
if (firstResolver.source === 'model') {
|
|
1162
|
+
this.observeDetachedToolProvider(toolOutcome, run);
|
|
1163
|
+
}
|
|
1164
|
+
throw resolverError;
|
|
722
1165
|
}
|
|
723
1166
|
|
|
724
1167
|
const secondResolver = await Promise.race([
|
|
@@ -727,29 +1170,53 @@ export class FlexHarness<TScope = unknown> {
|
|
|
727
1170
|
]);
|
|
728
1171
|
run.controller.signal.removeEventListener('abort', onResolverAbort);
|
|
729
1172
|
if (!secondResolver.success) {
|
|
1173
|
+
const secondError = secondResolver.source === 'abort'
|
|
1174
|
+
? secondResolver.error
|
|
1175
|
+
: this.projectExternalError(
|
|
1176
|
+
run,
|
|
1177
|
+
secondResolver.error,
|
|
1178
|
+
secondResolver.source === 'model' ? 'modelResolver' : 'toolProvider',
|
|
1179
|
+
);
|
|
730
1180
|
if (secondResolver.source !== 'abort') {
|
|
731
|
-
this.abortRunInternally(run,
|
|
1181
|
+
this.abortRunInternally(run, secondError);
|
|
732
1182
|
}
|
|
733
1183
|
if (firstResolver.source === 'model' && secondResolver.source === 'abort') {
|
|
734
|
-
this.observeDetachedToolProvider(toolOutcome);
|
|
1184
|
+
this.observeDetachedToolProvider(toolOutcome, run);
|
|
735
1185
|
}
|
|
736
|
-
throw
|
|
1186
|
+
throw secondError;
|
|
737
1187
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1188
|
+
try {
|
|
1189
|
+
if (secondResolver.source === 'model') {
|
|
1190
|
+
modelResolution = normalizeResolvedModel(secondResolver.value as IFlexResolvedModel);
|
|
1191
|
+
} else {
|
|
1192
|
+
toolHandle = this.normalizeToolHandle(
|
|
1193
|
+
run,
|
|
1194
|
+
secondResolver.value as IFlexToolHandle | undefined,
|
|
1195
|
+
);
|
|
745
1196
|
}
|
|
746
|
-
}
|
|
747
|
-
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
const resolverError = secondResolver.source === 'model'
|
|
1199
|
+
? this.projectExternalError(run, error, 'modelResolver')
|
|
1200
|
+
: error;
|
|
1201
|
+
this.abortRunInternally(run, resolverError);
|
|
1202
|
+
throw resolverError;
|
|
748
1203
|
}
|
|
749
1204
|
if (run.controller.signal.aborted) {
|
|
750
1205
|
throw run.controller.signal.reason;
|
|
751
1206
|
}
|
|
752
1207
|
|
|
1208
|
+
let tools: TFlexAgentToolSet | undefined;
|
|
1209
|
+
try {
|
|
1210
|
+
tools = toolHandle
|
|
1211
|
+
? wrapToolSet(
|
|
1212
|
+
toolHandle.tools,
|
|
1213
|
+
this.toolOutputLimits,
|
|
1214
|
+
(error) => this.projectExternalError(run, error, 'toolExecution'),
|
|
1215
|
+
)
|
|
1216
|
+
: undefined;
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
throw this.projectExternalError(run, error, 'toolProvider');
|
|
1219
|
+
}
|
|
753
1220
|
const runnerOptions: Parameters<typeof this.runner>[0] = {
|
|
754
1221
|
model: modelResolution!.model,
|
|
755
1222
|
prompt: prompt.agentPrompt,
|
|
@@ -759,7 +1226,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
759
1226
|
...(options.system ?? modelResolution!.system
|
|
760
1227
|
? { system: options.system ?? modelResolution!.system }
|
|
761
1228
|
: {}),
|
|
762
|
-
...(
|
|
1229
|
+
...(tools ? { tools } : {}),
|
|
763
1230
|
...(modelResolution!.providerOptions
|
|
764
1231
|
? { providerOptions: modelResolution!.providerOptions }
|
|
765
1232
|
: {}),
|
|
@@ -775,16 +1242,22 @@ export class FlexHarness<TScope = unknown> {
|
|
|
775
1242
|
onToolCallFinish: (event) => this.onToolFinish(run, event),
|
|
776
1243
|
};
|
|
777
1244
|
run.phase = 'running';
|
|
778
|
-
|
|
1245
|
+
try {
|
|
1246
|
+
const runnerResult = await this.runner(runnerOptions);
|
|
1247
|
+
serializedResultMessages = serializeAgentMessages(runnerResult.messages);
|
|
1248
|
+
result = normalizeRunResult(runnerResult);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
throw this.projectExternalError(run, error, 'runner');
|
|
1251
|
+
}
|
|
779
1252
|
if (run.callbackError) {
|
|
780
1253
|
throw run.callbackError;
|
|
781
1254
|
}
|
|
782
1255
|
if (run.controller.signal.aborted) {
|
|
783
1256
|
throw run.controller.signal.reason;
|
|
784
1257
|
}
|
|
785
|
-
serializedResultMessages = serializeAgentMessages(result.messages);
|
|
786
1258
|
} catch (error) {
|
|
787
|
-
|
|
1259
|
+
const runError = error ?? new Error('The run failed without an error value.');
|
|
1260
|
+
originalError = reserved ? this.projectExternalError(run, runError, 'runner') : runError;
|
|
788
1261
|
}
|
|
789
1262
|
run.callbacksClosed = true;
|
|
790
1263
|
run.phase = 'closing';
|
|
@@ -793,15 +1266,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
793
1266
|
? run.callbackError
|
|
794
1267
|
: combineErrors([originalError, run.callbackError]);
|
|
795
1268
|
}
|
|
1269
|
+
if (!reserved) {
|
|
1270
|
+
const admissionError = this.projectExternalError(run, originalError, 'persistence');
|
|
1271
|
+
this.rejectRunAdmission(run, admissionError);
|
|
1272
|
+
if (resolved.state.activeRuns.get(run.sessionId) === run) {
|
|
1273
|
+
resolved.state.activeRuns.delete(run.sessionId);
|
|
1274
|
+
}
|
|
1275
|
+
throw admissionError;
|
|
1276
|
+
}
|
|
796
1277
|
if (originalError !== undefined && run.ownerCancellation === undefined) {
|
|
797
1278
|
run.internalFailure ??= originalError;
|
|
798
1279
|
}
|
|
799
1280
|
|
|
800
|
-
if (!reserved) {
|
|
801
|
-
resolved.state.activeRuns.delete(run.sessionId);
|
|
802
|
-
throw originalError;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
1281
|
this.rejectRunPermissions(
|
|
806
1282
|
resolved.state,
|
|
807
1283
|
run,
|
|
@@ -810,7 +1286,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
810
1286
|
new FlexHarnessAbortError('The run ended before permission was resolved.'),
|
|
811
1287
|
);
|
|
812
1288
|
const closeResult = await Promise.allSettled([
|
|
813
|
-
Promise.resolve()
|
|
1289
|
+
Promise.resolve()
|
|
1290
|
+
.then(() => toolHandle?.close?.())
|
|
1291
|
+
.catch((error) => {
|
|
1292
|
+
throw this.projectExternalError(run, error, 'toolCleanup');
|
|
1293
|
+
}),
|
|
814
1294
|
]).then(([settled]) => settled);
|
|
815
1295
|
const errorsBeforePersistence: unknown[] = [];
|
|
816
1296
|
if (originalError !== undefined) errorsBeforePersistence.push(originalError);
|
|
@@ -848,7 +1328,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
848
1328
|
)]).then(([settled]) => settled);
|
|
849
1329
|
|
|
850
1330
|
if (persistenceResult.status === 'rejected') {
|
|
851
|
-
const
|
|
1331
|
+
const persistenceError = this.projectExternalError(
|
|
1332
|
+
run,
|
|
1333
|
+
persistenceResult.reason,
|
|
1334
|
+
'persistence',
|
|
1335
|
+
);
|
|
1336
|
+
const allErrors = [...errorsBeforePersistence, persistenceError];
|
|
852
1337
|
const combinedError = combineErrors(allErrors);
|
|
853
1338
|
const fallback = this.applyRunFinalState(
|
|
854
1339
|
resolved.state,
|
|
@@ -860,14 +1345,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
860
1345
|
combinedError,
|
|
861
1346
|
cancelled,
|
|
862
1347
|
);
|
|
863
|
-
resolved.state.activeRuns.
|
|
1348
|
+
if (resolved.state.activeRuns.get(run.sessionId) === run) {
|
|
1349
|
+
resolved.state.activeRuns.delete(run.sessionId);
|
|
1350
|
+
}
|
|
864
1351
|
this.emitFinalMutationEvents(run, fallback);
|
|
865
1352
|
this.emitFinalRunEvent(run, fallback.assistantMessage, combinedError, cancelled);
|
|
866
1353
|
throw combinedError;
|
|
867
1354
|
}
|
|
868
1355
|
|
|
869
1356
|
const finalized = persistenceResult.value;
|
|
870
|
-
resolved.state.activeRuns.
|
|
1357
|
+
if (resolved.state.activeRuns.get(run.sessionId) === run) {
|
|
1358
|
+
resolved.state.activeRuns.delete(run.sessionId);
|
|
1359
|
+
}
|
|
871
1360
|
this.emitFinalRunEvent(run, finalized.assistantMessage, terminalError, cancelled);
|
|
872
1361
|
if (terminalError !== undefined) throw terminalError;
|
|
873
1362
|
return {
|
|
@@ -945,22 +1434,25 @@ export class FlexHarness<TScope = unknown> {
|
|
|
945
1434
|
run: IActiveRun,
|
|
946
1435
|
prompt: INormalizedFlexPrompt,
|
|
947
1436
|
model: IFlexResolvedModel | undefined,
|
|
948
|
-
result:
|
|
1437
|
+
result: IRunResultProjection | undefined,
|
|
949
1438
|
serializedResultMessages: TFlexAgentModelMessage[] | undefined,
|
|
950
1439
|
originalError: unknown,
|
|
951
1440
|
cancelled: boolean,
|
|
952
1441
|
): Promise<IFinalMutationResult> {
|
|
953
|
-
const final = await this.mutateAndSave(
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1442
|
+
const final = await this.mutateAndSave(
|
|
1443
|
+
state,
|
|
1444
|
+
() =>
|
|
1445
|
+
this.applyRunFinalState(
|
|
1446
|
+
state,
|
|
1447
|
+
run,
|
|
1448
|
+
prompt,
|
|
1449
|
+
model,
|
|
1450
|
+
result,
|
|
1451
|
+
serializedResultMessages,
|
|
1452
|
+
originalError,
|
|
1453
|
+
cancelled,
|
|
1454
|
+
),
|
|
1455
|
+
true,
|
|
964
1456
|
);
|
|
965
1457
|
this.emitFinalMutationEvents(run, final);
|
|
966
1458
|
return final;
|
|
@@ -971,7 +1463,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
971
1463
|
run: IActiveRun,
|
|
972
1464
|
prompt: INormalizedFlexPrompt,
|
|
973
1465
|
model: IFlexResolvedModel | undefined,
|
|
974
|
-
result:
|
|
1466
|
+
result: IRunResultProjection | undefined,
|
|
975
1467
|
serializedResultMessages: TFlexAgentModelMessage[] | undefined,
|
|
976
1468
|
originalError: unknown,
|
|
977
1469
|
cancelled: boolean,
|
|
@@ -1190,16 +1682,19 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1190
1682
|
const output = event.success
|
|
1191
1683
|
? normalizeJsonValue(event.output, this.toolOutputLimits)
|
|
1192
1684
|
: undefined;
|
|
1685
|
+
const projectedError = event.success
|
|
1686
|
+
? undefined
|
|
1687
|
+
: this.projectExternalError(run, event.error, 'toolCallback');
|
|
1193
1688
|
const byteLength = event.success
|
|
1194
1689
|
? Buffer.byteLength(JSON.stringify(output))
|
|
1195
|
-
: Buffer.byteLength(
|
|
1690
|
+
: Buffer.byteLength(projectedError!.message);
|
|
1196
1691
|
if (!this.reserveCallbackCapacity(run, 1, byteLength, 0)) return;
|
|
1197
1692
|
if (event.success) {
|
|
1198
1693
|
part.status = 'completed';
|
|
1199
1694
|
part.output = output!;
|
|
1200
1695
|
} else {
|
|
1201
1696
|
part.status = 'failed';
|
|
1202
|
-
part.error =
|
|
1697
|
+
part.error = projectedError!.message;
|
|
1203
1698
|
}
|
|
1204
1699
|
this.emitPartEvent(run, 'part.completed', part);
|
|
1205
1700
|
}
|
|
@@ -1218,8 +1713,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1218
1713
|
nextOutputBytes > this.callbackLimits.maxOutputBytes ||
|
|
1219
1714
|
nextParts > this.callbackLimits.maxParts
|
|
1220
1715
|
) {
|
|
1221
|
-
const error =
|
|
1222
|
-
|
|
1716
|
+
const error = Object.freeze(
|
|
1717
|
+
new FlexHarnessCallbackOverflowError(
|
|
1718
|
+
`Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
|
|
1719
|
+
),
|
|
1223
1720
|
);
|
|
1224
1721
|
run.callbackError = error;
|
|
1225
1722
|
this.abortRunInternally(run, error);
|
|
@@ -1308,7 +1805,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1308
1805
|
state.pendingPermissions.delete(request.permissionId);
|
|
1309
1806
|
run.pendingPermissionIds.delete(request.permissionId);
|
|
1310
1807
|
run.controller.signal.removeEventListener('abort', abortListener);
|
|
1311
|
-
throw error;
|
|
1808
|
+
throw this.projectExternalError(run, error, 'persistence');
|
|
1312
1809
|
}
|
|
1313
1810
|
if (pending.settled) {
|
|
1314
1811
|
return permissionPromise;
|
|
@@ -1344,49 +1841,67 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1344
1841
|
let addedRememberKey = false;
|
|
1345
1842
|
pending.responding = true;
|
|
1346
1843
|
try {
|
|
1347
|
-
await this.mutateAndSave(
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1844
|
+
await this.mutateAndSave(
|
|
1845
|
+
state,
|
|
1846
|
+
() => {
|
|
1847
|
+
const stored = this.requireSession(state, pending.request.sessionId);
|
|
1848
|
+
if (decision === 'always' && rememberKey) {
|
|
1849
|
+
addedRememberKey = !stored.rememberedPermissionKeys.has(rememberKey);
|
|
1850
|
+
stored.rememberedPermissionKeys.add(rememberKey);
|
|
1851
|
+
}
|
|
1852
|
+
const hasOtherPending = [...state.pendingPermissions.values()].some(
|
|
1853
|
+
(entry) =>
|
|
1854
|
+
entry !== pending &&
|
|
1855
|
+
!entry.settled &&
|
|
1856
|
+
entry.request.sessionId === pending.request.sessionId,
|
|
1857
|
+
);
|
|
1858
|
+
stored.session.status = hasOtherPending ? 'waiting_permission' : 'running';
|
|
1859
|
+
stored.session.activity.status = hasOtherPending ? 'waiting_permission' : 'running';
|
|
1860
|
+
stored.session.updatedAt = new Date().toISOString();
|
|
1861
|
+
},
|
|
1862
|
+
true,
|
|
1863
|
+
);
|
|
1363
1864
|
} catch (error) {
|
|
1865
|
+
const persistenceError = this.projectExternalError(pending.run, error, 'persistence');
|
|
1364
1866
|
pending.responding = false;
|
|
1365
1867
|
if (pending.abortReason !== undefined) {
|
|
1366
1868
|
const abortReason = pending.abortReason;
|
|
1367
1869
|
this.forceRejectPending(state, pending, abortReason);
|
|
1368
|
-
throw combineErrors([abortReason,
|
|
1870
|
+
throw combineErrors([abortReason, persistenceError]);
|
|
1369
1871
|
}
|
|
1370
|
-
throw
|
|
1872
|
+
throw persistenceError;
|
|
1371
1873
|
}
|
|
1372
1874
|
|
|
1373
1875
|
if (pending.abortReason !== undefined) {
|
|
1374
1876
|
const abortReason = pending.abortReason;
|
|
1375
1877
|
const rollbackPromise = addedRememberKey && rememberKey
|
|
1376
|
-
? this.mutateAndSave(
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1878
|
+
? this.mutateAndSave(
|
|
1879
|
+
state,
|
|
1880
|
+
() => {
|
|
1881
|
+
this.requireSession(
|
|
1882
|
+
state,
|
|
1883
|
+
pending.request.sessionId,
|
|
1884
|
+
).rememberedPermissionKeys.delete(rememberKey);
|
|
1885
|
+
},
|
|
1886
|
+
true,
|
|
1887
|
+
)
|
|
1382
1888
|
: Promise.resolve();
|
|
1383
1889
|
pending.responding = false;
|
|
1384
|
-
this.forceRejectPending(state, pending, abortReason);
|
|
1385
1890
|
try {
|
|
1386
1891
|
await rollbackPromise;
|
|
1387
1892
|
} catch (rollbackError) {
|
|
1388
|
-
|
|
1893
|
+
if (addedRememberKey && rememberKey) {
|
|
1894
|
+
state.sessions
|
|
1895
|
+
.get(pending.request.sessionId)
|
|
1896
|
+
?.rememberedPermissionKeys.delete(rememberKey);
|
|
1897
|
+
}
|
|
1898
|
+
this.forceRejectPending(state, pending, abortReason);
|
|
1899
|
+
throw combineErrors([
|
|
1900
|
+
abortReason,
|
|
1901
|
+
this.projectExternalError(pending.run, rollbackError, 'persistence'),
|
|
1902
|
+
]);
|
|
1389
1903
|
}
|
|
1904
|
+
this.forceRejectPending(state, pending, abortReason);
|
|
1390
1905
|
throw abortReason;
|
|
1391
1906
|
}
|
|
1392
1907
|
|
|
@@ -1402,7 +1917,10 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1402
1917
|
decision,
|
|
1403
1918
|
});
|
|
1404
1919
|
if (decision === 'reject') {
|
|
1405
|
-
|
|
1920
|
+
const error = this.trustInternalError(
|
|
1921
|
+
new FlexHarnessPermissionRejectedError(pending.request.permissionId),
|
|
1922
|
+
);
|
|
1923
|
+
pending.reject(error);
|
|
1406
1924
|
} else {
|
|
1407
1925
|
pending.resolve();
|
|
1408
1926
|
}
|
|
@@ -1460,19 +1978,102 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1460
1978
|
}
|
|
1461
1979
|
}
|
|
1462
1980
|
|
|
1981
|
+
private projectExternalError(
|
|
1982
|
+
run: IActiveRun,
|
|
1983
|
+
error: unknown,
|
|
1984
|
+
source: TFlexExternalErrorSource,
|
|
1985
|
+
): Error {
|
|
1986
|
+
if ((typeof error === 'object' && error !== null) || typeof error === 'function') {
|
|
1987
|
+
if (
|
|
1988
|
+
error === run.ownerCancellation
|
|
1989
|
+
|| error === run.callbackError
|
|
1990
|
+
|| this.trustedInternalErrors.has(error)
|
|
1991
|
+
) {
|
|
1992
|
+
return error as Error;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
let info = externalErrorFallback;
|
|
1996
|
+
if (this.externalErrorProjector) {
|
|
1997
|
+
try {
|
|
1998
|
+
info = validateProjectedErrorInfo(this.externalErrorProjector(error, {
|
|
1999
|
+
source,
|
|
2000
|
+
scopeId: run.scopeId,
|
|
2001
|
+
sessionId: run.sessionId,
|
|
2002
|
+
runId: run.runId,
|
|
2003
|
+
}));
|
|
2004
|
+
} catch {
|
|
2005
|
+
info = externalErrorFallback;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return this.trustInternalError(new FlexHarnessExternalError(info));
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
private trustInternalError<TError extends Error>(error: TError): TError {
|
|
2012
|
+
Object.freeze(error);
|
|
2013
|
+
this.trustedInternalErrors.add(error);
|
|
2014
|
+
return error;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
private normalizeToolHandle(
|
|
2018
|
+
run: IActiveRun,
|
|
2019
|
+
toolHandle: IFlexToolHandle | undefined,
|
|
2020
|
+
): IFlexToolHandle | undefined {
|
|
2021
|
+
if (toolHandle === undefined) return undefined;
|
|
2022
|
+
let close: IFlexToolHandle['close'];
|
|
2023
|
+
try {
|
|
2024
|
+
close = toolHandle.close;
|
|
2025
|
+
if (close !== undefined && typeof close !== 'function') {
|
|
2026
|
+
throw new FlexHarnessValidationError('Tool handle close must be a function.');
|
|
2027
|
+
}
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
throw this.projectExternalError(run, error, 'toolProvider');
|
|
2030
|
+
}
|
|
2031
|
+
const closeHandle = close === undefined ? undefined : () => close.call(toolHandle);
|
|
2032
|
+
try {
|
|
2033
|
+
const tools = toolHandle.tools;
|
|
2034
|
+
return {
|
|
2035
|
+
tools,
|
|
2036
|
+
...(closeHandle === undefined ? {} : { close: closeHandle }),
|
|
2037
|
+
};
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
if (closeHandle) this.trackDetachedToolCleanup(closeHandle, run);
|
|
2040
|
+
throw this.projectExternalError(run, error, 'toolProvider');
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
private trackDetachedToolCleanup(
|
|
2045
|
+
cleanup: () => Promise<void> | void,
|
|
2046
|
+
run: IActiveRun,
|
|
2047
|
+
): void {
|
|
2048
|
+
const record = { state: run.state } as IDetachedToolCleanup;
|
|
2049
|
+
record.completion = Promise.resolve()
|
|
2050
|
+
.then(cleanup)
|
|
2051
|
+
.catch((error) => {
|
|
2052
|
+
if (run.state.detachedToolCleanupErrors.length < maxDetachedCleanupErrors) {
|
|
2053
|
+
run.state.detachedToolCleanupErrors.push(
|
|
2054
|
+
this.projectExternalError(run, error, 'toolCleanup'),
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
})
|
|
2058
|
+
.finally(() => {
|
|
2059
|
+
this.detachedToolCleanups.delete(record);
|
|
2060
|
+
});
|
|
2061
|
+
this.detachedToolCleanups.add(record);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1463
2064
|
private observeDetachedToolProvider(
|
|
1464
2065
|
outcomePromise: Promise<IResolverOutcome<IFlexToolHandle | undefined>>,
|
|
2066
|
+
run: IActiveRun,
|
|
1465
2067
|
): void {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
2068
|
+
this.trackDetachedToolCleanup(async () => {
|
|
2069
|
+
const outcome = await outcomePromise;
|
|
2070
|
+
if (!outcome.success || outcome.source !== 'tools' || !outcome.value) return;
|
|
2071
|
+
const close = outcome.value.close;
|
|
2072
|
+
if (close !== undefined && typeof close !== 'function') {
|
|
2073
|
+
throw new FlexHarnessValidationError('Tool handle close must be a function.');
|
|
1469
2074
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
void cleanup.then(
|
|
1473
|
-
() => this.detachedToolCleanups.delete(cleanup),
|
|
1474
|
-
() => undefined,
|
|
1475
|
-
);
|
|
2075
|
+
await close?.call(outcome.value);
|
|
2076
|
+
}, run);
|
|
1476
2077
|
}
|
|
1477
2078
|
|
|
1478
2079
|
private emitEvent(scopeId: string, sessionId: string, details: TEventDetails): void {
|
|
@@ -1495,27 +2096,188 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1495
2096
|
}
|
|
1496
2097
|
}
|
|
1497
2098
|
|
|
1498
|
-
private
|
|
1499
|
-
scopeId
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
2099
|
+
private getScopeAdmission(scopeId: string): IScopeAdmissionState {
|
|
2100
|
+
let admission = this.scopeAdmissions.get(scopeId);
|
|
2101
|
+
if (!admission) {
|
|
2102
|
+
admission = { generation: 0, inFlightResolvers: 0, retiring: false };
|
|
2103
|
+
this.scopeAdmissions.set(scopeId, admission);
|
|
2104
|
+
}
|
|
2105
|
+
return admission;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
private pruneScopeAdmission(scopeId: string, admission: IScopeAdmissionState): void {
|
|
2109
|
+
if (
|
|
2110
|
+
!admission.retiring
|
|
2111
|
+
&& admission.inFlightResolvers === 0
|
|
2112
|
+
&& this.scopeAdmissions.get(scopeId) === admission
|
|
2113
|
+
) {
|
|
2114
|
+
this.scopeAdmissions.delete(scopeId);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
private async retireScopeInternal(scopeId: string): Promise<void> {
|
|
1503
2119
|
const scope = await this.scopeResolver.resolveScope(scopeId);
|
|
1504
2120
|
this.assertOpen();
|
|
1505
2121
|
validateIdentifier(scope.storageKey, 'resolved storageKey');
|
|
1506
|
-
|
|
2122
|
+
const stateLoad = this.stateLoads.get(scope.storageKey);
|
|
1507
2123
|
if (!stateLoad) {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2124
|
+
const existingDrain = this.storageDrains.get(scope.storageKey);
|
|
2125
|
+
if (existingDrain) await existingDrain;
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
await this.drainStorage(
|
|
2129
|
+
scope.storageKey,
|
|
2130
|
+
stateLoad,
|
|
2131
|
+
this.createScopeRetirementError(),
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
private drainStorage(
|
|
2136
|
+
storageKey: string,
|
|
2137
|
+
stateLoad: Promise<IStorageState>,
|
|
2138
|
+
abortReason: FlexHarnessAbortError,
|
|
2139
|
+
): Promise<void> {
|
|
2140
|
+
const existingDrain = this.storageDrains.get(storageKey);
|
|
2141
|
+
if (existingDrain) return existingDrain;
|
|
2142
|
+
let drain!: Promise<void>;
|
|
2143
|
+
drain = this.drainStorageInternal(stateLoad, abortReason).finally(() => {
|
|
2144
|
+
if (this.stateLoads.get(storageKey) === stateLoad) {
|
|
2145
|
+
this.stateLoads.delete(storageKey);
|
|
2146
|
+
}
|
|
2147
|
+
if (this.storageDrains.get(storageKey) === drain) {
|
|
2148
|
+
this.storageDrains.delete(storageKey);
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
this.storageDrains.set(storageKey, drain);
|
|
2152
|
+
return drain;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
private async drainStorageInternal(
|
|
2156
|
+
stateLoad: Promise<IStorageState>,
|
|
2157
|
+
abortReason: FlexHarnessAbortError,
|
|
2158
|
+
): Promise<void> {
|
|
2159
|
+
let state: IStorageState;
|
|
2160
|
+
try {
|
|
2161
|
+
state = await stateLoad;
|
|
2162
|
+
} catch {
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const finalizers: Promise<IFlexPromptResult>[] = [];
|
|
2167
|
+
try {
|
|
2168
|
+
state.lifecycle = 'retiring';
|
|
2169
|
+
for (const run of state.activeRuns.values()) {
|
|
2170
|
+
finalizers.push(run.finalizer);
|
|
2171
|
+
if (run.phase === 'committing') continue;
|
|
2172
|
+
if (run.internalFailure === undefined) run.ownerCancellation ??= abortReason;
|
|
2173
|
+
this.rejectRunPermissions(state, run, abortReason);
|
|
2174
|
+
run.controller.abort(abortReason);
|
|
2175
|
+
}
|
|
2176
|
+
for (const pending of [...state.pendingPermissions.values()]) {
|
|
2177
|
+
this.rejectPending(state, pending, abortReason);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const results = await Promise.allSettled(finalizers);
|
|
2181
|
+
await state.saveTail;
|
|
2182
|
+
await this.drainDetachedToolCleanups(state);
|
|
2183
|
+
const unexpectedErrors: unknown[] = [];
|
|
2184
|
+
for (const result of results) {
|
|
2185
|
+
if (result.status === 'rejected') {
|
|
2186
|
+
this.appendUnexpectedErrors(unexpectedErrors, result.reason);
|
|
1513
2187
|
}
|
|
1514
|
-
}
|
|
2188
|
+
}
|
|
2189
|
+
unexpectedErrors.push(...state.detachedToolCleanupErrors.splice(0));
|
|
2190
|
+
if (unexpectedErrors.length > 0) {
|
|
2191
|
+
throw combineErrors(unexpectedErrors);
|
|
2192
|
+
}
|
|
2193
|
+
} finally {
|
|
2194
|
+
state.lifecycle = 'retired';
|
|
2195
|
+
state.sessions.clear();
|
|
2196
|
+
state.activeRuns.clear();
|
|
2197
|
+
state.pendingPermissions.clear();
|
|
2198
|
+
state.detachedToolCleanupErrors.length = 0;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
private async drainDetachedToolCleanups(state: IStorageState): Promise<void> {
|
|
2203
|
+
while (true) {
|
|
2204
|
+
const cleanups = [...this.detachedToolCleanups]
|
|
2205
|
+
.filter((cleanup) => cleanup.state === state)
|
|
2206
|
+
.map((cleanup) => cleanup.completion);
|
|
2207
|
+
if (cleanups.length === 0) return;
|
|
2208
|
+
await Promise.all(cleanups);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
private appendUnexpectedErrors(target: unknown[], error: unknown): void {
|
|
2213
|
+
if (error instanceof FlexHarnessRunError) {
|
|
2214
|
+
for (const nestedError of error.errors) {
|
|
2215
|
+
this.appendUnexpectedErrors(target, nestedError);
|
|
2216
|
+
}
|
|
2217
|
+
} else if (!isAbortError(error)) {
|
|
2218
|
+
target.push(error);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
private async waitForReadableState(state: IStorageState): Promise<void> {
|
|
2223
|
+
await state.saveTail;
|
|
2224
|
+
this.assertStateAcceptingWork(state);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
private assertStateAcceptingWork(state: IStorageState): void {
|
|
2228
|
+
if (state.lifecycle !== 'active' || this.storageDrains.has(state.storageKey)) {
|
|
2229
|
+
throw this.createScopeRetirementError();
|
|
1515
2230
|
}
|
|
1516
|
-
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
private createScopeRetirementError(): FlexHarnessAbortError {
|
|
2234
|
+
return this.trustInternalError(new FlexHarnessAbortError(scopeRetirementMessage));
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
private async resolveState(
|
|
2238
|
+
scopeId: string,
|
|
2239
|
+
): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
|
|
1517
2240
|
this.assertOpen();
|
|
1518
|
-
|
|
2241
|
+
validateIdentifier(scopeId, 'scopeId');
|
|
2242
|
+
const admission = this.getScopeAdmission(scopeId);
|
|
2243
|
+
if (admission.retiring) throw this.createScopeRetirementError();
|
|
2244
|
+
const generation = admission.generation;
|
|
2245
|
+
admission.inFlightResolvers++;
|
|
2246
|
+
try {
|
|
2247
|
+
const scope = await this.scopeResolver.resolveScope(scopeId);
|
|
2248
|
+
this.assertOpen();
|
|
2249
|
+
if (admission.retiring || admission.generation !== generation) {
|
|
2250
|
+
throw this.createScopeRetirementError();
|
|
2251
|
+
}
|
|
2252
|
+
validateIdentifier(scope.storageKey, 'resolved storageKey');
|
|
2253
|
+
if (this.storageDrains.has(scope.storageKey)) {
|
|
2254
|
+
throw this.createScopeRetirementError();
|
|
2255
|
+
}
|
|
2256
|
+
let stateLoad = this.stateLoads.get(scope.storageKey);
|
|
2257
|
+
if (!stateLoad) {
|
|
2258
|
+
stateLoad = this.loadState(scope.storageKey);
|
|
2259
|
+
this.stateLoads.set(scope.storageKey, stateLoad);
|
|
2260
|
+
void stateLoad.catch(() => {
|
|
2261
|
+
if (this.stateLoads.get(scope.storageKey) === stateLoad) {
|
|
2262
|
+
this.stateLoads.delete(scope.storageKey);
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
const state = await stateLoad;
|
|
2267
|
+
this.assertOpen();
|
|
2268
|
+
if (
|
|
2269
|
+
admission.retiring
|
|
2270
|
+
|| admission.generation !== generation
|
|
2271
|
+
|| this.storageDrains.has(scope.storageKey)
|
|
2272
|
+
|| state.lifecycle !== 'active'
|
|
2273
|
+
) {
|
|
2274
|
+
throw this.createScopeRetirementError();
|
|
2275
|
+
}
|
|
2276
|
+
return { scope, state };
|
|
2277
|
+
} finally {
|
|
2278
|
+
admission.inFlightResolvers--;
|
|
2279
|
+
this.pruneScopeAdmission(scopeId, admission);
|
|
2280
|
+
}
|
|
1519
2281
|
}
|
|
1520
2282
|
|
|
1521
2283
|
private async loadState(storageKey: string): Promise<IStorageState> {
|
|
@@ -1560,11 +2322,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1560
2322
|
activeRuns: new Map<string, IActiveRun>(),
|
|
1561
2323
|
pendingPermissions: new Map<string, IPendingPermission>(),
|
|
1562
2324
|
saveTail: Promise.resolve(),
|
|
2325
|
+
lifecycle: 'active',
|
|
2326
|
+
detachedToolCleanupErrors: [],
|
|
1563
2327
|
};
|
|
1564
2328
|
}
|
|
1565
2329
|
|
|
1566
|
-
private mutateAndSave<T>(
|
|
2330
|
+
private mutateAndSave<T>(
|
|
2331
|
+
state: IStorageState,
|
|
2332
|
+
mutation: () => T,
|
|
2333
|
+
allowDuringDrain = false,
|
|
2334
|
+
): Promise<T> {
|
|
1567
2335
|
const operation = state.saveTail.then(async () => {
|
|
2336
|
+
if (!allowDuringDrain) this.assertStateAcceptingWork(state);
|
|
1568
2337
|
const beforeMutation = this.createSnapshot(state, state.revision);
|
|
1569
2338
|
try {
|
|
1570
2339
|
const result = mutation();
|
|
@@ -1615,6 +2384,76 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1615
2384
|
};
|
|
1616
2385
|
}
|
|
1617
2386
|
|
|
2387
|
+
private messageCursorNamespace(storageKey: string): string {
|
|
2388
|
+
return plugins.crypto.createHash('sha256').update(storageKey).digest('base64url');
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
private createMessageCursor(
|
|
2392
|
+
namespace: string,
|
|
2393
|
+
sessionId: string,
|
|
2394
|
+
anchorMessageId: string,
|
|
2395
|
+
): string {
|
|
2396
|
+
const cursor = Buffer.from(JSON.stringify({
|
|
2397
|
+
version: 1,
|
|
2398
|
+
namespace,
|
|
2399
|
+
sessionId,
|
|
2400
|
+
anchorMessageId,
|
|
2401
|
+
} satisfies IFlexMessageCursor)).toString('base64url');
|
|
2402
|
+
if (Buffer.byteLength(cursor, 'utf8') > maxMessagePageCursorBytes) {
|
|
2403
|
+
throw new FlexHarnessValidationError('Message page cursor exceeds its transfer limit.');
|
|
2404
|
+
}
|
|
2405
|
+
return cursor;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
private parseMessageCursor(cursor: string): IFlexMessageCursor {
|
|
2409
|
+
let parsed: unknown;
|
|
2410
|
+
try {
|
|
2411
|
+
const decoded = Buffer.from(cursor, 'base64url');
|
|
2412
|
+
if (decoded.toString('base64url') !== cursor) {
|
|
2413
|
+
throw new Error('non-canonical cursor');
|
|
2414
|
+
}
|
|
2415
|
+
parsed = JSON.parse(decoded.toString('utf8'));
|
|
2416
|
+
} catch {
|
|
2417
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
2418
|
+
}
|
|
2419
|
+
if (
|
|
2420
|
+
!parsed
|
|
2421
|
+
|| typeof parsed !== 'object'
|
|
2422
|
+
|| Array.isArray(parsed)
|
|
2423
|
+
|| Object.keys(parsed).length !== 4
|
|
2424
|
+
|| (parsed as Partial<IFlexMessageCursor>).version !== 1
|
|
2425
|
+
|| typeof (parsed as Partial<IFlexMessageCursor>).namespace !== 'string'
|
|
2426
|
+
|| typeof (parsed as Partial<IFlexMessageCursor>).sessionId !== 'string'
|
|
2427
|
+
|| typeof (parsed as Partial<IFlexMessageCursor>).anchorMessageId !== 'string'
|
|
2428
|
+
) {
|
|
2429
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
2430
|
+
}
|
|
2431
|
+
const result = parsed as IFlexMessageCursor;
|
|
2432
|
+
if (
|
|
2433
|
+
result.namespace.length === 0
|
|
2434
|
+
|| result.sessionId.length === 0
|
|
2435
|
+
|| result.anchorMessageId.length === 0
|
|
2436
|
+
|| Buffer.byteLength(result.namespace, 'utf8') > 128
|
|
2437
|
+
|| Buffer.byteLength(result.sessionId, 'utf8') > maxTransferIdentifierBytes
|
|
2438
|
+
|| Buffer.byteLength(result.anchorMessageId, 'utf8') > maxTransferIdentifierBytes
|
|
2439
|
+
) {
|
|
2440
|
+
throw new FlexHarnessValidationError('Message page cursor is invalid.');
|
|
2441
|
+
}
|
|
2442
|
+
return result;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
private resolveRunAdmission(run: IActiveRun): void {
|
|
2446
|
+
if (run.admissionSettled) return;
|
|
2447
|
+
run.admissionSettled = true;
|
|
2448
|
+
run.resolveAdmission();
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
private rejectRunAdmission(run: IActiveRun, error: unknown): void {
|
|
2452
|
+
if (run.admissionSettled) return;
|
|
2453
|
+
run.admissionSettled = true;
|
|
2454
|
+
run.rejectAdmission(error);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
1618
2457
|
private requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
|
|
1619
2458
|
validateIdentifier(sessionId, 'sessionId');
|
|
1620
2459
|
const session = state.sessions.get(sessionId);
|