@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +17 -2
- package/dist_ts/classes.flexharness.js +603 -83
- 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 +1 -1
- package/readme.md +27 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +733 -89
- 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,
|
|
@@ -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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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(
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
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,
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
-
|
|
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:
|
|
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> => ({
|
|
664
|
-
|
|
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> => ({
|
|
682
|
-
|
|
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,
|
|
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
|
|
1127
|
+
throw firstError;
|
|
708
1128
|
}
|
|
709
1129
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
}
|
|
721
|
-
|
|
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,
|
|
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
|
|
1170
|
+
throw secondError;
|
|
737
1171
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
}
|
|
747
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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(
|
|
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 =
|
|
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 =
|
|
1222
|
-
|
|
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,
|
|
1847
|
+
throw combineErrors([abortReason, persistenceError]);
|
|
1369
1848
|
}
|
|
1370
|
-
throw
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
|
|
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);
|