@modelprofile.com/flexharness 1.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/.smartconfig.json +34 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.flexharness.d.ts +61 -0
- package/dist_ts/classes.flexharness.js +1312 -0
- package/dist_ts/classes.stores.d.ts +26 -0
- package/dist_ts/classes.stores.js +156 -0
- package/dist_ts/errors.d.ts +44 -0
- package/dist_ts/errors.js +105 -0
- package/dist_ts/index.d.ts +5 -0
- package/dist_ts/index.js +6 -0
- package/dist_ts/interfaces.d.ts +288 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/plugins.d.ts +7 -0
- package/dist_ts/plugins.js +9 -0
- package/dist_ts/utils.json.d.ts +8 -0
- package/dist_ts/utils.json.js +540 -0
- package/dist_ts/utils.prompt.d.ts +8 -0
- package/dist_ts/utils.prompt.js +170 -0
- package/license.md +21 -0
- package/package.json +36 -0
- package/readme.hints.md +29 -0
- package/readme.md +285 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.flexharness.ts +1648 -0
- package/ts/classes.stores.ts +195 -0
- package/ts/errors.ts +125 -0
- package/ts/index.ts +5 -0
- package/ts/interfaces.ts +369 -0
- package/ts/plugins.ts +15 -0
- package/ts/utils.json.ts +651 -0
- package/ts/utils.prompt.ts +193 -0
|
@@ -0,0 +1,1648 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessCallbackOverflowError,
|
|
5
|
+
FlexHarnessClosedError,
|
|
6
|
+
FlexHarnessNotFoundError,
|
|
7
|
+
FlexHarnessPermissionRejectedError,
|
|
8
|
+
FlexHarnessPermissionStateError,
|
|
9
|
+
FlexHarnessRunError,
|
|
10
|
+
FlexHarnessSessionBusyError,
|
|
11
|
+
FlexHarnessValidationError,
|
|
12
|
+
errorMessage,
|
|
13
|
+
errorToInfo,
|
|
14
|
+
} from './errors.js';
|
|
15
|
+
import type {
|
|
16
|
+
IFlexAttachmentMessagePart,
|
|
17
|
+
IFlexCallbackLimits,
|
|
18
|
+
IFlexCreateSessionOptions,
|
|
19
|
+
IFlexHarnessOptions,
|
|
20
|
+
IFlexHarnessSnapshot,
|
|
21
|
+
IFlexMessage,
|
|
22
|
+
IFlexModelIdentity,
|
|
23
|
+
IFlexPartChangedEvent,
|
|
24
|
+
IFlexPermissionRequest,
|
|
25
|
+
IFlexPermissionRequestInput,
|
|
26
|
+
IFlexPromptOptions,
|
|
27
|
+
IFlexPromptResult,
|
|
28
|
+
IFlexResolvedModel,
|
|
29
|
+
IFlexResolvedScope,
|
|
30
|
+
IFlexRunFinishedEvent,
|
|
31
|
+
IFlexSession,
|
|
32
|
+
IFlexStoredSessionSnapshot,
|
|
33
|
+
IFlexToolHandle,
|
|
34
|
+
IFlexToolMessagePart,
|
|
35
|
+
IFlexUpdateSessionOptions,
|
|
36
|
+
IFlexUsage,
|
|
37
|
+
TFlexAgentModelMessage,
|
|
38
|
+
TFlexAgentRunResult,
|
|
39
|
+
TFlexAgentToolCallFinishEvent,
|
|
40
|
+
TFlexAgentToolSet,
|
|
41
|
+
TFlexAttachmentSource,
|
|
42
|
+
TFlexHarnessEvent,
|
|
43
|
+
TFlexHarnessEventListener,
|
|
44
|
+
TFlexMessagePart,
|
|
45
|
+
TFlexPermissionDecision,
|
|
46
|
+
TFlexPrompt,
|
|
47
|
+
TFlexPromptPart,
|
|
48
|
+
} from './interfaces.js';
|
|
49
|
+
import { InMemoryFlexHarnessStore } from './classes.stores.js';
|
|
50
|
+
import {
|
|
51
|
+
assertFlexHarnessSnapshot,
|
|
52
|
+
cloneSerializable,
|
|
53
|
+
deepFreeze,
|
|
54
|
+
normalizeJsonValue,
|
|
55
|
+
resolveJsonLimits,
|
|
56
|
+
serializeAgentMessages,
|
|
57
|
+
} from './utils.json.js';
|
|
58
|
+
import {
|
|
59
|
+
hydrateAgentMessages,
|
|
60
|
+
normalizeFlexPrompt,
|
|
61
|
+
type INormalizedFlexPrompt,
|
|
62
|
+
} from './utils.prompt.js';
|
|
63
|
+
|
|
64
|
+
interface IStoredSessionState {
|
|
65
|
+
session: IFlexSession;
|
|
66
|
+
messages: IFlexMessage[];
|
|
67
|
+
modelHistory: TFlexAgentModelMessage[];
|
|
68
|
+
rememberedPermissionKeys: Set<string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface IStorageState {
|
|
72
|
+
storageKey: string;
|
|
73
|
+
revision: number;
|
|
74
|
+
sessions: Map<string, IStoredSessionState>;
|
|
75
|
+
activeRuns: Map<string, IActiveRun>;
|
|
76
|
+
pendingPermissions: Map<string, IPendingPermission>;
|
|
77
|
+
saveTail: Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface IActiveRun {
|
|
81
|
+
scopeId: string;
|
|
82
|
+
sessionId: string;
|
|
83
|
+
runId: string;
|
|
84
|
+
userMessageId: string;
|
|
85
|
+
assistantMessageId: string;
|
|
86
|
+
controller: AbortController;
|
|
87
|
+
ownerCancellation?: FlexHarnessAbortError;
|
|
88
|
+
internalFailure?: unknown;
|
|
89
|
+
finalizer: Promise<IFlexPromptResult>;
|
|
90
|
+
callbackError?: unknown;
|
|
91
|
+
callbackEventCount: number;
|
|
92
|
+
callbackOutputBytes: number;
|
|
93
|
+
callbackParts: TFlexMessagePart[];
|
|
94
|
+
callbacksClosed: boolean;
|
|
95
|
+
previousHistory: TFlexAgentModelMessage[];
|
|
96
|
+
reasoningPartIds: Map<string, string>;
|
|
97
|
+
toolPartIds: Map<string, string>;
|
|
98
|
+
pendingPermissionIds: Set<string>;
|
|
99
|
+
phase: 'resolving' | 'running' | 'closing' | 'committing';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface IPendingPermission {
|
|
103
|
+
request: IFlexPermissionRequest;
|
|
104
|
+
run: IActiveRun;
|
|
105
|
+
resolve: () => void;
|
|
106
|
+
reject: (error: unknown) => void;
|
|
107
|
+
settled: boolean;
|
|
108
|
+
responding: boolean;
|
|
109
|
+
abortReason?: unknown;
|
|
110
|
+
responseTail: Promise<void>;
|
|
111
|
+
abortListener: () => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface IFinalMutationResult {
|
|
115
|
+
userMessage: IFlexMessage;
|
|
116
|
+
assistantMessage: IFlexMessage;
|
|
117
|
+
terminalParts: TFlexMessagePart[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type TEventDetails = Record<string, unknown> & {
|
|
121
|
+
type: TFlexHarnessEvent['type'];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
interface IExecutableToolRecord extends Record<string, unknown> {
|
|
125
|
+
execute?: (input: unknown, options: unknown) => unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface IResolverOutcome<T> {
|
|
129
|
+
source: 'model' | 'tools' | 'abort';
|
|
130
|
+
success: boolean;
|
|
131
|
+
value?: T;
|
|
132
|
+
error?: unknown;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
136
|
+
maxEvents: 10_000,
|
|
137
|
+
maxOutputBytes: 1024 * 1024,
|
|
138
|
+
maxParts: 2_000,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
142
|
+
return Boolean(
|
|
143
|
+
value &&
|
|
144
|
+
typeof value === 'object' &&
|
|
145
|
+
typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function* normalizeAsyncIterable(
|
|
150
|
+
iterable: AsyncIterable<unknown>,
|
|
151
|
+
limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
|
|
152
|
+
): AsyncGenerator<unknown> {
|
|
153
|
+
for await (const value of iterable) {
|
|
154
|
+
yield normalizeJsonValue(value, limits);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function wrapToolSet(
|
|
159
|
+
tools: TFlexAgentToolSet,
|
|
160
|
+
limits: Required<NonNullable<IFlexHarnessOptions<unknown>['toolOutputLimits']>>,
|
|
161
|
+
): TFlexAgentToolSet {
|
|
162
|
+
const wrapped: Record<string, unknown> = {};
|
|
163
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
164
|
+
const toolRecord = tool as unknown as IExecutableToolRecord;
|
|
165
|
+
if (typeof toolRecord.execute !== 'function') {
|
|
166
|
+
wrapped[name] = tool;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const execute = toolRecord.execute;
|
|
170
|
+
wrapped[name] = {
|
|
171
|
+
...toolRecord,
|
|
172
|
+
execute(input: unknown, options: unknown): unknown {
|
|
173
|
+
const output = execute.call(tool, input, options);
|
|
174
|
+
if (isAsyncIterable(output)) {
|
|
175
|
+
return normalizeAsyncIterable(output, limits);
|
|
176
|
+
}
|
|
177
|
+
return Promise.resolve(output).then((value) => normalizeJsonValue(value, limits));
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return wrapped as TFlexAgentToolSet;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function publicSnapshot<T>(value: T): T {
|
|
185
|
+
return deepFreeze(cloneSerializable(value));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validateIdentifier(value: string, name: string): void {
|
|
189
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
190
|
+
throw new FlexHarnessValidationError(`${name} must be a non-empty string.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateModelIdentity(identity: IFlexModelIdentity): void {
|
|
195
|
+
validateIdentifier(identity.provider, 'model identity provider');
|
|
196
|
+
validateIdentifier(identity.model, 'model identity model');
|
|
197
|
+
if (identity.displayName !== undefined) {
|
|
198
|
+
validateIdentifier(identity.displayName, 'model identity displayName');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function combineErrors(errors: unknown[]): unknown {
|
|
203
|
+
if (errors.length === 1) {
|
|
204
|
+
return errors[0];
|
|
205
|
+
}
|
|
206
|
+
return new FlexHarnessRunError(errors);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isAbortError(error: unknown): boolean {
|
|
210
|
+
return (
|
|
211
|
+
error instanceof FlexHarnessAbortError ||
|
|
212
|
+
(error instanceof DOMException && error.name === 'AbortError') ||
|
|
213
|
+
(error instanceof Error && error.name === 'AbortError')
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function makeUsage(result: TFlexAgentRunResult): IFlexUsage {
|
|
218
|
+
return {
|
|
219
|
+
inputTokens: result.usage.inputTokens,
|
|
220
|
+
outputTokens: result.usage.outputTokens,
|
|
221
|
+
totalTokens: result.usage.totalTokens,
|
|
222
|
+
cacheReadTokens: result.usage.cacheReadTokens,
|
|
223
|
+
cacheWriteTokens: result.usage.cacheWriteTokens,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function base64Size(data: string): number | undefined {
|
|
228
|
+
const compact = data.replace(/\s/g, '');
|
|
229
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
return Buffer.from(compact, 'base64').byteLength;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function describeAttachment(
|
|
236
|
+
part: Exclude<TFlexPromptPart, { type: 'text' }>,
|
|
237
|
+
): Omit<IFlexAttachmentMessagePart, 'partId' | 'type' | 'attachmentType'> {
|
|
238
|
+
let source: TFlexAttachmentSource = 'inline-base64';
|
|
239
|
+
let sizeBytes: number | undefined;
|
|
240
|
+
let inferredMediaType: string | undefined;
|
|
241
|
+
|
|
242
|
+
if (/^data:/i.test(part.data)) {
|
|
243
|
+
source = 'data-url';
|
|
244
|
+
const commaIndex = part.data.indexOf(',');
|
|
245
|
+
if (commaIndex >= 0) {
|
|
246
|
+
const metadata = part.data.slice(5, commaIndex);
|
|
247
|
+
const payload = part.data.slice(commaIndex + 1);
|
|
248
|
+
const metadataParts = metadata.split(';');
|
|
249
|
+
if (metadataParts[0]?.includes('/')) inferredMediaType = metadataParts[0];
|
|
250
|
+
if (metadataParts.some((entry) => entry.toLowerCase() === 'base64')) {
|
|
251
|
+
sizeBytes = base64Size(payload);
|
|
252
|
+
} else {
|
|
253
|
+
try {
|
|
254
|
+
sizeBytes = Buffer.byteLength(decodeURIComponent(payload));
|
|
255
|
+
} catch {
|
|
256
|
+
// Malformed URL encoding keeps size indeterminate without exposing the payload.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
try {
|
|
262
|
+
const url = new URL(part.data);
|
|
263
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
264
|
+
source = 'remote-url';
|
|
265
|
+
} else {
|
|
266
|
+
sizeBytes = base64Size(part.data);
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
sizeBytes = base64Size(part.data);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
source,
|
|
275
|
+
...(sizeBytes !== undefined ? { sizeBytes } : {}),
|
|
276
|
+
...(part.mediaType ?? inferredMediaType
|
|
277
|
+
? { mediaType: part.mediaType ?? inferredMediaType }
|
|
278
|
+
: {}),
|
|
279
|
+
...(part.name ? { name: part.name } : {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions): void {
|
|
284
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
285
|
+
throw new FlexHarnessValidationError('updateSession options must be a plain object.');
|
|
286
|
+
}
|
|
287
|
+
const keys = Object.keys(options);
|
|
288
|
+
if (keys.length === 0) {
|
|
289
|
+
throw new FlexHarnessValidationError('updateSession requires title or archived.');
|
|
290
|
+
}
|
|
291
|
+
const unsupportedKey = keys.find((key) => key !== 'title' && key !== 'archived');
|
|
292
|
+
if (unsupportedKey) {
|
|
293
|
+
throw new FlexHarnessValidationError(`updateSession does not support "${unsupportedKey}".`);
|
|
294
|
+
}
|
|
295
|
+
if (Object.prototype.hasOwnProperty.call(options, 'title')) {
|
|
296
|
+
if (options.title !== null) validateIdentifier(options.title!, 'title');
|
|
297
|
+
}
|
|
298
|
+
if (
|
|
299
|
+
Object.prototype.hasOwnProperty.call(options, 'archived') &&
|
|
300
|
+
typeof options.archived !== 'boolean'
|
|
301
|
+
) {
|
|
302
|
+
throw new FlexHarnessValidationError('archived must be a boolean.');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function resolveCallbackLimits(
|
|
307
|
+
limits: IFlexCallbackLimits = {},
|
|
308
|
+
): Required<IFlexCallbackLimits> {
|
|
309
|
+
const resolved = {
|
|
310
|
+
maxEvents: limits.maxEvents ?? DEFAULT_CALLBACK_LIMITS.maxEvents,
|
|
311
|
+
maxOutputBytes: limits.maxOutputBytes ?? DEFAULT_CALLBACK_LIMITS.maxOutputBytes,
|
|
312
|
+
maxParts: limits.maxParts ?? DEFAULT_CALLBACK_LIMITS.maxParts,
|
|
313
|
+
};
|
|
314
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
315
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
316
|
+
throw new FlexHarnessValidationError(`callbackLimits.${name} must be a positive integer.`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return resolved;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export class FlexHarness<TScope = unknown> {
|
|
323
|
+
private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
324
|
+
private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
325
|
+
private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
|
|
326
|
+
private readonly store: NonNullable<IFlexHarnessOptions<TScope>['store']>;
|
|
327
|
+
private readonly runner: NonNullable<IFlexHarnessOptions<TScope>['runner']>;
|
|
328
|
+
private readonly toolOutputLimits: Required<
|
|
329
|
+
NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>
|
|
330
|
+
>;
|
|
331
|
+
private readonly callbackLimits: Required<IFlexCallbackLimits>;
|
|
332
|
+
private readonly stateLoads = new Map<string, Promise<IStorageState>>();
|
|
333
|
+
private readonly listeners = new Set<TFlexHarnessEventListener>();
|
|
334
|
+
private readonly detachedToolCleanups = new Set<Promise<void>>();
|
|
335
|
+
private sequence = 0;
|
|
336
|
+
private closed = false;
|
|
337
|
+
private disposePromise?: Promise<void>;
|
|
338
|
+
|
|
339
|
+
constructor(options: IFlexHarnessOptions<TScope>) {
|
|
340
|
+
this.scopeResolver = options.scopeResolver;
|
|
341
|
+
this.modelResolver = options.modelResolver;
|
|
342
|
+
this.toolProvider = options.toolProvider;
|
|
343
|
+
this.store = options.store ?? new InMemoryFlexHarnessStore();
|
|
344
|
+
this.runner = options.runner ?? plugins.runAgent;
|
|
345
|
+
this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
|
|
346
|
+
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public async listSessions(scopeId: string): Promise<IFlexSession[]> {
|
|
350
|
+
const { state } = await this.resolveState(scopeId);
|
|
351
|
+
await state.saveTail;
|
|
352
|
+
return publicSnapshot(
|
|
353
|
+
[...state.sessions.values()]
|
|
354
|
+
.map((entry) => entry.session)
|
|
355
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public async createSession(
|
|
360
|
+
scopeId: string,
|
|
361
|
+
options: IFlexCreateSessionOptions = {},
|
|
362
|
+
): Promise<IFlexSession> {
|
|
363
|
+
const { state } = await this.resolveState(scopeId);
|
|
364
|
+
const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
|
|
365
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
366
|
+
if (options.title !== undefined) {
|
|
367
|
+
validateIdentifier(options.title, 'title');
|
|
368
|
+
}
|
|
369
|
+
const session = await this.mutateAndSave(state, () => {
|
|
370
|
+
if (state.sessions.has(sessionId)) {
|
|
371
|
+
throw new FlexHarnessValidationError(`Session "${sessionId}" already exists.`);
|
|
372
|
+
}
|
|
373
|
+
const timestamp = new Date().toISOString();
|
|
374
|
+
const nextSession: IFlexSession = {
|
|
375
|
+
scopeId,
|
|
376
|
+
sessionId,
|
|
377
|
+
...(options.title ? { title: options.title } : {}),
|
|
378
|
+
createdAt: timestamp,
|
|
379
|
+
updatedAt: timestamp,
|
|
380
|
+
status: 'idle',
|
|
381
|
+
activity: { status: 'idle' },
|
|
382
|
+
};
|
|
383
|
+
state.sessions.set(sessionId, {
|
|
384
|
+
session: nextSession,
|
|
385
|
+
messages: [],
|
|
386
|
+
modelHistory: [],
|
|
387
|
+
rememberedPermissionKeys: new Set<string>(),
|
|
388
|
+
});
|
|
389
|
+
return publicSnapshot(nextSession);
|
|
390
|
+
});
|
|
391
|
+
this.emitEvent(scopeId, sessionId, {
|
|
392
|
+
type: 'session.created',
|
|
393
|
+
session,
|
|
394
|
+
});
|
|
395
|
+
return session;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
public async getSession(scopeId: string, sessionId: string): Promise<IFlexSession> {
|
|
399
|
+
const { state } = await this.resolveState(scopeId);
|
|
400
|
+
await state.saveTail;
|
|
401
|
+
return publicSnapshot(this.requireSession(state, sessionId).session);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
public async updateSession(
|
|
405
|
+
scopeId: string,
|
|
406
|
+
sessionId: string,
|
|
407
|
+
options: IFlexUpdateSessionOptions,
|
|
408
|
+
): Promise<IFlexSession> {
|
|
409
|
+
validateUpdateSessionOptions(options);
|
|
410
|
+
const { state } = await this.resolveState(scopeId);
|
|
411
|
+
const session = await this.mutateAndSave(state, () => {
|
|
412
|
+
const stored = this.requireMutableSession(state, sessionId);
|
|
413
|
+
const timestamp = new Date().toISOString();
|
|
414
|
+
if (Object.prototype.hasOwnProperty.call(options, 'title')) {
|
|
415
|
+
if (options.title === null) {
|
|
416
|
+
delete stored.session.title;
|
|
417
|
+
} else {
|
|
418
|
+
stored.session.title = options.title!;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (options.archived === true) {
|
|
422
|
+
stored.session.archivedAt ??= timestamp;
|
|
423
|
+
} else if (options.archived === false) {
|
|
424
|
+
delete stored.session.archivedAt;
|
|
425
|
+
}
|
|
426
|
+
stored.session.updatedAt = timestamp;
|
|
427
|
+
return publicSnapshot(stored.session);
|
|
428
|
+
});
|
|
429
|
+
this.emitEvent(scopeId, sessionId, {
|
|
430
|
+
type: 'session.updated',
|
|
431
|
+
session,
|
|
432
|
+
});
|
|
433
|
+
return session;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
437
|
+
const { state } = await this.resolveState(scopeId);
|
|
438
|
+
const session = await this.mutateAndSave(state, () => {
|
|
439
|
+
const stored = this.requireMutableSession(state, sessionId);
|
|
440
|
+
const deletedSession = publicSnapshot(stored.session);
|
|
441
|
+
state.sessions.delete(sessionId);
|
|
442
|
+
return deletedSession;
|
|
443
|
+
});
|
|
444
|
+
this.emitEvent(scopeId, sessionId, {
|
|
445
|
+
type: 'session.deleted',
|
|
446
|
+
session,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
|
|
451
|
+
const { state } = await this.resolveState(scopeId);
|
|
452
|
+
await state.saveTail;
|
|
453
|
+
return publicSnapshot(this.requireSession(state, sessionId).messages);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
public async prompt(
|
|
457
|
+
scopeId: string,
|
|
458
|
+
sessionId: string,
|
|
459
|
+
prompt: TFlexPrompt,
|
|
460
|
+
options: IFlexPromptOptions = {},
|
|
461
|
+
): Promise<IFlexPromptResult> {
|
|
462
|
+
const normalizedPrompt = normalizeFlexPrompt(prompt);
|
|
463
|
+
if (options.maxSteps !== undefined && (!Number.isSafeInteger(options.maxSteps) || options.maxSteps < 1)) {
|
|
464
|
+
throw new FlexHarnessValidationError('maxSteps must be a positive integer.');
|
|
465
|
+
}
|
|
466
|
+
const resolved = await this.resolveState(scopeId);
|
|
467
|
+
await resolved.state.saveTail;
|
|
468
|
+
this.assertOpen();
|
|
469
|
+
this.requireSession(resolved.state, sessionId);
|
|
470
|
+
if (resolved.state.activeRuns.has(sessionId)) {
|
|
471
|
+
throw new FlexHarnessSessionBusyError(sessionId);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const run: IActiveRun = {
|
|
475
|
+
scopeId,
|
|
476
|
+
sessionId,
|
|
477
|
+
runId: plugins.crypto.randomUUID(),
|
|
478
|
+
userMessageId: plugins.crypto.randomUUID(),
|
|
479
|
+
assistantMessageId: plugins.crypto.randomUUID(),
|
|
480
|
+
controller: new AbortController(),
|
|
481
|
+
finalizer: Promise.reject(new Error('Run finalizer was not initialized.')),
|
|
482
|
+
callbackEventCount: 0,
|
|
483
|
+
callbackOutputBytes: 0,
|
|
484
|
+
callbackParts: [],
|
|
485
|
+
callbacksClosed: false,
|
|
486
|
+
previousHistory: [],
|
|
487
|
+
reasoningPartIds: new Map<string, string>(),
|
|
488
|
+
toolPartIds: new Map<string, string>(),
|
|
489
|
+
pendingPermissionIds: new Set<string>(),
|
|
490
|
+
phase: 'resolving',
|
|
491
|
+
};
|
|
492
|
+
void run.finalizer.catch(() => undefined);
|
|
493
|
+
resolved.state.activeRuns.set(sessionId, run);
|
|
494
|
+
run.finalizer = this.executeRun(resolved, run, normalizedPrompt, options);
|
|
495
|
+
return run.finalizer;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
public async abort(scopeId: string, sessionId: string, reason?: string): Promise<boolean> {
|
|
499
|
+
const { state } = await this.resolveState(scopeId);
|
|
500
|
+
const run = state.activeRuns.get(sessionId);
|
|
501
|
+
if (!run || run.phase === 'committing') {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const error = new FlexHarnessAbortError(reason ?? 'The run was aborted by the caller.');
|
|
505
|
+
if (run.internalFailure === undefined) run.ownerCancellation ??= error;
|
|
506
|
+
this.rejectRunPermissions(state, run, error);
|
|
507
|
+
run.controller.abort(error);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
public async listPendingPermissions(
|
|
512
|
+
scopeId: string,
|
|
513
|
+
sessionId?: string,
|
|
514
|
+
): Promise<IFlexPermissionRequest[]> {
|
|
515
|
+
const { state } = await this.resolveState(scopeId);
|
|
516
|
+
await state.saveTail;
|
|
517
|
+
return publicSnapshot(
|
|
518
|
+
[...state.pendingPermissions.values()]
|
|
519
|
+
.map((pending) => pending.request)
|
|
520
|
+
.filter((request) => !sessionId || request.sessionId === sessionId)
|
|
521
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt)),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
public async respondToPermission(
|
|
526
|
+
scopeId: string,
|
|
527
|
+
sessionId: string,
|
|
528
|
+
permissionId: string,
|
|
529
|
+
decision: TFlexPermissionDecision,
|
|
530
|
+
): Promise<void> {
|
|
531
|
+
const { state } = await this.resolveState(scopeId);
|
|
532
|
+
const pending = state.pendingPermissions.get(permissionId);
|
|
533
|
+
if (!pending || pending.request.sessionId !== sessionId) {
|
|
534
|
+
throw new FlexHarnessNotFoundError('Permission', permissionId);
|
|
535
|
+
}
|
|
536
|
+
const response = pending.responseTail.then(() =>
|
|
537
|
+
this.applyPermissionResponse(state, pending, decision),
|
|
538
|
+
);
|
|
539
|
+
pending.responseTail = response.then(
|
|
540
|
+
() => undefined,
|
|
541
|
+
() => undefined,
|
|
542
|
+
);
|
|
543
|
+
return response;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
public subscribe(listener: TFlexHarnessEventListener): () => void {
|
|
547
|
+
this.assertOpen();
|
|
548
|
+
this.listeners.add(listener);
|
|
549
|
+
let subscribed = true;
|
|
550
|
+
return () => {
|
|
551
|
+
if (!subscribed) return;
|
|
552
|
+
subscribed = false;
|
|
553
|
+
this.listeners.delete(listener);
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
public async dispose(): Promise<void> {
|
|
558
|
+
if (this.disposePromise) {
|
|
559
|
+
return this.disposePromise;
|
|
560
|
+
}
|
|
561
|
+
this.closed = true;
|
|
562
|
+
this.disposePromise = this.disposeInternal();
|
|
563
|
+
return this.disposePromise;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private async disposeInternal(): Promise<void> {
|
|
567
|
+
const states = (
|
|
568
|
+
await Promise.allSettled([...this.stateLoads.values()])
|
|
569
|
+
).flatMap((result) => (result.status === 'fulfilled' ? [result.value] : []));
|
|
570
|
+
const finalizers: Promise<IFlexPromptResult>[] = [];
|
|
571
|
+
for (const state of states) {
|
|
572
|
+
for (const run of state.activeRuns.values()) {
|
|
573
|
+
finalizers.push(run.finalizer);
|
|
574
|
+
if (run.phase === 'committing') continue;
|
|
575
|
+
const error = new FlexHarnessAbortError('The run was aborted because FlexHarness was disposed.');
|
|
576
|
+
if (run.internalFailure === undefined) run.ownerCancellation ??= error;
|
|
577
|
+
this.rejectRunPermissions(state, run, error);
|
|
578
|
+
run.controller.abort(error);
|
|
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]);
|
|
591
|
+
this.listeners.clear();
|
|
592
|
+
this.stateLoads.clear();
|
|
593
|
+
this.detachedToolCleanups.clear();
|
|
594
|
+
for (const state of states) {
|
|
595
|
+
state.sessions.clear();
|
|
596
|
+
state.activeRuns.clear();
|
|
597
|
+
state.pendingPermissions.clear();
|
|
598
|
+
}
|
|
599
|
+
const unexpectedErrors: unknown[] = [];
|
|
600
|
+
for (const result of results) {
|
|
601
|
+
if (result.status === 'fulfilled') continue;
|
|
602
|
+
if (result.reason instanceof FlexHarnessRunError) {
|
|
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);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
for (const result of detachedCleanupResults) {
|
|
611
|
+
if (result.status === 'rejected') unexpectedErrors.push(result.reason);
|
|
612
|
+
}
|
|
613
|
+
if (unexpectedErrors.length > 0) {
|
|
614
|
+
throw combineErrors(unexpectedErrors);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private async executeRun(
|
|
619
|
+
resolved: { scope: IFlexResolvedScope<TScope>; state: IStorageState },
|
|
620
|
+
run: IActiveRun,
|
|
621
|
+
prompt: INormalizedFlexPrompt,
|
|
622
|
+
options: IFlexPromptOptions,
|
|
623
|
+
): Promise<IFlexPromptResult> {
|
|
624
|
+
let modelResolution: IFlexResolvedModel | undefined;
|
|
625
|
+
let toolHandle: IFlexToolHandle | undefined;
|
|
626
|
+
let result: TFlexAgentRunResult | undefined;
|
|
627
|
+
let serializedResultMessages: TFlexAgentModelMessage[] | undefined;
|
|
628
|
+
let originalError: unknown;
|
|
629
|
+
let reserved = false;
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const reservation = await this.reserveRun(resolved.state, run, prompt);
|
|
633
|
+
reserved = true;
|
|
634
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
635
|
+
type: 'run.started',
|
|
636
|
+
runId: run.runId,
|
|
637
|
+
messageId: run.userMessageId,
|
|
638
|
+
session: reservation.session,
|
|
639
|
+
});
|
|
640
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
641
|
+
type: 'message.created',
|
|
642
|
+
runId: run.runId,
|
|
643
|
+
messageId: run.userMessageId,
|
|
644
|
+
message: reservation.userMessage,
|
|
645
|
+
});
|
|
646
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
647
|
+
type: 'message.created',
|
|
648
|
+
runId: run.runId,
|
|
649
|
+
messageId: run.assistantMessageId,
|
|
650
|
+
message: reservation.assistantMessage,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const modelOutcome = Promise.resolve()
|
|
654
|
+
.then(() => this.modelResolver.resolveModel({
|
|
655
|
+
scopeId: run.scopeId,
|
|
656
|
+
scope: resolved.scope.scope,
|
|
657
|
+
sessionId: run.sessionId,
|
|
658
|
+
runId: run.runId,
|
|
659
|
+
...(options.modelHint ? { modelHint: options.modelHint } : {}),
|
|
660
|
+
signal: run.controller.signal,
|
|
661
|
+
}))
|
|
662
|
+
.then(
|
|
663
|
+
(value): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: true, value }),
|
|
664
|
+
(error: unknown): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: false, error }),
|
|
665
|
+
);
|
|
666
|
+
const toolOutcome = Promise.resolve()
|
|
667
|
+
.then(() =>
|
|
668
|
+
this.toolProvider
|
|
669
|
+
? this.toolProvider.provideTools({
|
|
670
|
+
scopeId: run.scopeId,
|
|
671
|
+
scope: resolved.scope.scope,
|
|
672
|
+
sessionId: run.sessionId,
|
|
673
|
+
runId: run.runId,
|
|
674
|
+
signal: run.controller.signal,
|
|
675
|
+
requestPermission: (request) =>
|
|
676
|
+
this.requestPermission(resolved.state, run, request),
|
|
677
|
+
})
|
|
678
|
+
: undefined,
|
|
679
|
+
)
|
|
680
|
+
.then(
|
|
681
|
+
(value): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: true, value }),
|
|
682
|
+
(error: unknown): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: false, error }),
|
|
683
|
+
);
|
|
684
|
+
let resolveAbort!: (outcome: IResolverOutcome<never>) => void;
|
|
685
|
+
const abortOutcome = new Promise<IResolverOutcome<never>>((resolve) => {
|
|
686
|
+
resolveAbort = resolve;
|
|
687
|
+
});
|
|
688
|
+
const onResolverAbort = () => {
|
|
689
|
+
resolveAbort({
|
|
690
|
+
source: 'abort',
|
|
691
|
+
success: false,
|
|
692
|
+
error: run.controller.signal.reason ?? new FlexHarnessAbortError(),
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
run.controller.signal.addEventListener('abort', onResolverAbort, { once: true });
|
|
696
|
+
if (run.controller.signal.aborted) onResolverAbort();
|
|
697
|
+
|
|
698
|
+
const firstResolver = await Promise.race([modelOutcome, toolOutcome, abortOutcome]);
|
|
699
|
+
if (!firstResolver.success) {
|
|
700
|
+
run.controller.signal.removeEventListener('abort', onResolverAbort);
|
|
701
|
+
if (firstResolver.source !== 'abort') {
|
|
702
|
+
this.abortRunInternally(run, firstResolver.error);
|
|
703
|
+
}
|
|
704
|
+
if (firstResolver.source !== 'tools') {
|
|
705
|
+
this.observeDetachedToolProvider(toolOutcome);
|
|
706
|
+
}
|
|
707
|
+
throw firstResolver.error;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (firstResolver.source === 'model') {
|
|
711
|
+
modelResolution = firstResolver.value as IFlexResolvedModel;
|
|
712
|
+
try {
|
|
713
|
+
validateModelIdentity(modelResolution.identity);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
run.controller.signal.removeEventListener('abort', onResolverAbort);
|
|
716
|
+
this.abortRunInternally(run, error);
|
|
717
|
+
this.observeDetachedToolProvider(toolOutcome);
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
toolHandle = firstResolver.value as IFlexToolHandle | undefined;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const secondResolver = await Promise.race([
|
|
725
|
+
firstResolver.source === 'model' ? toolOutcome : modelOutcome,
|
|
726
|
+
abortOutcome,
|
|
727
|
+
]);
|
|
728
|
+
run.controller.signal.removeEventListener('abort', onResolverAbort);
|
|
729
|
+
if (!secondResolver.success) {
|
|
730
|
+
if (secondResolver.source !== 'abort') {
|
|
731
|
+
this.abortRunInternally(run, secondResolver.error);
|
|
732
|
+
}
|
|
733
|
+
if (firstResolver.source === 'model' && secondResolver.source === 'abort') {
|
|
734
|
+
this.observeDetachedToolProvider(toolOutcome);
|
|
735
|
+
}
|
|
736
|
+
throw secondResolver.error;
|
|
737
|
+
}
|
|
738
|
+
if (secondResolver.source === 'model') {
|
|
739
|
+
modelResolution = secondResolver.value as IFlexResolvedModel;
|
|
740
|
+
try {
|
|
741
|
+
validateModelIdentity(modelResolution.identity);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
this.abortRunInternally(run, error);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
toolHandle = secondResolver.value as IFlexToolHandle | undefined;
|
|
748
|
+
}
|
|
749
|
+
if (run.controller.signal.aborted) {
|
|
750
|
+
throw run.controller.signal.reason;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const runnerOptions: Parameters<typeof this.runner>[0] = {
|
|
754
|
+
model: modelResolution!.model,
|
|
755
|
+
prompt: prompt.agentPrompt,
|
|
756
|
+
messages: hydrateAgentMessages(run.previousHistory),
|
|
757
|
+
sessionId: run.sessionId,
|
|
758
|
+
abort: run.controller.signal,
|
|
759
|
+
...(options.system ?? modelResolution!.system
|
|
760
|
+
? { system: options.system ?? modelResolution!.system }
|
|
761
|
+
: {}),
|
|
762
|
+
...(toolHandle ? { tools: wrapToolSet(toolHandle.tools, this.toolOutputLimits) } : {}),
|
|
763
|
+
...(modelResolution!.providerOptions
|
|
764
|
+
? { providerOptions: modelResolution!.providerOptions }
|
|
765
|
+
: {}),
|
|
766
|
+
...(modelResolution!.cache !== undefined ? { cache: modelResolution!.cache } : {}),
|
|
767
|
+
...(options.maxSteps ?? modelResolution!.maxSteps
|
|
768
|
+
? { maxSteps: options.maxSteps ?? modelResolution!.maxSteps }
|
|
769
|
+
: {}),
|
|
770
|
+
onToken: (delta) => this.onTextDelta(run, delta),
|
|
771
|
+
onReasoningStart: (id) => this.onReasoningStart(run, id),
|
|
772
|
+
onReasoningDelta: (id, delta) => this.onReasoningDelta(run, id, delta),
|
|
773
|
+
onReasoningEnd: (id, text) => this.onReasoningEnd(run, id, text),
|
|
774
|
+
onToolCallStart: (event) => this.onToolStart(run, event),
|
|
775
|
+
onToolCallFinish: (event) => this.onToolFinish(run, event),
|
|
776
|
+
};
|
|
777
|
+
run.phase = 'running';
|
|
778
|
+
result = await this.runner(runnerOptions);
|
|
779
|
+
if (run.callbackError) {
|
|
780
|
+
throw run.callbackError;
|
|
781
|
+
}
|
|
782
|
+
if (run.controller.signal.aborted) {
|
|
783
|
+
throw run.controller.signal.reason;
|
|
784
|
+
}
|
|
785
|
+
serializedResultMessages = serializeAgentMessages(result.messages);
|
|
786
|
+
} catch (error) {
|
|
787
|
+
originalError = error ?? new Error('The run failed without an error value.');
|
|
788
|
+
}
|
|
789
|
+
run.callbacksClosed = true;
|
|
790
|
+
run.phase = 'closing';
|
|
791
|
+
if (run.callbackError && originalError !== run.callbackError) {
|
|
792
|
+
originalError = originalError === undefined
|
|
793
|
+
? run.callbackError
|
|
794
|
+
: combineErrors([originalError, run.callbackError]);
|
|
795
|
+
}
|
|
796
|
+
if (originalError !== undefined && run.ownerCancellation === undefined) {
|
|
797
|
+
run.internalFailure ??= originalError;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!reserved) {
|
|
801
|
+
resolved.state.activeRuns.delete(run.sessionId);
|
|
802
|
+
throw originalError;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
this.rejectRunPermissions(
|
|
806
|
+
resolved.state,
|
|
807
|
+
run,
|
|
808
|
+
originalError ??
|
|
809
|
+
run.ownerCancellation ??
|
|
810
|
+
new FlexHarnessAbortError('The run ended before permission was resolved.'),
|
|
811
|
+
);
|
|
812
|
+
const closeResult = await Promise.allSettled([
|
|
813
|
+
Promise.resolve().then(() => toolHandle?.close?.()),
|
|
814
|
+
]).then(([settled]) => settled);
|
|
815
|
+
const errorsBeforePersistence: unknown[] = [];
|
|
816
|
+
if (originalError !== undefined) errorsBeforePersistence.push(originalError);
|
|
817
|
+
if (
|
|
818
|
+
originalError === undefined &&
|
|
819
|
+
run.ownerCancellation !== undefined
|
|
820
|
+
) {
|
|
821
|
+
errorsBeforePersistence.push(run.ownerCancellation);
|
|
822
|
+
}
|
|
823
|
+
if (closeResult.status === 'rejected') {
|
|
824
|
+
errorsBeforePersistence.push(closeResult.reason);
|
|
825
|
+
run.internalFailure ??= closeResult.reason;
|
|
826
|
+
}
|
|
827
|
+
run.phase = 'committing';
|
|
828
|
+
if (
|
|
829
|
+
originalError === undefined &&
|
|
830
|
+
run.ownerCancellation !== undefined &&
|
|
831
|
+
!errorsBeforePersistence.includes(run.ownerCancellation)
|
|
832
|
+
) {
|
|
833
|
+
errorsBeforePersistence.push(run.ownerCancellation);
|
|
834
|
+
}
|
|
835
|
+
const cancelled = run.ownerCancellation !== undefined && run.internalFailure === undefined;
|
|
836
|
+
const terminalError = errorsBeforePersistence.length > 0
|
|
837
|
+
? combineErrors(errorsBeforePersistence)
|
|
838
|
+
: undefined;
|
|
839
|
+
const persistenceResult = await Promise.allSettled([this.finalizeRun(
|
|
840
|
+
resolved.state,
|
|
841
|
+
run,
|
|
842
|
+
prompt,
|
|
843
|
+
modelResolution,
|
|
844
|
+
result,
|
|
845
|
+
serializedResultMessages,
|
|
846
|
+
terminalError,
|
|
847
|
+
cancelled,
|
|
848
|
+
)]).then(([settled]) => settled);
|
|
849
|
+
|
|
850
|
+
if (persistenceResult.status === 'rejected') {
|
|
851
|
+
const allErrors = [...errorsBeforePersistence, persistenceResult.reason];
|
|
852
|
+
const combinedError = combineErrors(allErrors);
|
|
853
|
+
const fallback = this.applyRunFinalState(
|
|
854
|
+
resolved.state,
|
|
855
|
+
run,
|
|
856
|
+
prompt,
|
|
857
|
+
modelResolution,
|
|
858
|
+
result,
|
|
859
|
+
serializedResultMessages,
|
|
860
|
+
combinedError,
|
|
861
|
+
cancelled,
|
|
862
|
+
);
|
|
863
|
+
resolved.state.activeRuns.delete(run.sessionId);
|
|
864
|
+
this.emitFinalMutationEvents(run, fallback);
|
|
865
|
+
this.emitFinalRunEvent(run, fallback.assistantMessage, combinedError, cancelled);
|
|
866
|
+
throw combinedError;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const finalized = persistenceResult.value;
|
|
870
|
+
resolved.state.activeRuns.delete(run.sessionId);
|
|
871
|
+
this.emitFinalRunEvent(run, finalized.assistantMessage, terminalError, cancelled);
|
|
872
|
+
if (terminalError !== undefined) throw terminalError;
|
|
873
|
+
return {
|
|
874
|
+
runId: run.runId,
|
|
875
|
+
sessionId: run.sessionId,
|
|
876
|
+
userMessage: finalized.userMessage,
|
|
877
|
+
assistantMessage: finalized.assistantMessage,
|
|
878
|
+
model: publicSnapshot(modelResolution!.identity),
|
|
879
|
+
usage: publicSnapshot(makeUsage(result!)),
|
|
880
|
+
finishReason: result!.finishReason,
|
|
881
|
+
steps: result!.steps,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private async reserveRun(
|
|
886
|
+
state: IStorageState,
|
|
887
|
+
run: IActiveRun,
|
|
888
|
+
prompt: INormalizedFlexPrompt,
|
|
889
|
+
): Promise<{ session: IFlexSession; userMessage: IFlexMessage; assistantMessage: IFlexMessage }> {
|
|
890
|
+
return this.mutateAndSave(state, () => {
|
|
891
|
+
const stored = this.requireSession(state, run.sessionId);
|
|
892
|
+
const timestamp = new Date().toISOString();
|
|
893
|
+
run.previousHistory = cloneSerializable(stored.modelHistory);
|
|
894
|
+
const userMessage: IFlexMessage = {
|
|
895
|
+
messageId: run.userMessageId,
|
|
896
|
+
sessionId: run.sessionId,
|
|
897
|
+
runId: run.runId,
|
|
898
|
+
role: 'user',
|
|
899
|
+
status: 'streaming',
|
|
900
|
+
createdAt: timestamp,
|
|
901
|
+
parts: prompt.parts.map((part) => {
|
|
902
|
+
if (part.type === 'text') {
|
|
903
|
+
return {
|
|
904
|
+
partId: plugins.crypto.randomUUID(),
|
|
905
|
+
type: 'text' as const,
|
|
906
|
+
text: part.text,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const attachment: IFlexAttachmentMessagePart = {
|
|
910
|
+
partId: plugins.crypto.randomUUID(),
|
|
911
|
+
type: 'attachment',
|
|
912
|
+
attachmentType: part.type,
|
|
913
|
+
...describeAttachment(part),
|
|
914
|
+
};
|
|
915
|
+
return attachment;
|
|
916
|
+
}),
|
|
917
|
+
};
|
|
918
|
+
const assistantMessage: IFlexMessage = {
|
|
919
|
+
messageId: run.assistantMessageId,
|
|
920
|
+
sessionId: run.sessionId,
|
|
921
|
+
runId: run.runId,
|
|
922
|
+
role: 'assistant',
|
|
923
|
+
status: 'streaming',
|
|
924
|
+
createdAt: timestamp,
|
|
925
|
+
parts: [],
|
|
926
|
+
};
|
|
927
|
+
stored.messages.push(userMessage, assistantMessage);
|
|
928
|
+
stored.session.status = 'running';
|
|
929
|
+
stored.session.activity = {
|
|
930
|
+
runId: run.runId,
|
|
931
|
+
status: 'running',
|
|
932
|
+
startedAt: timestamp,
|
|
933
|
+
};
|
|
934
|
+
stored.session.updatedAt = timestamp;
|
|
935
|
+
return {
|
|
936
|
+
session: publicSnapshot(stored.session),
|
|
937
|
+
userMessage: publicSnapshot(userMessage),
|
|
938
|
+
assistantMessage: publicSnapshot(assistantMessage),
|
|
939
|
+
};
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private async finalizeRun(
|
|
944
|
+
state: IStorageState,
|
|
945
|
+
run: IActiveRun,
|
|
946
|
+
prompt: INormalizedFlexPrompt,
|
|
947
|
+
model: IFlexResolvedModel | undefined,
|
|
948
|
+
result: TFlexAgentRunResult | undefined,
|
|
949
|
+
serializedResultMessages: TFlexAgentModelMessage[] | undefined,
|
|
950
|
+
originalError: unknown,
|
|
951
|
+
cancelled: boolean,
|
|
952
|
+
): Promise<IFinalMutationResult> {
|
|
953
|
+
const final = await this.mutateAndSave(state, () =>
|
|
954
|
+
this.applyRunFinalState(
|
|
955
|
+
state,
|
|
956
|
+
run,
|
|
957
|
+
prompt,
|
|
958
|
+
model,
|
|
959
|
+
result,
|
|
960
|
+
serializedResultMessages,
|
|
961
|
+
originalError,
|
|
962
|
+
cancelled,
|
|
963
|
+
),
|
|
964
|
+
);
|
|
965
|
+
this.emitFinalMutationEvents(run, final);
|
|
966
|
+
return final;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
private applyRunFinalState(
|
|
970
|
+
state: IStorageState,
|
|
971
|
+
run: IActiveRun,
|
|
972
|
+
prompt: INormalizedFlexPrompt,
|
|
973
|
+
model: IFlexResolvedModel | undefined,
|
|
974
|
+
result: TFlexAgentRunResult | undefined,
|
|
975
|
+
serializedResultMessages: TFlexAgentModelMessage[] | undefined,
|
|
976
|
+
originalError: unknown,
|
|
977
|
+
cancelled: boolean,
|
|
978
|
+
): IFinalMutationResult {
|
|
979
|
+
const stored = this.requireSession(state, run.sessionId);
|
|
980
|
+
const userMessage = this.requireMessage(stored, run.userMessageId);
|
|
981
|
+
const assistantMessage = this.requireMessage(stored, run.assistantMessageId);
|
|
982
|
+
assistantMessage.parts = cloneSerializable(run.callbackParts);
|
|
983
|
+
const timestamp = new Date().toISOString();
|
|
984
|
+
const terminalParts: TFlexMessagePart[] = [];
|
|
985
|
+
const success =
|
|
986
|
+
originalError === undefined &&
|
|
987
|
+
!cancelled &&
|
|
988
|
+
model !== undefined &&
|
|
989
|
+
result !== undefined &&
|
|
990
|
+
serializedResultMessages !== undefined;
|
|
991
|
+
|
|
992
|
+
for (const part of assistantMessage.parts) {
|
|
993
|
+
if (part.type === 'reasoning' && part.status === 'running') {
|
|
994
|
+
part.status = success ? 'completed' : 'cancelled';
|
|
995
|
+
terminalParts.push(publicSnapshot(part));
|
|
996
|
+
}
|
|
997
|
+
if (part.type === 'tool' && part.status === 'running') {
|
|
998
|
+
part.status = success ? 'failed' : cancelled ? 'cancelled' : 'failed';
|
|
999
|
+
part.error = success
|
|
1000
|
+
? 'Tool execution ended without a terminal event.'
|
|
1001
|
+
: cancelled
|
|
1002
|
+
? 'Tool execution was cancelled.'
|
|
1003
|
+
: errorMessage(originalError);
|
|
1004
|
+
terminalParts.push(publicSnapshot(part));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (success) {
|
|
1009
|
+
if (!assistantMessage.parts.some((part) => part.type === 'text')) {
|
|
1010
|
+
assistantMessage.parts.push({
|
|
1011
|
+
partId: plugins.crypto.randomUUID(),
|
|
1012
|
+
type: 'text',
|
|
1013
|
+
text: result.text,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
const usage = makeUsage(result);
|
|
1017
|
+
userMessage.status = 'completed';
|
|
1018
|
+
userMessage.completedAt = timestamp;
|
|
1019
|
+
assistantMessage.status = 'completed';
|
|
1020
|
+
assistantMessage.completedAt = timestamp;
|
|
1021
|
+
assistantMessage.model = cloneSerializable(model.identity);
|
|
1022
|
+
assistantMessage.usage = usage;
|
|
1023
|
+
stored.modelHistory = [
|
|
1024
|
+
...run.previousHistory,
|
|
1025
|
+
cloneSerializable(prompt.modelMessage),
|
|
1026
|
+
...serializedResultMessages,
|
|
1027
|
+
];
|
|
1028
|
+
stored.session.status = 'idle';
|
|
1029
|
+
stored.session.activity = {
|
|
1030
|
+
runId: run.runId,
|
|
1031
|
+
status: 'idle',
|
|
1032
|
+
startedAt: stored.session.activity.startedAt,
|
|
1033
|
+
completedAt: timestamp,
|
|
1034
|
+
};
|
|
1035
|
+
} else {
|
|
1036
|
+
const status = cancelled ? 'cancelled' : 'failed';
|
|
1037
|
+
const message = errorMessage(originalError);
|
|
1038
|
+
userMessage.status = status;
|
|
1039
|
+
userMessage.completedAt = timestamp;
|
|
1040
|
+
userMessage.error = message;
|
|
1041
|
+
assistantMessage.status = status;
|
|
1042
|
+
assistantMessage.completedAt = timestamp;
|
|
1043
|
+
assistantMessage.error = message;
|
|
1044
|
+
if (model) assistantMessage.model = cloneSerializable(model.identity);
|
|
1045
|
+
stored.session.status = status;
|
|
1046
|
+
stored.session.activity = {
|
|
1047
|
+
runId: run.runId,
|
|
1048
|
+
status,
|
|
1049
|
+
startedAt: stored.session.activity.startedAt,
|
|
1050
|
+
completedAt: timestamp,
|
|
1051
|
+
error: message,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
stored.session.updatedAt = timestamp;
|
|
1055
|
+
return {
|
|
1056
|
+
userMessage: publicSnapshot(userMessage),
|
|
1057
|
+
assistantMessage: publicSnapshot(assistantMessage),
|
|
1058
|
+
terminalParts,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private emitFinalMutationEvents(run: IActiveRun, final: IFinalMutationResult): void {
|
|
1063
|
+
for (const part of final.terminalParts) {
|
|
1064
|
+
this.emitPartEvent(run, 'part.completed', part);
|
|
1065
|
+
}
|
|
1066
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1067
|
+
type: 'message.updated',
|
|
1068
|
+
runId: run.runId,
|
|
1069
|
+
messageId: run.userMessageId,
|
|
1070
|
+
message: final.userMessage,
|
|
1071
|
+
});
|
|
1072
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1073
|
+
type: 'message.updated',
|
|
1074
|
+
runId: run.runId,
|
|
1075
|
+
messageId: run.assistantMessageId,
|
|
1076
|
+
message: final.assistantMessage,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
private emitFinalRunEvent(
|
|
1081
|
+
run: IActiveRun,
|
|
1082
|
+
message: IFlexMessage,
|
|
1083
|
+
error: unknown,
|
|
1084
|
+
cancelled: boolean,
|
|
1085
|
+
): void {
|
|
1086
|
+
const status: IFlexRunFinishedEvent['status'] = error
|
|
1087
|
+
? cancelled
|
|
1088
|
+
? 'cancelled'
|
|
1089
|
+
: 'failed'
|
|
1090
|
+
: 'completed';
|
|
1091
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1092
|
+
type: 'run.finished',
|
|
1093
|
+
runId: run.runId,
|
|
1094
|
+
messageId: run.assistantMessageId,
|
|
1095
|
+
status,
|
|
1096
|
+
message,
|
|
1097
|
+
...(error !== undefined ? { error: errorToInfo(error) } : {}),
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private onTextDelta(run: IActiveRun, delta: string): void {
|
|
1102
|
+
if (!delta || run.callbacksClosed || run.callbackError) return;
|
|
1103
|
+
const lastPart = run.callbackParts.at(-1);
|
|
1104
|
+
const created = lastPart?.type !== 'text';
|
|
1105
|
+
const byteLength = Buffer.byteLength(delta);
|
|
1106
|
+
if (!this.reserveCallbackCapacity(run, created ? 2 : 1, byteLength, created ? 1 : 0)) return;
|
|
1107
|
+
const part = lastPart?.type === 'text'
|
|
1108
|
+
? lastPart
|
|
1109
|
+
: { partId: plugins.crypto.randomUUID(), type: 'text' as const, text: '' };
|
|
1110
|
+
if (created) run.callbackParts.push(part);
|
|
1111
|
+
part.text += delta;
|
|
1112
|
+
if (created) this.emitPartEvent(run, 'part.started', part);
|
|
1113
|
+
this.emitPartEvent(run, 'part.delta', part, delta);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
private onReasoningStart(run: IActiveRun, id: string): void {
|
|
1117
|
+
if (run.callbacksClosed || run.callbackError || run.reasoningPartIds.has(id)) return;
|
|
1118
|
+
if (!this.reserveCallbackCapacity(run, 1, 0, 1)) return;
|
|
1119
|
+
const part = {
|
|
1120
|
+
partId: plugins.crypto.randomUUID(),
|
|
1121
|
+
type: 'reasoning' as const,
|
|
1122
|
+
text: '',
|
|
1123
|
+
status: 'running' as const,
|
|
1124
|
+
};
|
|
1125
|
+
run.reasoningPartIds.set(id, part.partId);
|
|
1126
|
+
run.callbackParts.push(part);
|
|
1127
|
+
this.emitPartEvent(run, 'part.started', part);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
private onReasoningDelta(run: IActiveRun, id: string, delta: string): void {
|
|
1131
|
+
if (run.callbacksClosed || run.callbackError || !delta) return;
|
|
1132
|
+
if (!run.reasoningPartIds.has(id)) this.onReasoningStart(run, id);
|
|
1133
|
+
const partId = run.reasoningPartIds.get(id);
|
|
1134
|
+
if (!partId || run.callbackError) return;
|
|
1135
|
+
const part = run.callbackParts.find(
|
|
1136
|
+
(entry) => entry.partId === partId && entry.type === 'reasoning',
|
|
1137
|
+
);
|
|
1138
|
+
if (!part || part.type !== 'reasoning' || part.status !== 'running') return;
|
|
1139
|
+
if (!this.reserveCallbackCapacity(run, 1, Buffer.byteLength(delta), 0)) return;
|
|
1140
|
+
part.text += delta;
|
|
1141
|
+
this.emitPartEvent(run, 'part.delta', part, delta);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
private onReasoningEnd(run: IActiveRun, id: string, text: string): void {
|
|
1145
|
+
if (run.callbacksClosed || run.callbackError) return;
|
|
1146
|
+
if (!run.reasoningPartIds.has(id)) this.onReasoningStart(run, id);
|
|
1147
|
+
const partId = run.reasoningPartIds.get(id);
|
|
1148
|
+
if (!partId || run.callbackError) return;
|
|
1149
|
+
const part = run.callbackParts.find(
|
|
1150
|
+
(entry) => entry.partId === partId && entry.type === 'reasoning',
|
|
1151
|
+
);
|
|
1152
|
+
if (!part || part.type !== 'reasoning' || part.status !== 'running') return;
|
|
1153
|
+
const missingText = part.text.length === 0 ? text : '';
|
|
1154
|
+
if (!this.reserveCallbackCapacity(run, 1, Buffer.byteLength(missingText), 0)) return;
|
|
1155
|
+
if (missingText) part.text = missingText;
|
|
1156
|
+
part.status = 'completed';
|
|
1157
|
+
this.emitPartEvent(run, 'part.completed', part);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private onToolStart(
|
|
1161
|
+
run: IActiveRun,
|
|
1162
|
+
event: { toolCallId: string; toolName: string; input: unknown },
|
|
1163
|
+
): void {
|
|
1164
|
+
if (run.callbacksClosed || run.callbackError || run.toolPartIds.has(event.toolCallId)) return;
|
|
1165
|
+
const input = normalizeJsonValue(event.input, this.toolOutputLimits);
|
|
1166
|
+
const byteLength = Buffer.byteLength(JSON.stringify(input));
|
|
1167
|
+
if (!this.reserveCallbackCapacity(run, 1, byteLength, 1)) return;
|
|
1168
|
+
const part: IFlexToolMessagePart = {
|
|
1169
|
+
partId: plugins.crypto.randomUUID(),
|
|
1170
|
+
type: 'tool',
|
|
1171
|
+
toolCallId: event.toolCallId,
|
|
1172
|
+
toolName: event.toolName,
|
|
1173
|
+
status: 'running',
|
|
1174
|
+
input,
|
|
1175
|
+
};
|
|
1176
|
+
run.toolPartIds.set(event.toolCallId, part.partId);
|
|
1177
|
+
run.callbackParts.push(part);
|
|
1178
|
+
this.emitPartEvent(run, 'part.started', part);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
private onToolFinish(run: IActiveRun, event: TFlexAgentToolCallFinishEvent): void {
|
|
1182
|
+
if (run.callbacksClosed || run.callbackError) return;
|
|
1183
|
+
if (!run.toolPartIds.has(event.toolCallId)) this.onToolStart(run, event);
|
|
1184
|
+
const partId = run.toolPartIds.get(event.toolCallId);
|
|
1185
|
+
if (!partId || run.callbackError) return;
|
|
1186
|
+
const part = run.callbackParts.find(
|
|
1187
|
+
(entry) => entry.partId === partId && entry.type === 'tool',
|
|
1188
|
+
);
|
|
1189
|
+
if (!part || part.type !== 'tool' || part.status !== 'running') return;
|
|
1190
|
+
const output = event.success
|
|
1191
|
+
? normalizeJsonValue(event.output, this.toolOutputLimits)
|
|
1192
|
+
: undefined;
|
|
1193
|
+
const byteLength = event.success
|
|
1194
|
+
? Buffer.byteLength(JSON.stringify(output))
|
|
1195
|
+
: Buffer.byteLength(event.error);
|
|
1196
|
+
if (!this.reserveCallbackCapacity(run, 1, byteLength, 0)) return;
|
|
1197
|
+
if (event.success) {
|
|
1198
|
+
part.status = 'completed';
|
|
1199
|
+
part.output = output!;
|
|
1200
|
+
} else {
|
|
1201
|
+
part.status = 'failed';
|
|
1202
|
+
part.error = event.error;
|
|
1203
|
+
}
|
|
1204
|
+
this.emitPartEvent(run, 'part.completed', part);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
private reserveCallbackCapacity(
|
|
1208
|
+
run: IActiveRun,
|
|
1209
|
+
eventCount: number,
|
|
1210
|
+
outputBytes: number,
|
|
1211
|
+
partCount: number,
|
|
1212
|
+
): boolean {
|
|
1213
|
+
const nextEvents = run.callbackEventCount + eventCount;
|
|
1214
|
+
const nextOutputBytes = run.callbackOutputBytes + outputBytes;
|
|
1215
|
+
const nextParts = run.callbackParts.length + partCount;
|
|
1216
|
+
if (
|
|
1217
|
+
nextEvents > this.callbackLimits.maxEvents ||
|
|
1218
|
+
nextOutputBytes > this.callbackLimits.maxOutputBytes ||
|
|
1219
|
+
nextParts > this.callbackLimits.maxParts
|
|
1220
|
+
) {
|
|
1221
|
+
const error = new FlexHarnessCallbackOverflowError(
|
|
1222
|
+
`Callback buffer exceeded its limit (${nextEvents} events, ${nextOutputBytes} bytes, ${nextParts} parts).`,
|
|
1223
|
+
);
|
|
1224
|
+
run.callbackError = error;
|
|
1225
|
+
this.abortRunInternally(run, error);
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
run.callbackEventCount = nextEvents;
|
|
1229
|
+
run.callbackOutputBytes = nextOutputBytes;
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
private async requestPermission(
|
|
1234
|
+
state: IStorageState,
|
|
1235
|
+
run: IActiveRun,
|
|
1236
|
+
input: IFlexPermissionRequestInput,
|
|
1237
|
+
): Promise<void> {
|
|
1238
|
+
validateIdentifier(input.kind, 'permission kind');
|
|
1239
|
+
validateIdentifier(input.description, 'permission description');
|
|
1240
|
+
if (input.rememberKey !== undefined) validateIdentifier(input.rememberKey, 'rememberKey');
|
|
1241
|
+
if (input.toolCallId !== undefined) validateIdentifier(input.toolCallId, 'toolCallId');
|
|
1242
|
+
if (run.controller.signal.aborted) {
|
|
1243
|
+
throw run.controller.signal.reason;
|
|
1244
|
+
}
|
|
1245
|
+
await state.saveTail;
|
|
1246
|
+
if (run.controller.signal.aborted) {
|
|
1247
|
+
throw run.controller.signal.reason;
|
|
1248
|
+
}
|
|
1249
|
+
if (
|
|
1250
|
+
state.activeRuns.get(run.sessionId) !== run ||
|
|
1251
|
+
run.callbacksClosed ||
|
|
1252
|
+
run.phase === 'closing' ||
|
|
1253
|
+
run.phase === 'committing'
|
|
1254
|
+
) {
|
|
1255
|
+
throw new FlexHarnessPermissionStateError(
|
|
1256
|
+
`Run "${run.runId}" no longer accepts permission requests.`,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
const stored = this.requireSession(state, run.sessionId);
|
|
1260
|
+
if (input.rememberKey && stored.rememberedPermissionKeys.has(input.rememberKey)) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const request: IFlexPermissionRequest = {
|
|
1265
|
+
permissionId: plugins.crypto.randomUUID(),
|
|
1266
|
+
scopeId: run.scopeId,
|
|
1267
|
+
sessionId: run.sessionId,
|
|
1268
|
+
runId: run.runId,
|
|
1269
|
+
kind: input.kind,
|
|
1270
|
+
description: input.description,
|
|
1271
|
+
...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
|
|
1272
|
+
...(input.rememberKey ? { rememberKey: input.rememberKey } : {}),
|
|
1273
|
+
...(input.metadata !== undefined ? { metadata: cloneSerializable(input.metadata) } : {}),
|
|
1274
|
+
createdAt: new Date().toISOString(),
|
|
1275
|
+
};
|
|
1276
|
+
let resolvePermission!: () => void;
|
|
1277
|
+
let rejectPermission!: (error: unknown) => void;
|
|
1278
|
+
const permissionPromise = new Promise<void>((resolve, reject) => {
|
|
1279
|
+
resolvePermission = resolve;
|
|
1280
|
+
rejectPermission = reject;
|
|
1281
|
+
});
|
|
1282
|
+
void permissionPromise.catch(() => undefined);
|
|
1283
|
+
const abortListener = () => {
|
|
1284
|
+
const pending = state.pendingPermissions.get(request.permissionId);
|
|
1285
|
+
if (pending) this.rejectPending(state, pending, run.controller.signal.reason);
|
|
1286
|
+
};
|
|
1287
|
+
const pending: IPendingPermission = {
|
|
1288
|
+
request,
|
|
1289
|
+
run,
|
|
1290
|
+
resolve: resolvePermission,
|
|
1291
|
+
reject: rejectPermission,
|
|
1292
|
+
settled: false,
|
|
1293
|
+
responding: false,
|
|
1294
|
+
responseTail: Promise.resolve(),
|
|
1295
|
+
abortListener,
|
|
1296
|
+
};
|
|
1297
|
+
state.pendingPermissions.set(request.permissionId, pending);
|
|
1298
|
+
run.pendingPermissionIds.add(request.permissionId);
|
|
1299
|
+
run.controller.signal.addEventListener('abort', abortListener, { once: true });
|
|
1300
|
+
try {
|
|
1301
|
+
await this.mutateAndSave(state, () => {
|
|
1302
|
+
const current = this.requireSession(state, run.sessionId);
|
|
1303
|
+
current.session.status = 'waiting_permission';
|
|
1304
|
+
current.session.activity.status = 'waiting_permission';
|
|
1305
|
+
current.session.updatedAt = new Date().toISOString();
|
|
1306
|
+
});
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
state.pendingPermissions.delete(request.permissionId);
|
|
1309
|
+
run.pendingPermissionIds.delete(request.permissionId);
|
|
1310
|
+
run.controller.signal.removeEventListener('abort', abortListener);
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
if (pending.settled) {
|
|
1314
|
+
return permissionPromise;
|
|
1315
|
+
}
|
|
1316
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1317
|
+
type: 'permission.requested',
|
|
1318
|
+
runId: run.runId,
|
|
1319
|
+
request: publicSnapshot(request),
|
|
1320
|
+
});
|
|
1321
|
+
return permissionPromise;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
private async applyPermissionResponse(
|
|
1325
|
+
state: IStorageState,
|
|
1326
|
+
pending: IPendingPermission,
|
|
1327
|
+
decision: TFlexPermissionDecision,
|
|
1328
|
+
): Promise<void> {
|
|
1329
|
+
if (pending.settled) {
|
|
1330
|
+
throw new FlexHarnessPermissionStateError(
|
|
1331
|
+
`Permission "${pending.request.permissionId}" has already been resolved.`,
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
if (!['once', 'always', 'reject'].includes(decision)) {
|
|
1335
|
+
throw new FlexHarnessPermissionStateError(`Unknown permission decision "${decision}".`);
|
|
1336
|
+
}
|
|
1337
|
+
if (decision === 'always' && !pending.request.rememberKey) {
|
|
1338
|
+
throw new FlexHarnessPermissionStateError(
|
|
1339
|
+
'The always decision requires the permission request to provide rememberKey.',
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const rememberKey = pending.request.rememberKey;
|
|
1344
|
+
let addedRememberKey = false;
|
|
1345
|
+
pending.responding = true;
|
|
1346
|
+
try {
|
|
1347
|
+
await this.mutateAndSave(state, () => {
|
|
1348
|
+
const stored = this.requireSession(state, pending.request.sessionId);
|
|
1349
|
+
if (decision === 'always' && rememberKey) {
|
|
1350
|
+
addedRememberKey = !stored.rememberedPermissionKeys.has(rememberKey);
|
|
1351
|
+
stored.rememberedPermissionKeys.add(rememberKey);
|
|
1352
|
+
}
|
|
1353
|
+
const hasOtherPending = [...state.pendingPermissions.values()].some(
|
|
1354
|
+
(entry) =>
|
|
1355
|
+
entry !== pending &&
|
|
1356
|
+
!entry.settled &&
|
|
1357
|
+
entry.request.sessionId === pending.request.sessionId,
|
|
1358
|
+
);
|
|
1359
|
+
stored.session.status = hasOtherPending ? 'waiting_permission' : 'running';
|
|
1360
|
+
stored.session.activity.status = hasOtherPending ? 'waiting_permission' : 'running';
|
|
1361
|
+
stored.session.updatedAt = new Date().toISOString();
|
|
1362
|
+
});
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
pending.responding = false;
|
|
1365
|
+
if (pending.abortReason !== undefined) {
|
|
1366
|
+
const abortReason = pending.abortReason;
|
|
1367
|
+
this.forceRejectPending(state, pending, abortReason);
|
|
1368
|
+
throw combineErrors([abortReason, error]);
|
|
1369
|
+
}
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (pending.abortReason !== undefined) {
|
|
1374
|
+
const abortReason = pending.abortReason;
|
|
1375
|
+
const rollbackPromise = addedRememberKey && rememberKey
|
|
1376
|
+
? this.mutateAndSave(state, () => {
|
|
1377
|
+
this.requireSession(
|
|
1378
|
+
state,
|
|
1379
|
+
pending.request.sessionId,
|
|
1380
|
+
).rememberedPermissionKeys.delete(rememberKey);
|
|
1381
|
+
})
|
|
1382
|
+
: Promise.resolve();
|
|
1383
|
+
pending.responding = false;
|
|
1384
|
+
this.forceRejectPending(state, pending, abortReason);
|
|
1385
|
+
try {
|
|
1386
|
+
await rollbackPromise;
|
|
1387
|
+
} catch (rollbackError) {
|
|
1388
|
+
throw combineErrors([abortReason, rollbackError]);
|
|
1389
|
+
}
|
|
1390
|
+
throw abortReason;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
pending.settled = true;
|
|
1394
|
+
pending.responding = false;
|
|
1395
|
+
state.pendingPermissions.delete(pending.request.permissionId);
|
|
1396
|
+
pending.run.pendingPermissionIds.delete(pending.request.permissionId);
|
|
1397
|
+
pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
|
|
1398
|
+
this.emitEvent(pending.request.scopeId, pending.request.sessionId, {
|
|
1399
|
+
type: 'permission.resolved',
|
|
1400
|
+
runId: pending.request.runId,
|
|
1401
|
+
request: publicSnapshot(pending.request),
|
|
1402
|
+
decision,
|
|
1403
|
+
});
|
|
1404
|
+
if (decision === 'reject') {
|
|
1405
|
+
pending.reject(new FlexHarnessPermissionRejectedError(pending.request.permissionId));
|
|
1406
|
+
} else {
|
|
1407
|
+
pending.resolve();
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
private rejectRunPermissions(state: IStorageState, run: IActiveRun, error: unknown): void {
|
|
1412
|
+
for (const permissionId of [...run.pendingPermissionIds]) {
|
|
1413
|
+
const pending = state.pendingPermissions.get(permissionId);
|
|
1414
|
+
if (pending) this.rejectPending(state, pending, error);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
private rejectPending(state: IStorageState, pending: IPendingPermission, error: unknown): void {
|
|
1419
|
+
if (pending.settled) return;
|
|
1420
|
+
if (pending.responding) {
|
|
1421
|
+
pending.abortReason ??= error;
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
this.forceRejectPending(state, pending, error);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private forceRejectPending(
|
|
1428
|
+
state: IStorageState,
|
|
1429
|
+
pending: IPendingPermission,
|
|
1430
|
+
error: unknown,
|
|
1431
|
+
): void {
|
|
1432
|
+
if (pending.settled) return;
|
|
1433
|
+
pending.settled = true;
|
|
1434
|
+
state.pendingPermissions.delete(pending.request.permissionId);
|
|
1435
|
+
pending.run.pendingPermissionIds.delete(pending.request.permissionId);
|
|
1436
|
+
pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
|
|
1437
|
+
pending.reject(error);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
private emitPartEvent(
|
|
1441
|
+
run: IActiveRun,
|
|
1442
|
+
type: IFlexPartChangedEvent['type'],
|
|
1443
|
+
part: TFlexMessagePart,
|
|
1444
|
+
delta?: string,
|
|
1445
|
+
): void {
|
|
1446
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
1447
|
+
type,
|
|
1448
|
+
runId: run.runId,
|
|
1449
|
+
messageId: run.assistantMessageId,
|
|
1450
|
+
partId: part.partId,
|
|
1451
|
+
part: publicSnapshot(part),
|
|
1452
|
+
...(delta !== undefined ? { delta } : {}),
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
private abortRunInternally(run: IActiveRun, error: unknown): void {
|
|
1457
|
+
run.internalFailure ??= error ?? new Error('Internal FlexHarness run failure.');
|
|
1458
|
+
if (!run.controller.signal.aborted) {
|
|
1459
|
+
run.controller.abort(run.internalFailure);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
private observeDetachedToolProvider(
|
|
1464
|
+
outcomePromise: Promise<IResolverOutcome<IFlexToolHandle | undefined>>,
|
|
1465
|
+
): void {
|
|
1466
|
+
const cleanup = outcomePromise.then(async (outcome) => {
|
|
1467
|
+
if (outcome.success && outcome.source === 'tools' && outcome.value?.close) {
|
|
1468
|
+
await Promise.resolve().then(() => outcome.value!.close!());
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
this.detachedToolCleanups.add(cleanup);
|
|
1472
|
+
void cleanup.then(
|
|
1473
|
+
() => this.detachedToolCleanups.delete(cleanup),
|
|
1474
|
+
() => undefined,
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
private emitEvent(scopeId: string, sessionId: string, details: TEventDetails): void {
|
|
1479
|
+
const event = deepFreeze(
|
|
1480
|
+
cloneSerializable({
|
|
1481
|
+
eventId: plugins.crypto.randomUUID(),
|
|
1482
|
+
sequence: ++this.sequence,
|
|
1483
|
+
timestamp: new Date().toISOString(),
|
|
1484
|
+
scopeId,
|
|
1485
|
+
sessionId,
|
|
1486
|
+
...details,
|
|
1487
|
+
}),
|
|
1488
|
+
) as TFlexHarnessEvent;
|
|
1489
|
+
for (const listener of this.listeners) {
|
|
1490
|
+
try {
|
|
1491
|
+
listener(event);
|
|
1492
|
+
} catch {
|
|
1493
|
+
// Listener failures are isolated from the run and from other listeners.
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
private async resolveState(
|
|
1499
|
+
scopeId: string,
|
|
1500
|
+
): Promise<{ scope: IFlexResolvedScope<TScope>; state: IStorageState }> {
|
|
1501
|
+
this.assertOpen();
|
|
1502
|
+
validateIdentifier(scopeId, 'scopeId');
|
|
1503
|
+
const scope = await this.scopeResolver.resolveScope(scopeId);
|
|
1504
|
+
this.assertOpen();
|
|
1505
|
+
validateIdentifier(scope.storageKey, 'resolved storageKey');
|
|
1506
|
+
let stateLoad = this.stateLoads.get(scope.storageKey);
|
|
1507
|
+
if (!stateLoad) {
|
|
1508
|
+
stateLoad = this.loadState(scope.storageKey);
|
|
1509
|
+
this.stateLoads.set(scope.storageKey, stateLoad);
|
|
1510
|
+
void stateLoad.catch(() => {
|
|
1511
|
+
if (this.stateLoads.get(scope.storageKey) === stateLoad) {
|
|
1512
|
+
this.stateLoads.delete(scope.storageKey);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
const state = await stateLoad;
|
|
1517
|
+
this.assertOpen();
|
|
1518
|
+
return { scope, state };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
private async loadState(storageKey: string): Promise<IStorageState> {
|
|
1522
|
+
const loaded = await this.store.load(storageKey);
|
|
1523
|
+
if (loaded) assertFlexHarnessSnapshot(loaded);
|
|
1524
|
+
const snapshot = loaded ? cloneSerializable(loaded) : undefined;
|
|
1525
|
+
const sessions = new Map<string, IStoredSessionState>();
|
|
1526
|
+
const timestamp = new Date().toISOString();
|
|
1527
|
+
for (const stored of snapshot?.sessions ?? []) {
|
|
1528
|
+
if (stored.session.status === 'running' || stored.session.status === 'waiting_permission') {
|
|
1529
|
+
stored.session.status = 'idle';
|
|
1530
|
+
stored.session.activity = { status: 'idle' };
|
|
1531
|
+
stored.session.updatedAt = timestamp;
|
|
1532
|
+
}
|
|
1533
|
+
for (const message of stored.messages) {
|
|
1534
|
+
if (message.status === 'streaming') {
|
|
1535
|
+
message.status = 'cancelled';
|
|
1536
|
+
message.completedAt = timestamp;
|
|
1537
|
+
message.error = 'The process stopped before this message completed.';
|
|
1538
|
+
}
|
|
1539
|
+
for (const part of message.parts) {
|
|
1540
|
+
if (part.type === 'reasoning' && part.status === 'running') {
|
|
1541
|
+
part.status = 'cancelled';
|
|
1542
|
+
}
|
|
1543
|
+
if (part.type === 'tool' && part.status === 'running') {
|
|
1544
|
+
part.status = 'cancelled';
|
|
1545
|
+
part.error = 'The process stopped before this tool call completed.';
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
sessions.set(stored.session.sessionId, {
|
|
1550
|
+
session: stored.session,
|
|
1551
|
+
messages: stored.messages,
|
|
1552
|
+
modelHistory: stored.modelHistory as unknown as TFlexAgentModelMessage[],
|
|
1553
|
+
rememberedPermissionKeys: new Set(stored.rememberedPermissionKeys),
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
return {
|
|
1557
|
+
storageKey,
|
|
1558
|
+
revision: snapshot?.revision ?? 0,
|
|
1559
|
+
sessions,
|
|
1560
|
+
activeRuns: new Map<string, IActiveRun>(),
|
|
1561
|
+
pendingPermissions: new Map<string, IPendingPermission>(),
|
|
1562
|
+
saveTail: Promise.resolve(),
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
private mutateAndSave<T>(state: IStorageState, mutation: () => T): Promise<T> {
|
|
1567
|
+
const operation = state.saveTail.then(async () => {
|
|
1568
|
+
const beforeMutation = this.createSnapshot(state, state.revision);
|
|
1569
|
+
try {
|
|
1570
|
+
const result = mutation();
|
|
1571
|
+
const expectedRevision = state.revision;
|
|
1572
|
+
const snapshot = this.createSnapshot(state, expectedRevision + 1);
|
|
1573
|
+
assertFlexHarnessSnapshot(snapshot);
|
|
1574
|
+
await this.store.save(state.storageKey, snapshot, expectedRevision);
|
|
1575
|
+
state.revision = snapshot.revision;
|
|
1576
|
+
return result;
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
this.restorePersistentState(state, beforeMutation);
|
|
1579
|
+
throw error;
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
state.saveTail = operation.then(
|
|
1583
|
+
() => undefined,
|
|
1584
|
+
() => undefined,
|
|
1585
|
+
);
|
|
1586
|
+
return operation;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private restorePersistentState(state: IStorageState, snapshot: IFlexHarnessSnapshot): void {
|
|
1590
|
+
state.revision = snapshot.revision;
|
|
1591
|
+
state.sessions.clear();
|
|
1592
|
+
for (const stored of snapshot.sessions) {
|
|
1593
|
+
state.sessions.set(stored.session.sessionId, {
|
|
1594
|
+
session: cloneSerializable(stored.session),
|
|
1595
|
+
messages: cloneSerializable(stored.messages),
|
|
1596
|
+
modelHistory: cloneSerializable(stored.modelHistory) as unknown as TFlexAgentModelMessage[],
|
|
1597
|
+
rememberedPermissionKeys: new Set(stored.rememberedPermissionKeys),
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
private createSnapshot(state: IStorageState, revision: number): IFlexHarnessSnapshot {
|
|
1603
|
+
const sessions: IFlexStoredSessionSnapshot[] = [...state.sessions.values()]
|
|
1604
|
+
.sort((left, right) => left.session.sessionId.localeCompare(right.session.sessionId))
|
|
1605
|
+
.map((stored) => ({
|
|
1606
|
+
session: cloneSerializable(stored.session),
|
|
1607
|
+
messages: cloneSerializable(stored.messages),
|
|
1608
|
+
modelHistory: cloneSerializable(stored.modelHistory) as unknown as IFlexStoredSessionSnapshot['modelHistory'],
|
|
1609
|
+
rememberedPermissionKeys: [...stored.rememberedPermissionKeys].sort(),
|
|
1610
|
+
}));
|
|
1611
|
+
return {
|
|
1612
|
+
schemaVersion: 1,
|
|
1613
|
+
revision,
|
|
1614
|
+
sessions,
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
private requireSession(state: IStorageState, sessionId: string): IStoredSessionState {
|
|
1619
|
+
validateIdentifier(sessionId, 'sessionId');
|
|
1620
|
+
const session = state.sessions.get(sessionId);
|
|
1621
|
+
if (!session) throw new FlexHarnessNotFoundError('Session', sessionId);
|
|
1622
|
+
return session;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
private requireMessage(stored: IStoredSessionState, messageId: string): IFlexMessage {
|
|
1626
|
+
const message = stored.messages.find((entry) => entry.messageId === messageId);
|
|
1627
|
+
if (!message) throw new FlexHarnessNotFoundError('Message', messageId);
|
|
1628
|
+
return message;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private requireMutableSession(state: IStorageState, sessionId: string): IStoredSessionState {
|
|
1632
|
+
const stored = this.requireSession(state, sessionId);
|
|
1633
|
+
const hasPendingPermission = [...state.pendingPermissions.values()].some(
|
|
1634
|
+
(pending) => !pending.settled && pending.request.sessionId === sessionId,
|
|
1635
|
+
);
|
|
1636
|
+
if (state.activeRuns.has(sessionId) || hasPendingPermission) {
|
|
1637
|
+
throw new FlexHarnessSessionBusyError(
|
|
1638
|
+
sessionId,
|
|
1639
|
+
'cannot be changed while it has an active run or pending permission',
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
return stored;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
private assertOpen(): void {
|
|
1646
|
+
if (this.closed) throw new FlexHarnessClosedError();
|
|
1647
|
+
}
|
|
1648
|
+
}
|