@modelprofile.com/flexharness 10.11.0 → 10.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +21 -2
- package/dist_ts/classes.flexharness.js +430 -63
- package/dist_ts/classes.toolcatalog.d.ts +80 -0
- package/dist_ts/classes.toolcatalog.js +1336 -0
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +179 -2
- package/dist_ts/interfaces.js +29 -1
- package/dist_ts/plugins.d.ts +7 -3
- package/dist_ts/plugins.js +7 -3
- package/dist_ts/utils.harnessoptions.d.ts +2 -1
- package/dist_ts/utils.harnessoptions.js +19 -1
- package/dist_ts/utils.json.js +37 -2
- package/dist_ts/utils.schemadeclarations.d.ts +53 -0
- package/dist_ts/utils.schemadeclarations.js +315 -0
- package/dist_ts/utils.sessionmetrics.d.ts +17 -0
- package/dist_ts/utils.sessionmetrics.js +72 -0
- package/package.json +6 -5
- package/readme.md +58 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +484 -58
- package/ts/classes.toolcatalog.ts +1431 -0
- package/ts/errors.ts +8 -0
- package/ts/index.ts +1 -0
- package/ts/interfaces.ts +198 -2
- package/ts/plugins.ts +10 -2
- package/ts/readme.md +58 -8
- package/ts/utils.harnessoptions.ts +22 -0
- package/ts/utils.json.ts +33 -1
- package/ts/utils.schemadeclarations.ts +322 -0
- package/ts/utils.sessionmetrics.ts +104 -0
- package/ts_chat_harness/readme.md +1 -1
package/ts/errors.ts
CHANGED
|
@@ -118,6 +118,14 @@ export class FlexHarnessAbortError extends FlexHarnessError {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** The conversational run exhausted its model steps before producing a final answer. */
|
|
122
|
+
export class FlexHarnessStepLimitError extends FlexHarnessError {
|
|
123
|
+
constructor(public readonly steps: number) {
|
|
124
|
+
super('FLEX_STEP_LIMIT', `The run exhausted its ${steps} model steps before completing its answer.`);
|
|
125
|
+
this.name = 'FlexHarnessStepLimitError';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
export class FlexHarnessCallbackOverflowError extends FlexHarnessError {
|
|
122
130
|
constructor(message: string) {
|
|
123
131
|
super('FLEX_CALLBACK_OVERFLOW', message);
|
package/ts/index.ts
CHANGED
package/ts/interfaces.ts
CHANGED
|
@@ -62,6 +62,34 @@ export interface IFlexUsage {
|
|
|
62
62
|
cacheWriteTokens: number;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Durable fixed-size model-call accounting, independent of transcript visibility. */
|
|
66
|
+
export interface IFlexSessionMetricsState {
|
|
67
|
+
reportedLifetimeUsedTokens: number;
|
|
68
|
+
reportedLifetimeUsedTokensExact: boolean;
|
|
69
|
+
lifetimeUsageComplete: boolean;
|
|
70
|
+
pendingGenerationId?: string;
|
|
71
|
+
pendingCompactionId?: string;
|
|
72
|
+
lastObservedInputTokens?: number;
|
|
73
|
+
lastObservedInputTokensAt?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** No member estimates current context size or includes child-session / tool-owned model calls. */
|
|
77
|
+
export interface IFlexSessionMetrics {
|
|
78
|
+
/** Sum of durably settled per-call reports; a lower bound when incomplete. */
|
|
79
|
+
reportedLifetimeUsedTokens: number;
|
|
80
|
+
/** False if invalid reports or safe-integer overflow prevented an exact reported sum. */
|
|
81
|
+
reportedLifetimeUsedTokensExact: boolean;
|
|
82
|
+
lifetimeUsageComplete: boolean;
|
|
83
|
+
/** Present only when lifetimeUsageComplete is true. */
|
|
84
|
+
lifetimeUsedTokens?: number;
|
|
85
|
+
inFlight: boolean;
|
|
86
|
+
/** Input of the last durably settled generation call, not its compaction summary call. */
|
|
87
|
+
lastObservedInputTokens?: number;
|
|
88
|
+
lastObservedInputTokensAt?: string;
|
|
89
|
+
/** Always historical: output, steering, compaction and reversion can change context. */
|
|
90
|
+
lastObservedInputTokensFreshness?: 'historical';
|
|
91
|
+
}
|
|
92
|
+
|
|
65
93
|
export interface IFlexModelIdentity {
|
|
66
94
|
provider: string;
|
|
67
95
|
model: string;
|
|
@@ -620,15 +648,179 @@ export interface IFlexToolProviderContext<TScope> extends IFlexSessionGeneration
|
|
|
620
648
|
requestPermission(request: IFlexPermissionRequestInput): Promise<void>;
|
|
621
649
|
}
|
|
622
650
|
|
|
623
|
-
export interface IFlexToolHandle {
|
|
651
|
+
export interface IFlexToolHandle<TScope = unknown> {
|
|
624
652
|
tools: TFlexAgentToolSet;
|
|
653
|
+
/**
|
|
654
|
+
* Methods the model finds with `search_tools`, reads with `describe_tools` and calls with
|
|
655
|
+
* `execute`, instead of seeing each one as a tool on every step. Return the same object on
|
|
656
|
+
* every run: FlexHarness prepares a catalog once per object and treats it as immutable.
|
|
657
|
+
*/
|
|
658
|
+
catalog?: IFlexToolCatalog<TScope>;
|
|
625
659
|
close?(): Promise<void> | void;
|
|
626
660
|
}
|
|
627
661
|
|
|
628
662
|
export interface IFlexToolProvider<TScope> {
|
|
629
663
|
provideTools(
|
|
630
664
|
context: IFlexToolProviderContext<TScope>,
|
|
631
|
-
): Promise<IFlexToolHandle | undefined> | IFlexToolHandle | undefined;
|
|
665
|
+
): Promise<IFlexToolHandle<TScope> | undefined> | IFlexToolHandle<TScope> | undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** The names of the built-in catalog tools, reserved while a run has a catalog. */
|
|
669
|
+
export const FLEX_TOOL_CATALOG_TOOL_NAMES = Object.freeze({
|
|
670
|
+
search: 'search_tools',
|
|
671
|
+
describe: 'describe_tools',
|
|
672
|
+
execute: 'execute',
|
|
673
|
+
} as const);
|
|
674
|
+
|
|
675
|
+
export const FLEX_TOOL_CATALOG_LIMITS = Object.freeze({
|
|
676
|
+
maxMethods: 4096,
|
|
677
|
+
maxTypes: 4096,
|
|
678
|
+
maxMethodPathCharacters: 160,
|
|
679
|
+
maxMethodPathSegments: 8,
|
|
680
|
+
maxIdentifierCharacters: 128,
|
|
681
|
+
maxSummaryCharacters: 120,
|
|
682
|
+
maxDescriptionBytes: 16 * 1024,
|
|
683
|
+
maxDeclarationBytes: 16 * 1024,
|
|
684
|
+
maxTags: 16,
|
|
685
|
+
maxTagCharacters: 48,
|
|
686
|
+
maxExamples: 4,
|
|
687
|
+
maxExampleBytes: 1024,
|
|
688
|
+
maxSchemaBytes: 64 * 1024,
|
|
689
|
+
defaultSearchResults: 10,
|
|
690
|
+
maxSearchResults: 50,
|
|
691
|
+
maxQueryCharacters: 512,
|
|
692
|
+
maxDescribeMethods: 16,
|
|
693
|
+
maxDescribeTypes: 32,
|
|
694
|
+
maxDescribeBytes: 64 * 1024,
|
|
695
|
+
maxParamIssues: 10,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
export interface IFlexCatalogSearchInput {
|
|
699
|
+
/** Words to rank methods by. */
|
|
700
|
+
query?: string;
|
|
701
|
+
/** A dotted namespace to search in or, without `query`, to list. */
|
|
702
|
+
namespace?: string;
|
|
703
|
+
limit?: number;
|
|
704
|
+
offset?: number;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export interface IFlexCatalogDescribeInput {
|
|
708
|
+
/** Dotted method paths. */
|
|
709
|
+
methods?: readonly string[];
|
|
710
|
+
/** Named types to declare again or for the first time. */
|
|
711
|
+
types?: readonly string[];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export interface IFlexCatalogDescribeResult {
|
|
715
|
+
/** TypeScript text, or a JSON object in `json-schema` mode. */
|
|
716
|
+
output: TJsonValue;
|
|
717
|
+
/** The named types the output declares. */
|
|
718
|
+
declaredTypes: string[];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** One way `execute`'s params miss the method's schema. */
|
|
722
|
+
export interface IFlexCatalogParamIssue {
|
|
723
|
+
/** Where, such as `params.lines[0].amount`. */
|
|
724
|
+
path: string;
|
|
725
|
+
expected: string;
|
|
726
|
+
/** The given value as compact JSON, or `missing`. */
|
|
727
|
+
got: string;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** What `execute` reports to the model, as JSON, when it cannot call a method. */
|
|
731
|
+
export type TFlexCatalogCallError =
|
|
732
|
+
| {
|
|
733
|
+
error: 'unknown_method';
|
|
734
|
+
method: string;
|
|
735
|
+
didYouMean: string[];
|
|
736
|
+
hint: string;
|
|
737
|
+
}
|
|
738
|
+
| {
|
|
739
|
+
error: 'invalid_params';
|
|
740
|
+
method: string;
|
|
741
|
+
issues: IFlexCatalogParamIssue[];
|
|
742
|
+
/** How many more issues were found than listed. */
|
|
743
|
+
more?: number;
|
|
744
|
+
/** The lookup that returns the method's declaration. */
|
|
745
|
+
see: string;
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
export interface IFlexCatalogToolDefinition {
|
|
749
|
+
description: string;
|
|
750
|
+
/** The tool's input as JSON Schema. */
|
|
751
|
+
inputSchema: plugins.JSONSchema7;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* A parameter or result schema: a plain JSON Schema object, or a zod, Standard Schema or AI SDK
|
|
756
|
+
* schema. `$ref`s to `#/$defs/<Name>` or `#/definitions/<Name>` that the schema does not define name
|
|
757
|
+
* a catalog type.
|
|
758
|
+
*/
|
|
759
|
+
export type TFlexCatalogSchema = { readonly [keyword: string]: unknown } | plugins.FlexibleSchema<unknown>;
|
|
760
|
+
|
|
761
|
+
export interface IFlexCatalogRunContext<TScope = unknown> {
|
|
762
|
+
/** The dotted path the model called. */
|
|
763
|
+
readonly method: string;
|
|
764
|
+
/** The `execute` call's ID; pass it with permission requests. */
|
|
765
|
+
readonly toolCallId: string;
|
|
766
|
+
readonly abortSignal: AbortSignal;
|
|
767
|
+
/** The run's tool-provider context: scope, session, run and `requestPermission`. */
|
|
768
|
+
readonly provider: Readonly<IFlexToolProviderContext<TScope>>;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export interface IFlexCatalogMethod<TScope = unknown, TParams = any, TResult = unknown> {
|
|
772
|
+
/**
|
|
773
|
+
* The dotted path the model calls, such as `banking.listMovements`: up to 8 segments, each a
|
|
774
|
+
* JavaScript identifier of letters, digits and `_`.
|
|
775
|
+
*/
|
|
776
|
+
method: string;
|
|
777
|
+
/** One line of at most 120 characters, shown in search results. */
|
|
778
|
+
summary: string;
|
|
779
|
+
/** The full description, shown with the declaration. */
|
|
780
|
+
description?: string;
|
|
781
|
+
/** More words `search_tools` matches. */
|
|
782
|
+
tags?: readonly string[];
|
|
783
|
+
/** Code shown as `@example` in the declaration. */
|
|
784
|
+
examples?: readonly string[];
|
|
785
|
+
/** The parameter object's schema; `execute` validates every call against it. */
|
|
786
|
+
inputSchema: TFlexCatalogSchema;
|
|
787
|
+
/** The result's schema, declared as the method's return type. */
|
|
788
|
+
outputSchema?: TFlexCatalogSchema;
|
|
789
|
+
/**
|
|
790
|
+
* Declaration text that replaces the generated one, such as a declaration built from TypeScript
|
|
791
|
+
* sources with their JSDoc: `function listMovements(params: …): Promise<…>;`, optionally after
|
|
792
|
+
* its JSDoc, with the last path segment as the function name.
|
|
793
|
+
*/
|
|
794
|
+
declaration?: string;
|
|
795
|
+
/** Runs the method with validated parameters. */
|
|
796
|
+
run(
|
|
797
|
+
params: TParams,
|
|
798
|
+
context: IFlexCatalogRunContext<TScope>,
|
|
799
|
+
): TResult | PromiseLike<TResult> | AsyncIterable<TResult>;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** A named type that method declarations refer to; it needs a `declaration`, a `schema`, or both. */
|
|
803
|
+
export interface IFlexCatalogType {
|
|
804
|
+
/** A JavaScript identifier such as `IMoney`. */
|
|
805
|
+
name: string;
|
|
806
|
+
description?: string;
|
|
807
|
+
/** Validates parameters that refer to this type, and generates its declaration. */
|
|
808
|
+
schema?: TFlexCatalogSchema;
|
|
809
|
+
/** Declaration text that replaces the generated one, such as `interface IMoney { … }`. */
|
|
810
|
+
declaration?: string;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export interface IFlexToolCatalog<TScope = unknown> {
|
|
814
|
+
methods: readonly IFlexCatalogMethod<TScope>[];
|
|
815
|
+
types?: readonly IFlexCatalogType[];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export interface IFlexToolCatalogOptions {
|
|
819
|
+
/**
|
|
820
|
+
* What `describe_tools` returns: `typescript` (default) declarations with named types shared
|
|
821
|
+
* across the session, or `json-schema` with each method's self-contained JSON Schema.
|
|
822
|
+
*/
|
|
823
|
+
declarations?: 'typescript' | 'json-schema';
|
|
632
824
|
}
|
|
633
825
|
|
|
634
826
|
export interface IFlexResourceToolProviderResolverContext<TScope> extends IFlexSessionGeneration {
|
|
@@ -775,6 +967,8 @@ export interface IFlexProjectionSnapshotV2 {
|
|
|
775
967
|
|
|
776
968
|
export interface IFlexProjectionSnapshotV3 {
|
|
777
969
|
schemaVersion: 3;
|
|
970
|
+
/** Optional for existing schema-3 stores. Absence means lifetime usage is unknown. */
|
|
971
|
+
metrics?: IFlexSessionMetricsState;
|
|
778
972
|
revision: number;
|
|
779
973
|
messages: IFlexMessage[];
|
|
780
974
|
stagedTerminals: IFlexTerminalProjection[];
|
|
@@ -1389,7 +1583,9 @@ export interface IFlexHarnessOptions<TScope> {
|
|
|
1389
1583
|
externalErrorProjector?: TFlexExternalErrorProjector;
|
|
1390
1584
|
subagents?: IFlexSubagentDefinition[];
|
|
1391
1585
|
builtInTools?: IFlexBuiltInToolsOptions;
|
|
1586
|
+
toolCatalog?: IFlexToolCatalogOptions;
|
|
1392
1587
|
slashCommands?: readonly TFlexSlashCommandRegistration<TScope>[];
|
|
1588
|
+
/** Maximum child depth (root is 0). Defaults to 6; configurable from 1 through 8. */
|
|
1393
1589
|
maxSubagentDepth?: number;
|
|
1394
1590
|
maxSubagentCallsPerRun?: number;
|
|
1395
1591
|
}
|
package/ts/plugins.ts
CHANGED
|
@@ -6,6 +6,13 @@ import * as path from 'node:path';
|
|
|
6
6
|
|
|
7
7
|
export { AsyncLocalStorage, crypto, fs, path };
|
|
8
8
|
|
|
9
|
+
// third party scope
|
|
10
|
+
import { asSchema, jsonSchema } from 'ai';
|
|
11
|
+
import { Validator as JsonSchemaValidator } from '@cfworker/json-schema';
|
|
12
|
+
|
|
13
|
+
export { asSchema, jsonSchema, JsonSchemaValidator };
|
|
14
|
+
export type { FlexibleSchema, JSONSchema7, ModelMessage, Schema } from 'ai';
|
|
15
|
+
|
|
9
16
|
// @push.rocks scope
|
|
10
17
|
import {
|
|
11
18
|
AgentEventStoreConflictError,
|
|
@@ -44,6 +51,7 @@ export {
|
|
|
44
51
|
};
|
|
45
52
|
export type {
|
|
46
53
|
IAgentAppliedInput,
|
|
54
|
+
ISmartAiCacheDispatch,
|
|
47
55
|
IAgentEventArchiveV2,
|
|
48
56
|
IAgentContextCompactionOptions,
|
|
49
57
|
IAgentEventSnapshotV2,
|
|
@@ -77,7 +85,7 @@ export type {
|
|
|
77
85
|
TAgentToolExecutionReconciliationOptions,
|
|
78
86
|
} from '@modelprofile.com/flexharness-agent';
|
|
79
87
|
|
|
80
|
-
import { isModelLimitError, isModelLimitInfo } from '@modelprofile.com/flexharness-models';
|
|
88
|
+
import { getSmartAiCacheWaitMs, isModelLimitError, isModelLimitInfo } from '@modelprofile.com/flexharness-models';
|
|
81
89
|
|
|
82
|
-
export { isModelLimitError, isModelLimitInfo };
|
|
90
|
+
export { getSmartAiCacheWaitMs, isModelLimitError, isModelLimitInfo };
|
|
83
91
|
export type { IModelLimitInfo } from '@modelprofile.com/flexharness-models';
|
package/ts/readme.md
CHANGED
|
@@ -62,7 +62,7 @@ or provider SDKs.
|
|
|
62
62
|
| `/chat` | Portable streaming `ChatSession`, history and usage | None |
|
|
63
63
|
| `/chat/cli` | Ink/React terminal chat | `ink ink-text-input react`; TypeScript: `@types/react` |
|
|
64
64
|
| `/chat/web` | Lit `flexchat-window`, `flexchat-message`, `flexchat-input` | `lit` |
|
|
65
|
-
| `/chat/harness` | `FlexHarnessTranscript` and intents for the `@design.estate/dees-catalog` harness components, browser-safe | `@design.estate/dees-catalog` `^17.0.0 \|\| ^19.5.0 \|\| ^20.0.0` (types only) |
|
|
65
|
+
| `/chat/harness` | `FlexHarnessTranscript` and intents for the `@design.estate/dees-catalog` harness components, browser-safe | `@design.estate/dees-catalog` `^17.0.0 \|\| ^19.5.0 \|\| ^20.0.0 \|\| ^21.0.0` (types only) |
|
|
66
66
|
| `/kvm` | Browser control, terminal framing and OCR observation | `puppeteer` |
|
|
67
67
|
| `/documents` | PDF and image documents, `extractTextFromPdf` | `@push.rocks/smartpdf`; for image documents `sharp` |
|
|
68
68
|
| `/mcp` | MCP clients and AI SDK tool conversion | `@modelcontextprotocol/sdk` |
|
|
@@ -92,6 +92,41 @@ a model backed by its own server transport, or supply `run` in
|
|
|
92
92
|
`IChatSessionOptions`. The web element accepts an `IChatSession` and maintains its
|
|
93
93
|
own subscription. Full Node `AgentSession` remains at the agent package root.
|
|
94
94
|
|
|
95
|
+
## Durable session token metrics
|
|
96
|
+
|
|
97
|
+
Read `await harness.getSessionMetrics(scopeId, sessionId)` for bounded, durable
|
|
98
|
+
accounting independent of transcript visibility. It reads fixed-size projection
|
|
99
|
+
state rather than summing assistant messages or scanning event/archive/child history.
|
|
100
|
+
A warm read joins outstanding projection writes and surfaces accounting failures.
|
|
101
|
+
|
|
102
|
+
| Field | Meaning |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `reportedLifetimeUsedTokens` | Sum of durably settled per-call reports. Incomplete accounting makes this a lower bound. |
|
|
105
|
+
| `reportedLifetimeUsedTokensExact` | Whether the sum of those reports is exact; invalid reports and safe-integer overflow make it inexact. |
|
|
106
|
+
| `lifetimeUsageComplete` | Every owned call is accounted for and no accounting operation remains in flight. |
|
|
107
|
+
| `lifetimeUsedTokens` | Present only when the lifetime total is complete. Zero is a valid total. |
|
|
108
|
+
| `inFlight` | A generation or actual compactor invocation has not settled its accounting yet. |
|
|
109
|
+
| `lastObservedInputTokens`, `lastObservedInputTokensAt` | Input count and observation time of the last settled generation call with complete input/output totals. |
|
|
110
|
+
| `lastObservedInputTokensFreshness` | Always `historical`. This is **not** current context size: output, steering, compaction and reversion can change context. |
|
|
111
|
+
|
|
112
|
+
Accounting covers this session's generation calls and reported compaction calls,
|
|
113
|
+
including failure, cancellation and retries. Child sessions own their own spend;
|
|
114
|
+
tool-owned model calls are not included. Do not sum cumulative assistant-message
|
|
115
|
+
usage to reconstruct lifetime spend. Missing/partial reports remain incomplete;
|
|
116
|
+
unsafe totals saturate at `Number.MAX_SAFE_INTEGER` without claiming exactness.
|
|
117
|
+
A compactor that reports no calls cannot prove zero spend.
|
|
118
|
+
|
|
119
|
+
New sessions start complete at zero. Existing/imported sessions with unknown prior
|
|
120
|
+
spend start incomplete. Write-ahead operation markers make interrupted recovery
|
|
121
|
+
conservative: unresolved operations are cleared without certifying their unknown
|
|
122
|
+
spend. Undo, redo, compaction and transcript archiving never subtract lifetime
|
|
123
|
+
spend. Importing visible history does not reconstruct historical billing.
|
|
124
|
+
|
|
125
|
+
Conversational runs that exhaust `maxSteps` with tool work or unseen input remaining
|
|
126
|
+
fail with `FlexHarnessStepLimitError` (`FLEX_STEP_LIMIT`), rather than being marked
|
|
127
|
+
completed with an empty answer. Standalone Agent callers retain tool-only results
|
|
128
|
+
and can inspect `terminationReason: 'model' | 'step-limit'`.
|
|
129
|
+
|
|
95
130
|
## Provider subpaths
|
|
96
131
|
|
|
97
132
|
Install `@modelprofile.com/flexharness-providers` once. Its root exports all provider
|
|
@@ -260,7 +295,7 @@ const harness = new FlexHarness<IProjectScope>({
|
|
|
260
295
|
maxSteps: 8,
|
|
261
296
|
},
|
|
262
297
|
],
|
|
263
|
-
maxSubagentDepth:
|
|
298
|
+
maxSubagentDepth: 6,
|
|
264
299
|
maxSubagentCallsPerRun: 32,
|
|
265
300
|
externalErrorProjector: (_error, context) => ({
|
|
266
301
|
name: 'ModelOperationError',
|
|
@@ -414,12 +449,10 @@ Normal Flex session cleanup always waits in-flight local project operations, the
|
|
|
414
449
|
|
|
415
450
|
## Subagents
|
|
416
451
|
|
|
417
|
-
`subagents` enables the harness-owned `delegate` and `
|
|
452
|
+
`subagents` enables the harness-owned `delegate`, `delegate_result`, and `delegate_send` tools when at least one definition exists and the session depth is below `maxSubagentDepth`. A tool provider cannot replace any enabled built-in; global and agent tool allowlists still intersect to control model-facing exposure. Delegation is asynchronous by default: `delegate` returns `{ taskId, status: 'running', runId }` after durable child admission, so the parent can continue independent work. `delegate_result({ taskId, waitMs? })` reads live status and, once settled, bounded final text and model identity; its optional wait is limited to 240 seconds and observes parent cancellation. Set `foreground: true` to await the final answer in `delegate` itself. Children remain owned by the parent turn: Stop cancels its exact child runs, and the parent joins them before releasing authority and finalizing. A child result enters the parent's next model context as untrusted task data. When the parent's model ends its turn while background children still run, the turn waits for the next child to settle and takes its result in with another step (while steps remain), so a delegated result is never lost because the parent finished first.
|
|
418
453
|
|
|
419
454
|
The model calls it with this exact input shape:
|
|
420
455
|
|
|
421
|
-
When upgrading from 9.x, callers that require the previous synchronous delegate result must pass `foreground: true`. Tool providers must also leave the newly reserved `delegate_result` name available when subagents are enabled.
|
|
422
|
-
|
|
423
456
|
```typescript
|
|
424
457
|
interface IDelegateInput {
|
|
425
458
|
description: string;
|
|
@@ -430,9 +463,24 @@ interface IDelegateInput {
|
|
|
430
463
|
}
|
|
431
464
|
```
|
|
432
465
|
|
|
466
|
+
When upgrading from 9.x, callers that require the previous synchronous delegate result must pass `foreground: true`. Tool providers must also leave the reserved `delegate_result` and `delegate_send` names available when subagents are enabled.
|
|
467
|
+
|
|
468
|
+
`delegate_send({ taskId, prompt })` sends follow-up instructions to a running direct child owned by this exact parent turn. Both fields are required and extra fields are rejected; `taskId` is non-empty and at most 512 UTF-8 bytes, and `prompt` is non-empty and at most 64 KiB. It returns `{ steerId, queueId, runId }` on admission, with a stable steer identity derived from the parent run and tool call. The child takes the input at its next inference boundary, without interrupting a running tool or permission wait. A child waiting to finish while its own descendants run can take the follow-up and continue inference; those descendants keep running. Use `delegate_result` to inspect progress and collect the final answer.
|
|
469
|
+
|
|
470
|
+
Controllers can use the same admission path explicitly:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
await harness.sendSubagentInput(scopeId, parentSessionId, parentRunId, taskId, {
|
|
474
|
+
steerId: 'follow-up-1',
|
|
475
|
+
prompt: 'Prefer primary sources and include the publication date.',
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Sending requires the exact active parent run, its currently delegated direct child, matching captured configuration and parent/child generations, and live writable ancestors. It does not authorize a root to skip levels, or infer ownership from immutable creation-origin metadata: a resumed child belongs to the current delegate invocation. Foreign, unowned, stale, cancelled, settled, or ending runs reject; sending never starts an idle generation, restarts a child, or resumes one. General public `steerPrompt()` still forbids child sessions. Child sends share root steering's duplicate rejection, pending count/byte bounds, accepted/applied events, durable user-message application, and terminal unapplied-input accounting.
|
|
480
|
+
|
|
433
481
|
Before creating or resuming a child, FlexHarness requests permission on the parent run with `kind: 'subagent.start'`, the parent `toolCallId`, and separately bounded harness-owned agent/task metadata. Its metadata includes `childSessionId`, the exact ID reserved for this call: FlexHarness derives it deterministically when `taskId` is omitted and copies the supplied candidate when `taskId` is present. A resume also retains that candidate as `taskId`. This block is not truncated by `toolOutputLimits`. The reserved ID binds permission handling before child creation but does not prove that the child exists or is owned: applications must treat the later delegated admission context as authoritative. The controller answers through the normal permission APIs. This request has no `rememberKey`, so `always` is invalid; controllers use `once` or `reject`.
|
|
434
482
|
|
|
435
|
-
Each new invocation creates a durable child `IFlexSession` with immutable `parentSessionId`, origin `parentRunId`, origin `parentToolCallId`, `agent`, and `depth`. New public roots persist `depth: 0`; legacy schema-1 roots may omit it. These fields are harness-owned; public `createSession()`
|
|
483
|
+
Each new invocation creates a durable child `IFlexSession` with immutable `parentSessionId`, origin `parentRunId`, origin `parentToolCallId`, `agent`, and `depth`. New public roots persist `depth: 0`; legacy schema-1 roots may omit it. These fields are harness-owned; public `createSession()` accepts `sessionId`, `sessionGenerationId`, `configurationRef`, and `title`. A child inherits its parent's `configurationRef` and cannot resume under a different reference. Child sessions reject direct `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the `delegate` tool. The model and tool resolver contexts receive optional immutable `parentSessionId` and `agent` values so integrations can apply agent-specific model and tool policy. Child prompts use the definition's `modelHint`, `system`, and `maxSteps`.
|
|
436
484
|
|
|
437
485
|
The parent tool part receives `childSessionId` as soon as the child is acquired, so a host can offer an authorized live transcript. A foreground call also adds the resolved child model to its part and returns bounded JSON:
|
|
438
486
|
|
|
@@ -485,7 +533,7 @@ Acquisition completes before generation-side branch reversion, context compactio
|
|
|
485
533
|
|
|
486
534
|
For a normally acquired lease, FlexHarness gives `close()` an awaited attempt after AgentSession generation and before canonical accepted, rejected, or interrupted finalization and the terminal `prompt.finished` event. If an abort detaches an acquisition that ignores its signal, the run may settle before acquisition returns; FlexHarness retains that owner and closes any late lease. `close()` must be idempotent and safe to retry after rejection. A close failure prevents successful child acceptance and remains owned by the exact child session generation for retry by later exact-session deletion, scope retirement, or disposal. Retirement and disposal truthfully wait for late acquisition and lease cleanup.
|
|
487
535
|
|
|
488
|
-
Limits are validated and frozen at construction: at most 32 unique definitions; names are non-empty and at most 128 UTF-8 bytes; descriptions 2048 bytes; optional model hints 512 bytes; optional system prompts 64 KiB; and optional `maxSteps` a positive safe integer. `maxSubagentDepth` defaults to
|
|
536
|
+
Limits are validated and frozen at construction: at most 32 unique definitions; names are non-empty and at most 128 UTF-8 bytes; descriptions 2048 bytes; optional model hints 512 bytes; optional system prompts 64 KiB; and optional `maxSteps` a positive safe integer. `maxSubagentDepth` defaults to 6 and must be a positive safe integer at most 8. The root is depth 0, so the default permits six child levels (depths 1 through 6); delegation tools are absent at the configured maximum. `maxSubagentCallsPerRun` defaults to 32 and must be a positive safe integer at most 128. A call slot is consumed synchronously at the start of every schema-valid delegate execution, before semantic bounds, subagent type/depth validation, permission, or child work. Inputs rejected by the tool schema never start delegate execution and do not consume a slot. After successful semantic validation, the child ID candidate is reserved for the rest of the parent run, including after permission rejection or later failure. Permission rejection creates no child session. Omitting `taskId` reserves a deterministic new child ID; supplying `taskId` reserves that unverified candidate and attempts resume after permission only if it identifies a resumable child. Delegate descriptions are non-empty and at most 256 UTF-8 bytes, prompts non-empty and at most 64 KiB, subagent types at most 128 bytes, and task IDs at most 512 bytes.
|
|
489
537
|
|
|
490
538
|
Scope snapshots remain schema 1 and legacy child tombstones without `subagent` provenance continue to load. New child tombstones write the atomic provenance block and reject partial, parent-generation-mismatched, depth-mismatched, or duplicate-origin records. FlexHarness versions before this provenance addition reject that new optional key under their strict reader, so downgrading or mixing old readers with newly written scope snapshots is unsupported.
|
|
491
539
|
|
|
@@ -645,7 +693,7 @@ console.log(result.unappliedSteerIds); // [] once the run took the steer in
|
|
|
645
693
|
|
|
646
694
|
The run takes a steer in at its next step boundary: after the tool results of its current model step are recorded and before its next model call. A running tool call or a pending permission is never interrupted; the steer waits for the boundary that follows it. A steer taken in is a user message of the run (`steerId` set) that the model sees in its next call and every later turn. If the run had already answered, the assistant message that answered is completed and the run answers on in a new assistant message after the steer; a steer taken in before the run answered anything is placed before its still empty assistant message. `IFlexPromptResult.userMessage` stays the prompt and `assistantMessage` is the run's last assistant message. Undo and redo treat the run's messages as one turn.
|
|
647
695
|
|
|
648
|
-
`steerPrompt()` resolves with `{ steerId, queueId, runId }` once the run accepted the steer and `prompt.steer.accepted` was emitted; `prompt.steer.applied` carries the `messageId` of the user message the steer became. Every accepted steer ends in exactly one of two ways: it is in the model context of later turns, or it is listed in `unappliedSteerIds`, in arrival order, on `IFlexPromptResult` and `run.finished`, and on the finished queue entry when that list is not empty. A steer is listed when the run did not take it in before it ended, because its last model step was already running or because it was stopped or failed, and also when the run itself does not stay in context. `keptInContext` on `run.finished` and on the finished queue entry says whether it does: it is `false` for a failed run and for a run stopped before any model output, and such a run lists every steer it accepted, including the ones it took in and reported with `prompt.steer.applied`. Send its prompt and its unapplied steers again to have them answered. The run stops taking steers atomically when its generation ends or `abort()`/`cancelPrompt()` stops it. A steer for a prompt without a running run, or whose run is ending, is refused with `FlexHarnessSteerRejectedError` reason `not-running`; the caller sends it as the next turn instead. Reusing a `steerId` within a run is refused with reason `duplicate`, an unknown `queueId` throws `FlexHarnessNotFoundError`, and
|
|
696
|
+
`steerPrompt()` resolves with `{ steerId, queueId, runId }` once the run accepted the steer and `prompt.steer.accepted` was emitted; `prompt.steer.applied` carries the `messageId` of the user message the steer became. Every accepted steer ends in exactly one of two ways: it is in the model context of later turns, or it is listed in `unappliedSteerIds`, in arrival order, on `IFlexPromptResult` and `run.finished`, and on the finished queue entry when that list is not empty. A steer is listed when the run did not take it in before it ended, because its last model step was already running or because it was stopped or failed, and also when the run itself does not stay in context. `keptInContext` on `run.finished` and on the finished queue entry says whether it does: it is `false` for a failed run and for a run stopped before any model output, and such a run lists every steer it accepted, including the ones it took in and reported with `prompt.steer.applied`. Send its prompt and its unapplied steers again to have them answered. The run stops taking steers atomically when its generation ends or `abort()`/`cancelPrompt()` stops it. A steer for a prompt without a running run, or whose run is ending, is refused with `FlexHarnessSteerRejectedError` reason `not-running`; the caller sends it as the next turn instead. Reusing a `steerId` within a run is refused with reason `duplicate`, an unknown `queueId` throws `FlexHarnessNotFoundError`, and child sessions reject this general API (their exact parent run uses `sendSubagentInput()` or `delegate_send` instead). A run holds at most `maxOutstandingPromptsPerSession` pending steers, and their bytes count against the session's outstanding prompt byte limit until the run takes them in or ends, so `startPrompt()` and `enqueuePrompt()` are refused with `FlexHarnessQueueFullError` while pending steers fill it. They are process-local like queue entries: a process restart ends the run and drops its pending steers, while steers it took in are durable parts of the turn.
|
|
649
697
|
|
|
650
698
|
## Slash Commands
|
|
651
699
|
|
|
@@ -1089,6 +1137,8 @@ Each domain serializes its own mutations. A successful run first persists a hidd
|
|
|
1089
1137
|
|
|
1090
1138
|
Projection stores accept schema version 1, 2, or 3 from `load()`, but every `save()` receives the current schema-3 shape. Schema 3 records explicit reversion protocol, transcript/workspace provenance, and V2 disposition. A terminal V2 segment must be conclusively `revertible`, `no-change`, or `nonrevertible`; a pending V2 capture remains owned by its capture WAL and is never treated as transcript history.
|
|
1091
1139
|
|
|
1140
|
+
Current schema-3 projections also persist optional `metrics` state. Older strict readers that do not recognize this key cannot reopen a projection after metrics have been written; downgrade or mixed-version access to that session is unsupported.
|
|
1141
|
+
|
|
1092
1142
|
A loaded schema-1 projection has no reversion state. Schema-2 `workspaceCaptured` segments migrate as protocol-1 workspace/revertible history without discarding references; transcript-only segments migrate as protocol-1 transcript provenance. Under `workspace-required`, that legacy transcript history is a barrier. The next projection mutation writes schema 3. Custom stores must preserve strict compare-and-swap revisions across schema-1 and schema-2 read-upgrade-write cycles, including uncertain-save reconciliation.
|
|
1093
1143
|
|
|
1094
1144
|
## Migrating From 2.x
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
IFlexSlashCommandHandlerRegistration,
|
|
14
14
|
IFlexSlashCommandTemplateRegistration,
|
|
15
15
|
IFlexSubagentDefinition,
|
|
16
|
+
IFlexToolCatalogOptions,
|
|
16
17
|
TFlexSlashCommandRegistration,
|
|
17
18
|
} from './interfaces.js';
|
|
18
19
|
import {
|
|
@@ -156,6 +157,27 @@ export function normalizeSubagents(
|
|
|
156
157
|
return Object.freeze(normalized);
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
export function normalizeToolCatalogOptions(
|
|
161
|
+
options: IFlexHarnessOptions<unknown>['toolCatalog'],
|
|
162
|
+
): Readonly<Required<IFlexToolCatalogOptions>> {
|
|
163
|
+
if (options === undefined) return Object.freeze({ declarations: 'typescript' });
|
|
164
|
+
if (
|
|
165
|
+
!options
|
|
166
|
+
|| typeof options !== 'object'
|
|
167
|
+
|| Array.isArray(options)
|
|
168
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))
|
|
169
|
+
) {
|
|
170
|
+
throw new FlexHarnessValidationError('toolCatalog must be a plain object.');
|
|
171
|
+
}
|
|
172
|
+
const unsupported = Object.keys(options).find((key) => key !== 'declarations');
|
|
173
|
+
if (unsupported) throw new FlexHarnessValidationError(`toolCatalog does not support "${unsupported}".`);
|
|
174
|
+
const declarations = options.declarations ?? 'typescript';
|
|
175
|
+
if (declarations !== 'typescript' && declarations !== 'json-schema') {
|
|
176
|
+
throw new FlexHarnessValidationError('toolCatalog.declarations must be "typescript" or "json-schema".');
|
|
177
|
+
}
|
|
178
|
+
return Object.freeze({ declarations });
|
|
179
|
+
}
|
|
180
|
+
|
|
159
181
|
export function normalizeBuiltInTools(
|
|
160
182
|
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
161
183
|
): INormalizedBuiltInTools {
|
package/ts/utils.json.ts
CHANGED
|
@@ -1506,6 +1506,35 @@ function validateAffectedWorkspaces(value: unknown, path: string): void {
|
|
|
1506
1506
|
}
|
|
1507
1507
|
}
|
|
1508
1508
|
|
|
1509
|
+
function assertSessionMetrics(value: unknown): void {
|
|
1510
|
+
const path = '$snapshot.metrics';
|
|
1511
|
+
const metrics = requireRecord(value, path);
|
|
1512
|
+
requireOnlyKeys(metrics, [
|
|
1513
|
+
'reportedLifetimeUsedTokens', 'reportedLifetimeUsedTokensExact', 'lifetimeUsageComplete',
|
|
1514
|
+
'pendingGenerationId', 'pendingCompactionId', 'lastObservedInputTokens', 'lastObservedInputTokensAt',
|
|
1515
|
+
], path);
|
|
1516
|
+
requireNonNegativeInteger(metrics.reportedLifetimeUsedTokens, `${path}.reportedLifetimeUsedTokens`);
|
|
1517
|
+
for (const key of ['reportedLifetimeUsedTokensExact', 'lifetimeUsageComplete']) {
|
|
1518
|
+
if (typeof metrics[key] !== 'boolean') throw new FlexHarnessStoreFormatError(`${path}.${key} must be boolean.`);
|
|
1519
|
+
}
|
|
1520
|
+
if (!metrics.reportedLifetimeUsedTokensExact && metrics.lifetimeUsageComplete) {
|
|
1521
|
+
throw new FlexHarnessStoreFormatError(`${path} cannot certify an inexact total.`);
|
|
1522
|
+
}
|
|
1523
|
+
for (const key of ['pendingGenerationId', 'pendingCompactionId']) {
|
|
1524
|
+
if (Object.hasOwn(metrics, key)) requireBoundedString(metrics[key], `${path}.${key}`, 512);
|
|
1525
|
+
}
|
|
1526
|
+
const hasInput = Object.hasOwn(metrics, 'lastObservedInputTokens');
|
|
1527
|
+
const hasTime = Object.hasOwn(metrics, 'lastObservedInputTokensAt');
|
|
1528
|
+
if (hasInput !== hasTime) throw new FlexHarnessStoreFormatError(`${path} has a partial input observation.`);
|
|
1529
|
+
if (hasInput) {
|
|
1530
|
+
requireNonNegativeInteger(metrics.lastObservedInputTokens, `${path}.lastObservedInputTokens`);
|
|
1531
|
+
const timestamp = requireString(metrics.lastObservedInputTokensAt, `${path}.lastObservedInputTokensAt`);
|
|
1532
|
+
if (!Number.isFinite(Date.parse(timestamp)) || new Date(timestamp).toISOString() !== timestamp) {
|
|
1533
|
+
throw new FlexHarnessStoreFormatError(`${path}.lastObservedInputTokensAt must be an ISO timestamp.`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1509
1538
|
export function assertFlexProjectionSnapshotV3(
|
|
1510
1539
|
value: unknown,
|
|
1511
1540
|
): asserts value is IFlexProjectionSnapshotV3 {
|
|
@@ -1514,6 +1543,7 @@ export function assertFlexProjectionSnapshotV3(
|
|
|
1514
1543
|
requireOnlyKeys(snapshot, [
|
|
1515
1544
|
'schemaVersion',
|
|
1516
1545
|
'revision',
|
|
1546
|
+
'metrics',
|
|
1517
1547
|
'messages',
|
|
1518
1548
|
'stagedTerminals',
|
|
1519
1549
|
'reversionSegments',
|
|
@@ -1522,6 +1552,7 @@ export function assertFlexProjectionSnapshotV3(
|
|
|
1522
1552
|
'pendingReversion',
|
|
1523
1553
|
'pendingReversionReleases',
|
|
1524
1554
|
], '$snapshot');
|
|
1555
|
+
if (Object.hasOwn(snapshot, 'metrics')) assertSessionMetrics(snapshot.metrics);
|
|
1525
1556
|
if (snapshot.schemaVersion !== 3) {
|
|
1526
1557
|
throw new FlexHarnessStoreFormatError('Projection snapshot schemaVersion must be 3.');
|
|
1527
1558
|
}
|
|
@@ -1673,8 +1704,9 @@ export function assertFlexProjectionSnapshotV3(
|
|
|
1673
1704
|
}
|
|
1674
1705
|
releaseCaptureIds.add(captureId);
|
|
1675
1706
|
}
|
|
1707
|
+
const { metrics: _metrics, ...compatibleSnapshot } = snapshot;
|
|
1676
1708
|
const compatibleV2 = {
|
|
1677
|
-
...
|
|
1709
|
+
...compatibleSnapshot,
|
|
1678
1710
|
schemaVersion: 2,
|
|
1679
1711
|
reversionSegments: snapshot.reversionSegments.map((entry) => {
|
|
1680
1712
|
const segment = entry as Record<string, unknown>;
|