@modelprofile.com/flexharness 10.24.0 → 11.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 +2 -2
- package/dist_ts/classes.codesandbox.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/classes.stores.js +1 -1
- package/dist_ts/classes.toolcatalog.js +1 -1
- package/dist_ts/errors.js +1 -1
- package/dist_ts/functions.toolpermissions.js +1 -1
- package/dist_ts/interfaces.d.ts +3 -3
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts/utils.harnessoptions.js +1 -1
- package/dist_ts/utils.json.js +1 -1
- package/dist_ts/utils.schemadeclarations.js +1 -1
- package/dist_ts/utils.sessionimport.js +1 -1
- package/dist_ts/utils.sessionmetrics.d.ts +1 -1
- package/dist_ts_chat/classes.chatsession.js +1 -1
- package/dist_ts_chat_harness/classes.transcript.js +1 -1
- package/dist_ts_chat_harness/functions.intents.js +1 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
- package/dist_ts_chat_harness/functions.mapping.js +1 -1
- package/dist_ts_chat_harness_server/classes.chathub.d.ts +18 -7
- package/dist_ts_chat_harness_server/classes.chathub.js +65 -62
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +5 -3
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +4 -4
- package/dist_ts_chat_harness_server/classes.scopedirectory.d.ts +17 -0
- package/dist_ts_chat_harness_server/classes.scopedirectory.js +49 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +3 -5
- package/dist_ts_chat_harness_server/classes.sessionlists.js +11 -12
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +8 -2
- package/dist_ts_chat_harness_server/classes.turngate.js +22 -21
- package/dist_ts_chat_harness_server/functions.errorprojector.js +1 -1
- package/dist_ts_chat_harness_server/index.d.ts +3 -2
- package/dist_ts_chat_harness_server/index.js +3 -2
- package/dist_ts_chat_harness_server/interfaces.d.ts +35 -2
- package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +28 -3
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +71 -15
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +5 -3
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +79 -16
- package/dist_ts_chat_harness_typedsocket/index.d.ts +2 -2
- package/dist_ts_chat_harness_typedsocket/index.js +1 -1
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +20 -11
- package/dist_ts_chat_web/flexchat-input.js +1 -1
- package/dist_ts_chat_web/flexchat-message.js +1 -1
- package/dist_ts_chat_web/flexchat-window.js +1 -1
- package/dist_ts_compaction/smartagent.compaction.js +1 -1
- package/dist_ts_kvm/ocr.js +1 -1
- package/dist_ts_ocr/classes.imageocr.js +1 -1
- package/dist_ts_ocr/ocr.mistral.js +1 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +1 -1
- package/dist_ts_tools/tool.browser.js +1 -1
- package/dist_ts_tools/tool.filesystem.js +1 -1
- package/dist_ts_tools/tool.shell.js +1 -1
- package/dist_ts_tools_node/tool.context.js +1 -1
- package/dist_ts_typedrequest/catalog.d.ts +2 -2
- package/dist_ts_typedrequest/catalog.js +1 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/utils.flexharness.ts +121 -0
- package/ts_chat_harness_server/classes.chathub.ts +68 -61
- package/ts_chat_harness_server/classes.chatsubscription.ts +7 -5
- package/ts_chat_harness_server/classes.scopedirectory.ts +67 -0
- package/ts_chat_harness_server/classes.sessionlists.ts +11 -19
- package/ts_chat_harness_server/classes.turngate.ts +28 -21
- package/ts_chat_harness_server/index.ts +4 -5
- package/ts_chat_harness_server/interfaces.ts +35 -2
- package/ts_chat_harness_server/plugins.ts +0 -3
- package/ts_chat_harness_server/readme.md +6 -4
- package/ts_chat_harness_typedsocket/classes.clienttransport.ts +92 -23
- package/ts_chat_harness_typedsocket/classes.serverbinding.ts +88 -16
- package/ts_chat_harness_typedsocket/index.ts +4 -2
- package/ts_chat_harness_typedsocket/interfaces.requests.ts +15 -11
- package/ts_chat_harness_typedsocket/readme.md +7 -3
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { FlexHarnessValidationError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFlexPermissionRequestInput,
|
|
4
|
+
IFlexResourceToolProviderDescriptor,
|
|
5
|
+
IFlexToolHandle,
|
|
6
|
+
IFlexToolProviderContext,
|
|
7
|
+
TFlexAgentToolSet,
|
|
8
|
+
} from './interfaces.js';
|
|
9
|
+
import type {
|
|
10
|
+
IDetachedCleanup,
|
|
11
|
+
IStorageState,
|
|
12
|
+
IActiveRun,
|
|
13
|
+
IResolverOutcome,
|
|
14
|
+
} from './interfaces.flexharness.js';
|
|
15
|
+
import {
|
|
16
|
+
requireSessionGeneration,
|
|
17
|
+
createFlexResourceIdentityDigest,
|
|
18
|
+
createFlexResourceToolNamespace,
|
|
19
|
+
createFlexResourceToolName,
|
|
20
|
+
combineErrors,
|
|
21
|
+
} from './utils.flexharness.js';
|
|
22
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
23
|
+
|
|
24
|
+
const maxResourceToolProviders = 128;
|
|
25
|
+
|
|
26
|
+
/** Tool provider plumbing: run tool handles, resource tool providers and detached cleanups. */
|
|
27
|
+
export class FlexHarnessToolProviders<TScope> {
|
|
28
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
29
|
+
|
|
30
|
+
public normalizeToolHandle(run: IActiveRun, handle: IFlexToolHandle | undefined): IFlexToolHandle | undefined {
|
|
31
|
+
if (handle === undefined) return undefined;
|
|
32
|
+
let close: IFlexToolHandle['close'];
|
|
33
|
+
try {
|
|
34
|
+
close = handle.close;
|
|
35
|
+
if (close !== undefined && typeof close !== 'function') {
|
|
36
|
+
throw new FlexHarnessValidationError('Tool handle close must be a function.');
|
|
37
|
+
}
|
|
38
|
+
const tools = handle.tools;
|
|
39
|
+
this.core.runs.assertNoBuiltInToolCollisions(run, tools);
|
|
40
|
+
const catalog = handle.catalog;
|
|
41
|
+
if (catalog !== undefined && (typeof catalog !== 'object' || catalog === null)) {
|
|
42
|
+
throw new FlexHarnessValidationError('Tool handle catalog must be an object.');
|
|
43
|
+
}
|
|
44
|
+
const isReadOnlyToolCall = handle.isReadOnlyToolCall;
|
|
45
|
+
if (isReadOnlyToolCall !== undefined && typeof isReadOnlyToolCall !== 'function') {
|
|
46
|
+
throw new FlexHarnessValidationError('Tool handle isReadOnlyToolCall must be a function.');
|
|
47
|
+
}
|
|
48
|
+
const closeHandle = close;
|
|
49
|
+
return {
|
|
50
|
+
tools,
|
|
51
|
+
...(catalog === undefined ? {} : { catalog }),
|
|
52
|
+
...(isReadOnlyToolCall === undefined ? {} : {
|
|
53
|
+
isReadOnlyToolCall: (call: { toolName: string; input: unknown }) => isReadOnlyToolCall.call(handle, call),
|
|
54
|
+
}),
|
|
55
|
+
...(closeHandle ? { close: () => closeHandle.call(handle) } : {}),
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (close) this.trackDetachedCleanup(run.state, run, () => close!.call(handle));
|
|
59
|
+
throw this.core.projectExternalError(run, error, 'toolProvider');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async provideRunToolHandle(
|
|
64
|
+
run: IActiveRun,
|
|
65
|
+
context: Readonly<IFlexToolProviderContext<TScope>>,
|
|
66
|
+
): Promise<IFlexToolHandle | undefined> {
|
|
67
|
+
if (!this.core.resourceToolProviderResolver) return this.core.toolProvider?.provideTools(context);
|
|
68
|
+
|
|
69
|
+
const resolverContext = Object.freeze({
|
|
70
|
+
scopeId: context.scopeId,
|
|
71
|
+
scope: context.scope,
|
|
72
|
+
sessionId: context.sessionId,
|
|
73
|
+
...requireSessionGeneration(run.stored.session),
|
|
74
|
+
runId: context.runId,
|
|
75
|
+
...(context.parentSessionId === undefined ? {} : { parentSessionId: context.parentSessionId }),
|
|
76
|
+
...(context.agent === undefined ? {} : { agent: context.agent }),
|
|
77
|
+
signal: context.signal,
|
|
78
|
+
});
|
|
79
|
+
const resolved = await this.core.resourceToolProviderResolver.resolveResourceToolProviders(resolverContext);
|
|
80
|
+
const descriptors = this.normalizeResourceToolProviderDescriptors(resolved);
|
|
81
|
+
const owned: Array<{ handle: IFlexToolHandle; closed: boolean }> = [];
|
|
82
|
+
const tools: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
83
|
+
/** The handle and own name of each exposed resource tool, for the handle's classifier. */
|
|
84
|
+
const resourceTools = new Map<string, { handle: IFlexToolHandle; toolName: string }>();
|
|
85
|
+
const closeOwned = async (): Promise<void> => {
|
|
86
|
+
const errors: unknown[] = [];
|
|
87
|
+
for (const entry of [...owned].reverse()) {
|
|
88
|
+
if (entry.closed || !entry.handle.close) continue;
|
|
89
|
+
try {
|
|
90
|
+
await entry.handle.close();
|
|
91
|
+
entry.closed = true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
errors.push(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (errors.length > 0) throw combineErrors(errors);
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
context.signal.throwIfAborted();
|
|
100
|
+
const applicationHandle = this.normalizeToolHandle(
|
|
101
|
+
run,
|
|
102
|
+
await this.core.toolProvider?.provideTools(context),
|
|
103
|
+
);
|
|
104
|
+
if (applicationHandle) {
|
|
105
|
+
owned.push({ handle: applicationHandle, closed: false });
|
|
106
|
+
for (const [toolName, tool] of Object.entries(applicationHandle.tools)) {
|
|
107
|
+
tools[toolName] = tool;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const descriptor of descriptors) {
|
|
111
|
+
context.signal.throwIfAborted();
|
|
112
|
+
const namespace = createFlexResourceToolNamespace(
|
|
113
|
+
descriptor.resourceId,
|
|
114
|
+
descriptor.attachmentRevision,
|
|
115
|
+
);
|
|
116
|
+
const resourceIdentity = createFlexResourceIdentityDigest(
|
|
117
|
+
descriptor.resourceId,
|
|
118
|
+
descriptor.attachmentRevision,
|
|
119
|
+
);
|
|
120
|
+
const resourceContext = Object.freeze({
|
|
121
|
+
...context,
|
|
122
|
+
requestPermission: (request: IFlexPermissionRequestInput) => context.requestPermission({
|
|
123
|
+
...request,
|
|
124
|
+
kind: `resource.${resourceIdentity}.${request.kind}`,
|
|
125
|
+
...(request.rememberKey === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { rememberKey: `resource:${resourceIdentity}:${request.rememberKey}` }),
|
|
128
|
+
metadata: {
|
|
129
|
+
resourceId: descriptor.resourceId,
|
|
130
|
+
attachmentRevision: descriptor.attachmentRevision,
|
|
131
|
+
resourceIdentity,
|
|
132
|
+
toolNamespace: namespace,
|
|
133
|
+
...(request.metadata === undefined ? {} : { providerMetadata: request.metadata }),
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
const handle = this.normalizeToolHandle(
|
|
138
|
+
run,
|
|
139
|
+
await descriptor.provider.provideTools(resourceContext),
|
|
140
|
+
);
|
|
141
|
+
if (!handle) continue;
|
|
142
|
+
owned.push({ handle, closed: false });
|
|
143
|
+
if (handle.catalog !== undefined) {
|
|
144
|
+
throw new FlexHarnessValidationError('Resource tool providers cannot supply a tool catalog.');
|
|
145
|
+
}
|
|
146
|
+
for (const [toolName, tool] of Object.entries(handle.tools)) {
|
|
147
|
+
const exposedName = createFlexResourceToolName(namespace, toolName);
|
|
148
|
+
if (Object.prototype.hasOwnProperty.call(tools, exposedName)) {
|
|
149
|
+
throw new FlexHarnessValidationError(`Duplicate exposed tool "${exposedName}".`);
|
|
150
|
+
}
|
|
151
|
+
tools[exposedName] = tool;
|
|
152
|
+
resourceTools.set(exposedName, { handle, toolName });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
|
|
156
|
+
const classifies = applicationHandle?.isReadOnlyToolCall !== undefined
|
|
157
|
+
|| [...resourceTools.values()].some((entry) => entry.handle.isReadOnlyToolCall !== undefined);
|
|
158
|
+
return {
|
|
159
|
+
tools: tools as TFlexAgentToolSet,
|
|
160
|
+
...(applicationHandle?.catalog === undefined ? {} : { catalog: applicationHandle.catalog }),
|
|
161
|
+
...(classifies ? {
|
|
162
|
+
isReadOnlyToolCall: ({ toolName, input }: { toolName: string; input: unknown }) => {
|
|
163
|
+
const resource = resourceTools.get(toolName);
|
|
164
|
+
return resource
|
|
165
|
+
? resource.handle.isReadOnlyToolCall?.({ toolName: resource.toolName, input }) === true
|
|
166
|
+
: applicationHandle?.isReadOnlyToolCall?.({ toolName, input }) === true;
|
|
167
|
+
},
|
|
168
|
+
} : {}),
|
|
169
|
+
...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (owned.some((entry) => entry.handle.close && !entry.closed)) {
|
|
173
|
+
try {
|
|
174
|
+
await this.closePartialToolHandle({ tools: {}, close: closeOwned }, run);
|
|
175
|
+
} catch (cleanupError) {
|
|
176
|
+
throw combineErrors([error, cleanupError]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private normalizeResourceToolProviderDescriptors(
|
|
184
|
+
descriptors: readonly IFlexResourceToolProviderDescriptor<TScope>[],
|
|
185
|
+
): IFlexResourceToolProviderDescriptor<TScope>[] {
|
|
186
|
+
if (!Array.isArray(descriptors) || descriptors.length > maxResourceToolProviders) {
|
|
187
|
+
throw new FlexHarnessValidationError(
|
|
188
|
+
`Resource tool providers must be an array with at most ${maxResourceToolProviders} descriptors.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const resourceIds = new Set<string>();
|
|
192
|
+
const namespaces = new Set<string>();
|
|
193
|
+
return descriptors.map((descriptor, index) => {
|
|
194
|
+
if (
|
|
195
|
+
!descriptor
|
|
196
|
+
|| typeof descriptor !== 'object'
|
|
197
|
+
|| Array.isArray(descriptor)
|
|
198
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(descriptor))
|
|
199
|
+
) {
|
|
200
|
+
throw new FlexHarnessValidationError(`Resource tool provider descriptor ${index} is invalid.`);
|
|
201
|
+
}
|
|
202
|
+
const unsupported = Object.keys(descriptor).find(
|
|
203
|
+
(key) => !['resourceId', 'attachmentRevision', 'provider'].includes(key),
|
|
204
|
+
);
|
|
205
|
+
if (unsupported) {
|
|
206
|
+
throw new FlexHarnessValidationError(
|
|
207
|
+
`Resource tool provider descriptor ${index} does not support "${unsupported}".`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const resourceId = descriptor.resourceId;
|
|
211
|
+
const attachmentRevision = descriptor.attachmentRevision;
|
|
212
|
+
const provider = descriptor.provider;
|
|
213
|
+
const provideTools = provider?.provideTools;
|
|
214
|
+
const namespace = createFlexResourceToolNamespace(resourceId, attachmentRevision);
|
|
215
|
+
if (resourceIds.has(resourceId)) {
|
|
216
|
+
throw new FlexHarnessValidationError(`Duplicate resourceId "${resourceId}".`);
|
|
217
|
+
}
|
|
218
|
+
if (namespaces.has(namespace)) {
|
|
219
|
+
throw new FlexHarnessValidationError(`Duplicate resource tool namespace "${namespace}".`);
|
|
220
|
+
}
|
|
221
|
+
if (!provider || typeof provideTools !== 'function') {
|
|
222
|
+
throw new FlexHarnessValidationError(`Resource tool provider descriptor ${index} has no tool provider.`);
|
|
223
|
+
}
|
|
224
|
+
resourceIds.add(resourceId);
|
|
225
|
+
namespaces.add(namespace);
|
|
226
|
+
return {
|
|
227
|
+
resourceId,
|
|
228
|
+
attachmentRevision,
|
|
229
|
+
provider: {
|
|
230
|
+
provideTools: (context) => provideTools.call(provider, context),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public async closePartialToolHandle(handle: IFlexToolHandle, run: IActiveRun): Promise<void> {
|
|
237
|
+
try {
|
|
238
|
+
await handle.close?.();
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const projected = this.core.projectExternalError(run, error, 'toolCleanup');
|
|
241
|
+
this.retainDetachedCleanup(run.state, run, () => handle.close?.(), projected);
|
|
242
|
+
throw projected;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private retainDetachedCleanup(
|
|
247
|
+
state: IStorageState,
|
|
248
|
+
run: IActiveRun,
|
|
249
|
+
cleanup: () => Promise<void> | void,
|
|
250
|
+
error: Error,
|
|
251
|
+
): void {
|
|
252
|
+
state.detachedCleanups.add({
|
|
253
|
+
cleanup,
|
|
254
|
+
projectError: (cleanupError) => this.core.projectExternalError(run, cleanupError, 'toolCleanup'),
|
|
255
|
+
error,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public observeDetachedToolProvider(
|
|
260
|
+
outcome: Promise<IResolverOutcome<IFlexToolHandle | undefined>>,
|
|
261
|
+
run: IActiveRun,
|
|
262
|
+
): void {
|
|
263
|
+
this.trackDetachedCleanup(run.state, run, async () => {
|
|
264
|
+
const result = await outcome;
|
|
265
|
+
if (!result.success || result.source !== 'tools' || !result.value) return;
|
|
266
|
+
const close = result.value.close;
|
|
267
|
+
if (close !== undefined && typeof close !== 'function') {
|
|
268
|
+
throw new FlexHarnessValidationError('Tool handle close must be a function.');
|
|
269
|
+
}
|
|
270
|
+
await close?.call(result.value);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private trackDetachedCleanup(
|
|
275
|
+
state: IStorageState,
|
|
276
|
+
run: IActiveRun,
|
|
277
|
+
cleanup: () => Promise<void> | void,
|
|
278
|
+
): void {
|
|
279
|
+
const retained: IDetachedCleanup = {
|
|
280
|
+
cleanup,
|
|
281
|
+
projectError: (error) => this.core.projectExternalError(run, error, 'toolCleanup'),
|
|
282
|
+
};
|
|
283
|
+
state.detachedCleanups.add(retained);
|
|
284
|
+
void this.runDetachedCleanup(state, retained);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private runDetachedCleanup(state: IStorageState, retained: IDetachedCleanup): Promise<void> {
|
|
288
|
+
if (retained.completion) return retained.completion;
|
|
289
|
+
retained.error = undefined;
|
|
290
|
+
let completion!: Promise<void>;
|
|
291
|
+
completion = Promise.resolve()
|
|
292
|
+
.then(retained.cleanup)
|
|
293
|
+
.then(() => {
|
|
294
|
+
state.detachedCleanups.delete(retained);
|
|
295
|
+
}, (error: unknown) => {
|
|
296
|
+
retained.error = retained.projectError(error);
|
|
297
|
+
})
|
|
298
|
+
.finally(() => {
|
|
299
|
+
if (retained.completion === completion) retained.completion = undefined;
|
|
300
|
+
});
|
|
301
|
+
retained.completion = completion;
|
|
302
|
+
return completion;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public beginDetachedCleanupSettlement(state: IStorageState): Promise<void>[] {
|
|
306
|
+
return [...state.detachedCleanups].map((cleanup) =>
|
|
307
|
+
cleanup.completion ?? this.runDetachedCleanup(state, cleanup));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public async settleDetachedCleanups(
|
|
311
|
+
state: IStorageState,
|
|
312
|
+
attempts: readonly Promise<void>[],
|
|
313
|
+
): Promise<Error[]> {
|
|
314
|
+
await Promise.all(attempts);
|
|
315
|
+
await Promise.all([...state.detachedCleanups]
|
|
316
|
+
.map((cleanup) => cleanup.completion)
|
|
317
|
+
.filter((completion): completion is Promise<void> => completion !== undefined));
|
|
318
|
+
return [...state.detachedCleanups]
|
|
319
|
+
.map((cleanup) => cleanup.error)
|
|
320
|
+
.filter((error): error is Error => error !== undefined);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FLEX_RESULT_LIMITS,
|
|
4
|
+
FLEX_RESULT_METHOD,
|
|
5
|
+
type IFlexResultHandle,
|
|
6
|
+
type TJsonValue,
|
|
7
|
+
} from './interfaces.js';
|
|
8
|
+
import { describeShape, resultPage, selectPath, summarizeResult } from './utils.results.js';
|
|
9
|
+
import { normalizeJsonValue } from './utils.json.js';
|
|
10
|
+
import { jsonBytes } from './utils.transfer.js';
|
|
11
|
+
import type { IActiveRun, IResultRead } from './interfaces.flexharness.js';
|
|
12
|
+
import { queueOperation } from './utils.flexharness.js';
|
|
13
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
14
|
+
|
|
15
|
+
/** Budgeted tool output: oversized results stored behind result handles and read back by page. */
|
|
16
|
+
export class FlexHarnessResults<TScope> {
|
|
17
|
+
/** A session's result writes, one at a time: they share the session's index of results. */
|
|
18
|
+
public readonly resultQueues = new Map<string, Promise<void>>();
|
|
19
|
+
|
|
20
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The output the model gets: the output itself within the output budget, or, in a run with a
|
|
24
|
+
* catalog whose stores keep results, a handle to the stored output with a summary of it.
|
|
25
|
+
*/
|
|
26
|
+
public async budgetOutput(run: IActiveRun, output: unknown): Promise<TJsonValue> {
|
|
27
|
+
const results = this.core.stores.results;
|
|
28
|
+
const maxBytes = this.core.toolOutputLimits.maxBytes;
|
|
29
|
+
if (!results || !run.toolCatalog) return normalizeJsonValue(output, this.core.toolOutputLimits);
|
|
30
|
+
const stored = normalizeJsonValue(output, {
|
|
31
|
+
...this.core.toolOutputLimits,
|
|
32
|
+
maxBytes: Math.max(maxBytes, FLEX_RESULT_LIMITS.maxStoredBytes),
|
|
33
|
+
});
|
|
34
|
+
const bytes = jsonBytes(stored);
|
|
35
|
+
if (bytes <= maxBytes) return stored;
|
|
36
|
+
const handle = `res_${plugins.crypto.randomBytes(8).toString('hex')}`;
|
|
37
|
+
const read = `Too large to return whole: read it page by page with execute({ method: "${FLEX_RESULT_METHOD}", `
|
|
38
|
+
+ `params: { handle: "${handle}", page: 1 } })${run.toolCatalog.codeMode
|
|
39
|
+
? `, or in run_code with await api.results.get("${handle}", { page: 1 })`
|
|
40
|
+
: ''}; a dotted \`path\` reads a part of it.`;
|
|
41
|
+
const result: IFlexResultHandle = {
|
|
42
|
+
$flexType: 'result',
|
|
43
|
+
handle,
|
|
44
|
+
bytes,
|
|
45
|
+
pages: resultPage(stored, '', 1, this.resultPageBytes())!.pages,
|
|
46
|
+
summary: summarizeResult(stored, Math.min(FLEX_RESULT_LIMITS.summaryBytes, Math.floor(maxBytes / 4))),
|
|
47
|
+
read,
|
|
48
|
+
};
|
|
49
|
+
// A budget too small for even the handle keeps the truncated output.
|
|
50
|
+
if (jsonBytes(result) > maxBytes) return normalizeJsonValue(output, this.core.toolOutputLimits);
|
|
51
|
+
const { storageKey } = run.state;
|
|
52
|
+
await queueOperation(this.resultQueues, this.resultQueueKey(storageKey, run.sessionId), async () => {
|
|
53
|
+
await results.save(storageKey, run.sessionId, handle, stored);
|
|
54
|
+
// The session keeps its latest results; older ones are deleted.
|
|
55
|
+
const handles = await results.list(storageKey, run.sessionId);
|
|
56
|
+
for (const old of handles.slice(0, Math.max(0, handles.length - FLEX_RESULT_LIMITS.maxResultsPerSession))) {
|
|
57
|
+
await results.delete(storageKey, run.sessionId, old);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return result as unknown as TJsonValue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public resultQueueKey(storageKey: string, sessionId: string): string {
|
|
64
|
+
return JSON.stringify([storageKey, sessionId]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The JSON bytes of one `results.get` page: the output budget, less room for the page's fields. */
|
|
68
|
+
private resultPageBytes(): number {
|
|
69
|
+
return Math.max(64, this.core.toolOutputLimits.maxBytes - 512);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One page of a stored result of the run's session, or the error `results.get` reports. */
|
|
73
|
+
public async readResult(
|
|
74
|
+
run: IActiveRun,
|
|
75
|
+
params: IResultRead,
|
|
76
|
+
): Promise<{ ok: true; value: TJsonValue } | { ok: false; error: TJsonValue }> {
|
|
77
|
+
const method = FLEX_RESULT_METHOD;
|
|
78
|
+
const stored = await this.core.stores.results!.load(run.state.storageKey, run.sessionId, params.handle);
|
|
79
|
+
if (stored === undefined) {
|
|
80
|
+
return { ok: false, error: { error: 'unknown_handle', method, handle: params.handle, hint: `A handle reads one of the latest ${FLEX_RESULT_LIMITS.maxResultsPerSession} results this session stored.` } };
|
|
81
|
+
}
|
|
82
|
+
const path = params.path ?? '';
|
|
83
|
+
const value = selectPath(stored, path);
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
return { ok: false, error: { error: 'unknown_path', method, path, hint: `The result is ${describeShape(stored)}.` } };
|
|
86
|
+
}
|
|
87
|
+
const page = params.page ?? 1;
|
|
88
|
+
const content = resultPage(value, path, page, this.resultPageBytes());
|
|
89
|
+
if (!content) {
|
|
90
|
+
const pages = resultPage(value, path, 1, this.resultPageBytes())!.pages;
|
|
91
|
+
return { ok: false, error: { error: 'unknown_page', method, page, pages } };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
value: { handle: params.handle, path, page, pages: content.pages, ...content.range, value: content.value },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|