@modelprofile.com/flexharness 3.5.0 → 3.6.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/changelog.md +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +5 -0
- package/dist_ts/classes.flexharness.js +170 -4
- package/dist_ts/interfaces.d.ts +18 -0
- package/package.json +1 -1
- package/readme.hints.md +2 -0
- package/readme.md +35 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +208 -9
- package/ts/interfaces.ts +24 -0
package/dist_ts/interfaces.d.ts
CHANGED
|
@@ -234,6 +234,23 @@ export interface IFlexToolHandle {
|
|
|
234
234
|
export interface IFlexToolProvider<TScope> {
|
|
235
235
|
provideTools(context: IFlexToolProviderContext<TScope>): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
|
|
236
236
|
}
|
|
237
|
+
export interface IFlexResourceToolProviderResolverContext<TScope> {
|
|
238
|
+
scopeId: string;
|
|
239
|
+
scope: TScope;
|
|
240
|
+
sessionId: string;
|
|
241
|
+
runId: string;
|
|
242
|
+
readonly parentSessionId?: string;
|
|
243
|
+
readonly agent?: string;
|
|
244
|
+
signal: AbortSignal;
|
|
245
|
+
}
|
|
246
|
+
export interface IFlexResourceToolProviderDescriptor<TScope> {
|
|
247
|
+
resourceId: string;
|
|
248
|
+
attachmentRevision: number;
|
|
249
|
+
provider: IFlexToolProvider<TScope>;
|
|
250
|
+
}
|
|
251
|
+
export interface IFlexResourceToolProviderResolver<TScope> {
|
|
252
|
+
resolveResourceToolProviders(context: IFlexResourceToolProviderResolverContext<TScope>): Promise<readonly IFlexResourceToolProviderDescriptor<TScope>[]> | readonly IFlexResourceToolProviderDescriptor<TScope>[];
|
|
253
|
+
}
|
|
237
254
|
export interface IFlexSessionTombstone {
|
|
238
255
|
sessionId: string;
|
|
239
256
|
deletedAt: string;
|
|
@@ -367,6 +384,7 @@ export interface IFlexHarnessOptions<TScope> {
|
|
|
367
384
|
scopeResolver: IFlexScopeResolver<TScope>;
|
|
368
385
|
modelResolver: IFlexModelResolver<TScope>;
|
|
369
386
|
toolProvider?: IFlexToolProvider<TScope>;
|
|
387
|
+
resourceToolProviderResolver?: IFlexResourceToolProviderResolver<TScope>;
|
|
370
388
|
executionContextProvider?: IFlexExecutionContextProvider<TScope>;
|
|
371
389
|
stores?: IFlexHarnessStores;
|
|
372
390
|
agentSessionPolicy?: IFlexAgentSessionPolicy<TScope>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelprofile.com/flexharness",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
package/readme.hints.md
CHANGED
|
@@ -8,6 +8,7 @@ Implementation findings for flexharness.
|
|
|
8
8
|
- Public prompts remain JSON-safe. URL strings are converted to `URL` instances only at the private SmartAgent invocation boundary.
|
|
9
9
|
- Prompt attachment payloads exist only in canonical private Agent events. Public messages, prompt results, projection snapshots, and events expose only `attachmentType`, source kind, optional media/name, and decoded size when determinable.
|
|
10
10
|
- Resolver calls start in promise continuations so synchronous throws are observed. The first failure aborts the shared internal signal without awaiting an ignoring sibling; detached tool-provider settlement is observed and late handles are closed.
|
|
11
|
+
- Resource tool composition resolves descriptors per run before provider acquisition, validates duplicate resources and derived namespaces as one set, preserves legacy tool names, and derives bounded resource names from canonical resource/revision and original-tool hashes. Permission identity uses the complete resource digest while provider metadata remains nested below harness-owned attachment metadata.
|
|
11
12
|
|
|
12
13
|
## Persistence boundary
|
|
13
14
|
|
|
@@ -21,6 +22,7 @@ Implementation findings for flexharness.
|
|
|
21
22
|
- Canonical finalization is the cancellation linearization point. Abort returns false once a run starts committing, while the active-run entry continues to block new prompts until finalization and public promotion settle.
|
|
22
23
|
- Permission callbacks wait for the current save tail and then revalidate their run before reading remembered grants, so neither uncommitted grants nor captured callbacks can authorize later work.
|
|
23
24
|
- Detached tool-provider settlement remains tracked after prompt finalization. Failed late handle closure stays owned by its storage state and is retried by a later retirement or disposal call.
|
|
25
|
+
- A composed tool handle closes resource and application handles in reverse acquisition order. It attempts every close, aggregates failures, marks only successful handles complete, and retains the composite closure so later lifecycle drains retry exactly the failed handles.
|
|
24
26
|
- Scope retirement fences older resolver calls and late state admissions, drains the complete resolved storage namespace, and evicts only the exact cached state promise. It never deletes the durable snapshot. Applications must close admission across every resolver alias before retirement because aliases are unknowable before resolution.
|
|
25
27
|
- Context compaction runs under immutable async invocation context rather than storage-state scope hints. This keeps `scopeId`, `scope`, `storageKey`, and `sessionId` exact for manual, retention, and context-overflow compaction even when aliases share one cached namespace. Autonomous job callbacks use their immutable session-owner context because no later public invocation owns them.
|
|
26
28
|
- Detached tool-provider cleanup and retained session cleanup are owned by the exact loaded storage state. Failed drains retain that ownership and the cached state for retry. Scope retirement cannot await or consume another namespace's cleanup, while full disposal settles all storage drains and orphaned initialization cleanup before aggregating failures.
|
package/readme.md
CHANGED
|
@@ -110,6 +110,40 @@ const harness = new FlexHarness<IProjectScope>({
|
|
|
110
110
|
|
|
111
111
|
`modelRegistry`, `projectRegistry`, `createProjectTools`, and `closeProjectTools` in this example are application-owned integrations. FlexHarness passes the same run `AbortSignal` to the model resolver and tool provider.
|
|
112
112
|
|
|
113
|
+
## Resource Tool Providers
|
|
114
|
+
|
|
115
|
+
`resourceToolProviderResolver` composes zero or more resource-owned providers with the existing application `toolProvider`. The resolver runs fresh for every prompt and returns the current resource attachment descriptors:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
resourceToolProviderResolver: {
|
|
119
|
+
async resolveResourceToolProviders({ scope, sessionId, runId, signal }) {
|
|
120
|
+
const attachments = await resourceRegistry.listAttached({
|
|
121
|
+
scope,
|
|
122
|
+
sessionId,
|
|
123
|
+
runId,
|
|
124
|
+
signal,
|
|
125
|
+
});
|
|
126
|
+
return attachments.map((attachment) => ({
|
|
127
|
+
resourceId: attachment.resourceId,
|
|
128
|
+
attachmentRevision: attachment.attachmentRevision,
|
|
129
|
+
provider: createResourceToolProvider(attachment),
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each descriptor uses the existing `IFlexToolProvider<TScope>` contract. Its provider receives the normal run context and must return a fresh run-scoped handle. The original `toolProvider` remains optional and its tool names remain unchanged. Resource tool names are deterministic and bounded:
|
|
136
|
+
|
|
137
|
+
1. `resourceIdentity` is the lowercase hexadecimal SHA-256 of `JSON.stringify([resourceId, attachmentRevision])`.
|
|
138
|
+
2. The namespace is `resource_` plus the first 16 digest characters.
|
|
139
|
+
3. The exposed name is `<namespace>__<stem>__<toolDigest>`. `stem` replaces characters outside `[A-Za-z0-9_-]` with `_`, keeps the first 16 characters, and falls back to `tool`; `toolDigest` is the first 12 lowercase hexadecimal characters of SHA-256 over the original tool name.
|
|
140
|
+
|
|
141
|
+
The resolver accepts at most 128 descriptors per run. `resourceId` must be non-empty and at most 512 UTF-8 bytes, `attachmentRevision` must be a non-negative safe integer, and each original resource tool name must be non-empty and at most 512 UTF-8 bytes. FlexHarness rejects duplicate `resourceId` values even across revisions, duplicate derived namespaces, and duplicate final exposed tool names before model execution. Descriptor identity and namespace validation completes before any application or resource provider is acquired.
|
|
142
|
+
|
|
143
|
+
Resource permission requests are scoped with the complete 64-character `resourceIdentity`, not the shortened tool namespace. FlexHarness rewrites `kind` to `resource.<resourceIdentity>.<providerKind>` and an optional `rememberKey` to `resource:<resourceIdentity>:<providerRememberKey>`. Harness-owned metadata contains `resourceId`, `attachmentRevision`, `resourceIdentity`, and `toolNamespace`; provider metadata is nested under `providerMetadata`, so it cannot override attachment identity.
|
|
144
|
+
|
|
145
|
+
FlexHarness owns every acquired handle. Normal close and partial-failure cleanup run in reverse acquisition order, attempt every handle, aggregate multiple failures, and retain failed cleanup for retirement or disposal retry. Cancellation uses the same path. If model resolution fails while a resource provider is still settling, a late returned handle remains tracked and disposal waits for its closure.
|
|
146
|
+
|
|
113
147
|
## Foreground Subagents
|
|
114
148
|
|
|
115
149
|
`subagents` enables a harness-owned built-in tool named `task`. It is available only when at least one definition exists and the current session depth is below `maxSubagentDepth`. An application `toolProvider` must not return its own `task` tool when subagents are configured. The built-in is foreground-only: the parent tool call does not complete until the child prompt reaches a terminal outcome.
|
|
@@ -422,7 +456,7 @@ await migrateLegacyFlexHarnessSnapshot(storageKey, legacySnapshot, stores);
|
|
|
422
456
|
|
|
423
457
|
## Shutdown
|
|
424
458
|
|
|
425
|
-
Model and tool resolution share the run signal. Synchronous throws are observed as resolver failures; the first failure aborts that signal and finalizes immediately without waiting for an unresponsive sibling. A detached tool provider that resolves later is observed and
|
|
459
|
+
Model and tool resolution share the run signal. Synchronous throws are observed as resolver failures; the first failure aborts that signal and finalizes immediately without waiting for an unresponsive sibling. A detached application or resource tool provider that resolves later is observed and every acquired handle is closed; disposal waits for that settlement and reports a late close failure.
|
|
426
460
|
|
|
427
461
|
Tool-handle close settles before a turn can be successful. After model generation resolves, FlexHarness stages its terminal projection before canonical acceptance; earlier execution failures are finalized as interrupted and then published from that durable outcome. Cleanup failure prevents canonical acceptance. If canonical finalization or public promotion fails, FlexHarness fences the namespace; the next load repairs public state from the durable canonical outcome and any hidden terminal stage.
|
|
428
462
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/flexharness',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.6.0',
|
|
7
7
|
description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
|
|
8
8
|
}
|
|
@@ -44,6 +44,7 @@ import type {
|
|
|
44
44
|
IFlexProjectionSnapshot,
|
|
45
45
|
IFlexResolvedModel,
|
|
46
46
|
IFlexResolvedScope,
|
|
47
|
+
IFlexResourceToolProviderDescriptor,
|
|
47
48
|
IFlexRunFinishedEvent,
|
|
48
49
|
IFlexScheduledPromptAdmission,
|
|
49
50
|
IFlexSchedulePromptOptions,
|
|
@@ -54,6 +55,7 @@ import type {
|
|
|
54
55
|
IFlexTerminalProjection,
|
|
55
56
|
IFlexToolHandle,
|
|
56
57
|
IFlexToolMessagePart,
|
|
58
|
+
IFlexToolProviderContext,
|
|
57
59
|
IFlexUncertainToolExecution,
|
|
58
60
|
IFlexUpdateSessionOptions,
|
|
59
61
|
IFlexUsage,
|
|
@@ -337,6 +339,10 @@ const defaultMaxSubagentDepth = 1;
|
|
|
337
339
|
const maximumMaxSubagentDepth = 8;
|
|
338
340
|
const defaultMaxSubagentCallsPerRun = 32;
|
|
339
341
|
const maximumMaxSubagentCallsPerRun = 128;
|
|
342
|
+
const maxResourceToolProviders = 128;
|
|
343
|
+
const maxResourceIdBytes = 512;
|
|
344
|
+
const maxResourceToolNameBytes = 512;
|
|
345
|
+
const resourceToolStemLength = 16;
|
|
340
346
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
341
347
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
342
348
|
const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
@@ -436,6 +442,40 @@ function validateIdentifier(value: string, name: string): void {
|
|
|
436
442
|
}
|
|
437
443
|
}
|
|
438
444
|
|
|
445
|
+
function sha256Hex(value: string): string {
|
|
446
|
+
return plugins.crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function createFlexResourceIdentityDigest(
|
|
450
|
+
resourceId: string,
|
|
451
|
+
attachmentRevision: number,
|
|
452
|
+
): string {
|
|
453
|
+
validateUtf8String(resourceId, 'resourceId', maxResourceIdBytes, true);
|
|
454
|
+
if (!Number.isSafeInteger(attachmentRevision) || attachmentRevision < 0) {
|
|
455
|
+
throw new FlexHarnessValidationError('attachmentRevision must be a non-negative safe integer.');
|
|
456
|
+
}
|
|
457
|
+
return sha256Hex(JSON.stringify([resourceId, attachmentRevision]));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function createFlexResourceToolNamespace(
|
|
461
|
+
resourceId: string,
|
|
462
|
+
attachmentRevision: number,
|
|
463
|
+
): string {
|
|
464
|
+
const digest = createFlexResourceIdentityDigest(resourceId, attachmentRevision);
|
|
465
|
+
return `resource_${digest.slice(0, 16)}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function createFlexResourceToolName(namespace: string, toolName: string): string {
|
|
469
|
+
if (!/^resource_[a-f0-9]{16}$/u.test(namespace)) {
|
|
470
|
+
throw new FlexHarnessValidationError('Resource tool namespace is invalid.');
|
|
471
|
+
}
|
|
472
|
+
validateUtf8String(toolName, 'resource tool name', maxResourceToolNameBytes, true);
|
|
473
|
+
const stem = toolName
|
|
474
|
+
.replace(/[^A-Za-z0-9_-]/gu, '_')
|
|
475
|
+
.slice(0, resourceToolStemLength) || 'tool';
|
|
476
|
+
return `${namespace}__${stem}__${sha256Hex(toolName).slice(0, 12)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
439
479
|
function validateUtf8String(
|
|
440
480
|
value: unknown,
|
|
441
481
|
name: string,
|
|
@@ -859,6 +899,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
859
899
|
private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
860
900
|
private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
861
901
|
private readonly toolProvider: IFlexHarnessOptions<TScope>['toolProvider'];
|
|
902
|
+
private readonly resourceToolProviderResolver: IFlexHarnessOptions<TScope>['resourceToolProviderResolver'];
|
|
862
903
|
private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
|
|
863
904
|
private readonly stores: IFlexHarnessStores;
|
|
864
905
|
private readonly agentSessionPolicy: IFlexAgentSessionPolicy<TScope>;
|
|
@@ -906,6 +947,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
906
947
|
this.scopeResolver = options.scopeResolver;
|
|
907
948
|
this.modelResolver = options.modelResolver;
|
|
908
949
|
this.toolProvider = options.toolProvider;
|
|
950
|
+
this.resourceToolProviderResolver = options.resourceToolProviderResolver;
|
|
909
951
|
this.executionContextProvider = options.executionContextProvider;
|
|
910
952
|
this.stores = options.stores ?? new InMemoryFlexHarnessStores();
|
|
911
953
|
this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
|
|
@@ -2353,16 +2395,18 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2353
2395
|
(value): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: true, value }),
|
|
2354
2396
|
(error): IResolverOutcome<IFlexResolvedModel> => ({ source: 'model', success: false, error }),
|
|
2355
2397
|
);
|
|
2398
|
+
const toolProviderContext: Readonly<IFlexToolProviderContext<TScope>> = Object.freeze({
|
|
2399
|
+
scopeId: run.scopeId,
|
|
2400
|
+
scope: run.scope as TScope,
|
|
2401
|
+
sessionId: run.sessionId,
|
|
2402
|
+
runId: run.runId,
|
|
2403
|
+
...resolverRelationship,
|
|
2404
|
+
signal,
|
|
2405
|
+
requestPermission: (request: IFlexPermissionRequestInput) =>
|
|
2406
|
+
this.requestPermission(run.state, run, request),
|
|
2407
|
+
});
|
|
2356
2408
|
const toolOutcome = Promise.resolve()
|
|
2357
|
-
.then(() => this.
|
|
2358
|
-
scopeId: run.scopeId,
|
|
2359
|
-
scope: run.scope as TScope,
|
|
2360
|
-
sessionId: run.sessionId,
|
|
2361
|
-
runId: run.runId,
|
|
2362
|
-
...resolverRelationship,
|
|
2363
|
-
signal,
|
|
2364
|
-
requestPermission: (request) => this.requestPermission(run.state, run, request),
|
|
2365
|
-
})))
|
|
2409
|
+
.then(() => this.provideRunToolHandle(run, toolProviderContext))
|
|
2366
2410
|
.then(
|
|
2367
2411
|
(value): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: true, value }),
|
|
2368
2412
|
(error): IResolverOutcome<IFlexToolHandle | undefined> => ({ source: 'tools', success: false, error }),
|
|
@@ -3684,6 +3728,161 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3684
3728
|
}
|
|
3685
3729
|
}
|
|
3686
3730
|
|
|
3731
|
+
private async provideRunToolHandle(
|
|
3732
|
+
run: IActiveRun,
|
|
3733
|
+
context: Readonly<IFlexToolProviderContext<TScope>>,
|
|
3734
|
+
): Promise<IFlexToolHandle | undefined> {
|
|
3735
|
+
if (!this.resourceToolProviderResolver) return this.toolProvider?.provideTools(context);
|
|
3736
|
+
|
|
3737
|
+
const resolverContext = Object.freeze({
|
|
3738
|
+
scopeId: context.scopeId,
|
|
3739
|
+
scope: context.scope,
|
|
3740
|
+
sessionId: context.sessionId,
|
|
3741
|
+
runId: context.runId,
|
|
3742
|
+
...(context.parentSessionId === undefined ? {} : { parentSessionId: context.parentSessionId }),
|
|
3743
|
+
...(context.agent === undefined ? {} : { agent: context.agent }),
|
|
3744
|
+
signal: context.signal,
|
|
3745
|
+
});
|
|
3746
|
+
const resolved = await this.resourceToolProviderResolver.resolveResourceToolProviders(resolverContext);
|
|
3747
|
+
const descriptors = this.normalizeResourceToolProviderDescriptors(resolved);
|
|
3748
|
+
const owned: Array<{ handle: IFlexToolHandle; closed: boolean }> = [];
|
|
3749
|
+
const tools: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
3750
|
+
const closeOwned = async (): Promise<void> => {
|
|
3751
|
+
const errors: unknown[] = [];
|
|
3752
|
+
for (const entry of [...owned].reverse()) {
|
|
3753
|
+
if (entry.closed || !entry.handle.close) continue;
|
|
3754
|
+
try {
|
|
3755
|
+
await entry.handle.close();
|
|
3756
|
+
entry.closed = true;
|
|
3757
|
+
} catch (error) {
|
|
3758
|
+
errors.push(error);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
if (errors.length > 0) throw combineErrors(errors);
|
|
3762
|
+
};
|
|
3763
|
+
try {
|
|
3764
|
+
context.signal.throwIfAborted();
|
|
3765
|
+
const applicationHandle = this.normalizeToolHandle(
|
|
3766
|
+
run,
|
|
3767
|
+
await this.toolProvider?.provideTools(context),
|
|
3768
|
+
);
|
|
3769
|
+
if (applicationHandle) {
|
|
3770
|
+
owned.push({ handle: applicationHandle, closed: false });
|
|
3771
|
+
for (const [toolName, tool] of Object.entries(applicationHandle.tools)) {
|
|
3772
|
+
tools[toolName] = tool;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
for (const descriptor of descriptors) {
|
|
3776
|
+
context.signal.throwIfAborted();
|
|
3777
|
+
const namespace = createFlexResourceToolNamespace(
|
|
3778
|
+
descriptor.resourceId,
|
|
3779
|
+
descriptor.attachmentRevision,
|
|
3780
|
+
);
|
|
3781
|
+
const resourceIdentity = createFlexResourceIdentityDigest(
|
|
3782
|
+
descriptor.resourceId,
|
|
3783
|
+
descriptor.attachmentRevision,
|
|
3784
|
+
);
|
|
3785
|
+
const resourceContext = Object.freeze({
|
|
3786
|
+
...context,
|
|
3787
|
+
requestPermission: (request: IFlexPermissionRequestInput) => context.requestPermission({
|
|
3788
|
+
...request,
|
|
3789
|
+
kind: `resource.${resourceIdentity}.${request.kind}`,
|
|
3790
|
+
...(request.rememberKey === undefined
|
|
3791
|
+
? {}
|
|
3792
|
+
: { rememberKey: `resource:${resourceIdentity}:${request.rememberKey}` }),
|
|
3793
|
+
metadata: {
|
|
3794
|
+
resourceId: descriptor.resourceId,
|
|
3795
|
+
attachmentRevision: descriptor.attachmentRevision,
|
|
3796
|
+
resourceIdentity,
|
|
3797
|
+
toolNamespace: namespace,
|
|
3798
|
+
...(request.metadata === undefined ? {} : { providerMetadata: request.metadata }),
|
|
3799
|
+
},
|
|
3800
|
+
}),
|
|
3801
|
+
});
|
|
3802
|
+
const handle = this.normalizeToolHandle(
|
|
3803
|
+
run,
|
|
3804
|
+
await descriptor.provider.provideTools(resourceContext),
|
|
3805
|
+
);
|
|
3806
|
+
if (!handle) continue;
|
|
3807
|
+
owned.push({ handle, closed: false });
|
|
3808
|
+
for (const [toolName, tool] of Object.entries(handle.tools)) {
|
|
3809
|
+
const exposedName = createFlexResourceToolName(namespace, toolName);
|
|
3810
|
+
if (Object.prototype.hasOwnProperty.call(tools, exposedName)) {
|
|
3811
|
+
throw new FlexHarnessValidationError(`Duplicate exposed tool "${exposedName}".`);
|
|
3812
|
+
}
|
|
3813
|
+
tools[exposedName] = tool;
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
if (owned.length === 0 && Object.keys(tools).length === 0) return undefined;
|
|
3817
|
+
return {
|
|
3818
|
+
tools: tools as TFlexAgentToolSet,
|
|
3819
|
+
...(owned.some((entry) => entry.handle.close) ? { close: closeOwned } : {}),
|
|
3820
|
+
};
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
if (owned.some((entry) => entry.handle.close && !entry.closed)) {
|
|
3823
|
+
try {
|
|
3824
|
+
await this.closePartialToolHandle({ tools: {}, close: closeOwned }, run);
|
|
3825
|
+
} catch (cleanupError) {
|
|
3826
|
+
throw combineErrors([error, cleanupError]);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
throw error;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
private normalizeResourceToolProviderDescriptors(
|
|
3834
|
+
descriptors: readonly IFlexResourceToolProviderDescriptor<TScope>[],
|
|
3835
|
+
): IFlexResourceToolProviderDescriptor<TScope>[] {
|
|
3836
|
+
if (!Array.isArray(descriptors) || descriptors.length > maxResourceToolProviders) {
|
|
3837
|
+
throw new FlexHarnessValidationError(
|
|
3838
|
+
`Resource tool providers must be an array with at most ${maxResourceToolProviders} descriptors.`,
|
|
3839
|
+
);
|
|
3840
|
+
}
|
|
3841
|
+
const resourceIds = new Set<string>();
|
|
3842
|
+
const namespaces = new Set<string>();
|
|
3843
|
+
return descriptors.map((descriptor, index) => {
|
|
3844
|
+
if (
|
|
3845
|
+
!descriptor
|
|
3846
|
+
|| typeof descriptor !== 'object'
|
|
3847
|
+
|| Array.isArray(descriptor)
|
|
3848
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(descriptor))
|
|
3849
|
+
) {
|
|
3850
|
+
throw new FlexHarnessValidationError(`Resource tool provider descriptor ${index} is invalid.`);
|
|
3851
|
+
}
|
|
3852
|
+
const unsupported = Object.keys(descriptor).find(
|
|
3853
|
+
(key) => !['resourceId', 'attachmentRevision', 'provider'].includes(key),
|
|
3854
|
+
);
|
|
3855
|
+
if (unsupported) {
|
|
3856
|
+
throw new FlexHarnessValidationError(
|
|
3857
|
+
`Resource tool provider descriptor ${index} does not support "${unsupported}".`,
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
const resourceId = descriptor.resourceId;
|
|
3861
|
+
const attachmentRevision = descriptor.attachmentRevision;
|
|
3862
|
+
const provider = descriptor.provider;
|
|
3863
|
+
const provideTools = provider?.provideTools;
|
|
3864
|
+
const namespace = createFlexResourceToolNamespace(resourceId, attachmentRevision);
|
|
3865
|
+
if (resourceIds.has(resourceId)) {
|
|
3866
|
+
throw new FlexHarnessValidationError(`Duplicate resourceId "${resourceId}".`);
|
|
3867
|
+
}
|
|
3868
|
+
if (namespaces.has(namespace)) {
|
|
3869
|
+
throw new FlexHarnessValidationError(`Duplicate resource tool namespace "${namespace}".`);
|
|
3870
|
+
}
|
|
3871
|
+
if (!provider || typeof provideTools !== 'function') {
|
|
3872
|
+
throw new FlexHarnessValidationError(`Resource tool provider descriptor ${index} has no tool provider.`);
|
|
3873
|
+
}
|
|
3874
|
+
resourceIds.add(resourceId);
|
|
3875
|
+
namespaces.add(namespace);
|
|
3876
|
+
return {
|
|
3877
|
+
resourceId,
|
|
3878
|
+
attachmentRevision,
|
|
3879
|
+
provider: {
|
|
3880
|
+
provideTools: (context) => provideTools.call(provider, context),
|
|
3881
|
+
},
|
|
3882
|
+
};
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3687
3886
|
private async closePartialToolHandle(handle: IFlexToolHandle, run: IActiveRun): Promise<void> {
|
|
3688
3887
|
try {
|
|
3689
3888
|
await handle.close?.();
|
package/ts/interfaces.ts
CHANGED
|
@@ -300,6 +300,29 @@ export interface IFlexToolProvider<TScope> {
|
|
|
300
300
|
): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
export interface IFlexResourceToolProviderResolverContext<TScope> {
|
|
304
|
+
scopeId: string;
|
|
305
|
+
scope: TScope;
|
|
306
|
+
sessionId: string;
|
|
307
|
+
runId: string;
|
|
308
|
+
readonly parentSessionId?: string;
|
|
309
|
+
readonly agent?: string;
|
|
310
|
+
signal: AbortSignal;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface IFlexResourceToolProviderDescriptor<TScope> {
|
|
314
|
+
resourceId: string;
|
|
315
|
+
attachmentRevision: number;
|
|
316
|
+
provider: IFlexToolProvider<TScope>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface IFlexResourceToolProviderResolver<TScope> {
|
|
320
|
+
resolveResourceToolProviders(
|
|
321
|
+
context: IFlexResourceToolProviderResolverContext<TScope>,
|
|
322
|
+
): Promise<readonly IFlexResourceToolProviderDescriptor<TScope>[]>
|
|
323
|
+
| readonly IFlexResourceToolProviderDescriptor<TScope>[];
|
|
324
|
+
}
|
|
325
|
+
|
|
303
326
|
export interface IFlexSessionTombstone {
|
|
304
327
|
sessionId: string;
|
|
305
328
|
deletedAt: string;
|
|
@@ -494,6 +517,7 @@ export interface IFlexHarnessOptions<TScope> {
|
|
|
494
517
|
scopeResolver: IFlexScopeResolver<TScope>;
|
|
495
518
|
modelResolver: IFlexModelResolver<TScope>;
|
|
496
519
|
toolProvider?: IFlexToolProvider<TScope>;
|
|
520
|
+
resourceToolProviderResolver?: IFlexResourceToolProviderResolver<TScope>;
|
|
497
521
|
executionContextProvider?: IFlexExecutionContextProvider<TScope>;
|
|
498
522
|
stores?: IFlexHarnessStores;
|
|
499
523
|
agentSessionPolicy?: IFlexAgentSessionPolicy<TScope>;
|