@modelprofile.com/flexharness 12.5.0 → 13.1.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.callbacks.js +10 -3
- package/dist_ts/classes.flexharness.core.js +4 -2
- package/dist_ts/classes.flexharness.permissions.js +11 -3
- package/dist_ts/classes.flexharness.prompts.js +11 -2
- package/dist_ts/classes.flexharness.reversions.js +21 -6
- package/dist_ts/classes.flexharness.runs.js +13 -7
- package/dist_ts/classes.flexharness.subagents.js +4 -5
- package/dist_ts/classes.toolcatalog.d.ts +14 -0
- package/dist_ts/classes.toolcatalog.js +50 -28
- package/dist_ts/errors.d.ts +8 -2
- package/dist_ts/errors.js +13 -5
- package/dist_ts/interfaces.d.ts +27 -0
- package/dist_ts/interfaces.flexharness.d.ts +3 -1
- package/dist_ts/interfaces.js +7 -1
- package/dist_ts/utils.flexharness.d.ts +8 -2
- package/dist_ts/utils.flexharness.js +13 -3
- package/dist_ts/utils.json.js +9 -2
- package/dist_ts/utils.sessionpreview.d.ts +7 -0
- package/dist_ts/utils.sessionpreview.js +35 -0
- package/dist_ts_typedrequest/catalog.d.ts +3 -1
- package/dist_ts_typedrequest/catalog.js +8 -4
- package/dist_ts_typedrequest/describe.js +2 -22
- package/dist_ts_typedrequest/interfaces.d.ts +4 -1
- package/dist_ts_typedrequest/references.d.ts +17 -0
- package/dist_ts_typedrequest/references.js +65 -0
- package/package.json +3 -3
- package/readme.md +8 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.callbacks.ts +9 -3
- package/ts/classes.flexharness.core.ts +2 -1
- package/ts/classes.flexharness.permissions.ts +8 -2
- package/ts/classes.flexharness.prompts.ts +9 -1
- package/ts/classes.flexharness.reversions.ts +21 -5
- package/ts/classes.flexharness.runs.ts +11 -4
- package/ts/classes.flexharness.subagents.ts +3 -3
- package/ts/classes.toolcatalog.ts +51 -23
- package/ts/errors.ts +13 -4
- package/ts/interfaces.flexharness.ts +3 -0
- package/ts/interfaces.ts +33 -0
- package/ts/readme.md +8 -4
- package/ts/utils.flexharness.ts +16 -2
- package/ts/utils.json.ts +12 -1
- package/ts/utils.sessionpreview.ts +32 -0
- package/ts_typedrequest/catalog.ts +7 -3
- package/ts_typedrequest/describe.ts +1 -22
- package/ts_typedrequest/interfaces.ts +4 -1
- package/ts_typedrequest/readme.md +1 -1
- package/ts_typedrequest/references.ts +68 -0
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
FlexHarnessAbortError,
|
|
4
4
|
FlexHarnessExternalError,
|
|
5
5
|
FlexHarnessNotFoundError,
|
|
6
|
+
FlexHarnessPermissionRejectedError,
|
|
6
7
|
FlexHarnessRunError,
|
|
7
8
|
FlexHarnessStepLimitError,
|
|
8
9
|
FlexHarnessValidationError,
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
externalErrorFallback,
|
|
54
55
|
requireSessionGeneration,
|
|
55
56
|
combineErrors,
|
|
57
|
+
delegatedCancellation,
|
|
56
58
|
terminalActivityError,
|
|
57
59
|
} from './utils.flexharness.js';
|
|
58
60
|
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
@@ -664,9 +666,8 @@ export class FlexHarnessRuns<TScope> {
|
|
|
664
666
|
if (!active || active.runId !== runId || active.phase === 'finalizing' || active.phase === 'promoting') {
|
|
665
667
|
return false;
|
|
666
668
|
}
|
|
667
|
-
const cancellation = reason
|
|
668
|
-
|
|
669
|
-
: this.core.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
669
|
+
const cancellation = delegatedCancellation(reason)
|
|
670
|
+
?? this.core.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
670
671
|
this.cancelRun(active, cancellation);
|
|
671
672
|
return true;
|
|
672
673
|
}
|
|
@@ -914,9 +915,15 @@ export class FlexHarnessRuns<TScope> {
|
|
|
914
915
|
assistantMessage.usage = cloneSerializable(run.usage);
|
|
915
916
|
const message = truncateUtf8(error?.message ?? externalErrorFallback.message, maxTransferMetadataBytes);
|
|
916
917
|
const limit = error instanceof FlexHarnessExternalError ? error.limit : undefined;
|
|
917
|
-
|
|
918
|
+
const rejectedPermissionId = error instanceof FlexHarnessPermissionRejectedError
|
|
919
|
+
? error.permissionId
|
|
920
|
+
: undefined;
|
|
921
|
+
// A failed turn fails its prompt too. A stopped turn is no fault of its prompt, so the prompt
|
|
922
|
+
// stays an ordinary user message and only the answer it stopped carries the stop.
|
|
923
|
+
for (const failedMessage of status === 'cancelled' ? [assistantMessage] : [userMessage, assistantMessage]) {
|
|
918
924
|
failedMessage.error = message;
|
|
919
925
|
if (limit) failedMessage.limit = cloneSerializable(limit);
|
|
926
|
+
if (rejectedPermissionId) failedMessage.rejectedPermissionId = rejectedPermissionId;
|
|
920
927
|
}
|
|
921
928
|
}
|
|
922
929
|
return {
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
requireSessionGeneration,
|
|
42
42
|
sha256Hex,
|
|
43
43
|
combineErrors,
|
|
44
|
+
delegatedCancellation,
|
|
44
45
|
} from './utils.flexharness.js';
|
|
45
46
|
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
46
47
|
|
|
@@ -379,9 +380,8 @@ export class FlexHarnessSubagents<TScope> {
|
|
|
379
380
|
let queued: Awaited<ReturnType<typeof this.core.prompts.enqueuePromptInternal>> | undefined;
|
|
380
381
|
let admission: IFlexPromptAdmission | undefined;
|
|
381
382
|
const abortChild = () => {
|
|
382
|
-
const reason = run.controller.signal.reason
|
|
383
|
-
|
|
384
|
-
: this.core.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
383
|
+
const reason = delegatedCancellation(run.controller.signal.reason)
|
|
384
|
+
?? this.core.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
|
|
385
385
|
const ownedChild = child ?? run.state.sessions.get(reservedSessionId);
|
|
386
386
|
if (
|
|
387
387
|
ownedChild
|
|
@@ -396,6 +396,31 @@ const typeIdentifierOf = (name: string): string => {
|
|
|
396
396
|
return reservedWords.has(identifier) ? `${identifier}_` : identifier;
|
|
397
397
|
};
|
|
398
398
|
|
|
399
|
+
/**
|
|
400
|
+
* The identifier each of a catalog's type names is declared under. A name that is an identifier
|
|
401
|
+
* keeps it; the others are mapped in code-point order, so a catalog always declares the same
|
|
402
|
+
* identifiers.
|
|
403
|
+
*/
|
|
404
|
+
const typeIdentifiersOf = (typeNames: Iterable<string>): Map<string, string> => {
|
|
405
|
+
const typeIdentifiers = new Map<string, string>();
|
|
406
|
+
const taken = new Set<string>();
|
|
407
|
+
const names = [...new Set(typeNames)].sort(compareCodePoints);
|
|
408
|
+
for (const name of names) {
|
|
409
|
+
if (typeIdentifierOf(name) !== name) continue;
|
|
410
|
+
typeIdentifiers.set(name, name);
|
|
411
|
+
taken.add(name);
|
|
412
|
+
}
|
|
413
|
+
for (const name of names) {
|
|
414
|
+
if (typeIdentifiers.has(name)) continue;
|
|
415
|
+
const base = typeIdentifierOf(name);
|
|
416
|
+
let identifier = base;
|
|
417
|
+
for (let suffix = 2; taken.has(identifier); suffix++) identifier = `${base}_${suffix}`;
|
|
418
|
+
typeIdentifiers.set(name, identifier);
|
|
419
|
+
taken.add(identifier);
|
|
420
|
+
}
|
|
421
|
+
return typeIdentifiers;
|
|
422
|
+
};
|
|
423
|
+
|
|
399
424
|
const textListOf = (
|
|
400
425
|
value: unknown,
|
|
401
426
|
label: string,
|
|
@@ -603,24 +628,7 @@ class FlexToolCatalogCore {
|
|
|
603
628
|
}
|
|
604
629
|
namedTypes.set(entry.name, entry);
|
|
605
630
|
}
|
|
606
|
-
|
|
607
|
-
// catalog always declares the same identifiers.
|
|
608
|
-
const typeIdentifiers = new Map<string, string>();
|
|
609
|
-
const taken = new Set<string>();
|
|
610
|
-
const names = [...namedTypes.keys()].sort(compareCodePoints);
|
|
611
|
-
for (const name of names) {
|
|
612
|
-
if (typeIdentifierOf(name) !== name) continue;
|
|
613
|
-
typeIdentifiers.set(name, name);
|
|
614
|
-
taken.add(name);
|
|
615
|
-
}
|
|
616
|
-
for (const name of names) {
|
|
617
|
-
if (typeIdentifiers.has(name)) continue;
|
|
618
|
-
const base = typeIdentifierOf(name);
|
|
619
|
-
let identifier = base;
|
|
620
|
-
for (let suffix = 2; taken.has(identifier); suffix++) identifier = `${base}_${suffix}`;
|
|
621
|
-
typeIdentifiers.set(name, identifier);
|
|
622
|
-
taken.add(identifier);
|
|
623
|
-
}
|
|
631
|
+
const typeIdentifiers = typeIdentifiersOf(namedTypes.keys());
|
|
624
632
|
/** Catalog types by the name their `$ref`s use. */
|
|
625
633
|
const catalogTypes = new Map<string, ITypeEntry>();
|
|
626
634
|
for (const [name, entry] of namedTypes) {
|
|
@@ -949,11 +957,7 @@ class FlexToolCatalogCore {
|
|
|
949
957
|
|
|
950
958
|
/** The catalog types a declaration's code names; its comments and string literals name none. */
|
|
951
959
|
private scanTypes(text: string, self?: string): string[] {
|
|
952
|
-
|
|
953
|
-
for (const match of codeOf(text).matchAll(identifierScanPattern)) {
|
|
954
|
-
if (match[0] !== self && this.types.has(match[0])) found.add(match[0]);
|
|
955
|
-
}
|
|
956
|
-
return [...found].sort(compareCodePoints);
|
|
960
|
+
return identifiersIn(text).filter((name) => name !== self && this.types.has(name));
|
|
957
961
|
}
|
|
958
962
|
|
|
959
963
|
/** The named types a method's parameters and result use. */
|
|
@@ -1059,6 +1063,26 @@ export class FlexToolCatalog<TScope = unknown> {
|
|
|
1059
1063
|
return typeIdentifierOf(name);
|
|
1060
1064
|
}
|
|
1061
1065
|
|
|
1066
|
+
/**
|
|
1067
|
+
* The identifier each type name of a catalog of exactly these types is declared under: a name that
|
|
1068
|
+
* is an identifier keeps it, and each other takes `identifierOf()`'s, in code-point order with
|
|
1069
|
+
* `_2`, `_3` … appended where that is taken (`a_b` and `a-b` are declared as `a_b` and `a_b_2`).
|
|
1070
|
+
* As a name's identifier depends on the other names, a host that hands a catalog only some of its
|
|
1071
|
+
* types keeps the ones that took the lower identifiers along.
|
|
1072
|
+
*/
|
|
1073
|
+
public static identifiersOf(names: Iterable<string>): Map<string, string> {
|
|
1074
|
+
return typeIdentifiersOf(names);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* The identifiers a declaration's code names, sorted; its comments and the contents of its string
|
|
1079
|
+
* literals name none. The catalog takes the named types a declaration uses from these, so a host
|
|
1080
|
+
* that hands a catalog only some of its types finds the ones a declaration needs the same way.
|
|
1081
|
+
*/
|
|
1082
|
+
public static identifiersIn(declaration: string): string[] {
|
|
1083
|
+
return identifiersIn(declaration);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1062
1086
|
/**
|
|
1063
1087
|
* Prepares `catalog`. Preparing the same object again returns the cached preparation, so a host
|
|
1064
1088
|
* that returns one catalog object for every run pays for it once.
|
|
@@ -1824,6 +1848,10 @@ const codeOf = (text: string): string => {
|
|
|
1824
1848
|
return code;
|
|
1825
1849
|
};
|
|
1826
1850
|
|
|
1851
|
+
/** The identifiers a declaration's code names, sorted; its comments and string literals name none. */
|
|
1852
|
+
const identifiersIn = (text: string): string[] =>
|
|
1853
|
+
[...new Set(codeOf(text).match(identifierScanPattern))].sort(compareCodePoints);
|
|
1854
|
+
|
|
1827
1855
|
/** The index of the bracket that closes the one at `open`, or -1. */
|
|
1828
1856
|
const closingBracket = (code: string, open: number): number => {
|
|
1829
1857
|
const pairs: Record<string, string> = { '(': ')', '<': '>', '{': '}', '[': ']' };
|
package/ts/errors.ts
CHANGED
|
@@ -112,8 +112,9 @@ export class FlexHarnessQueueFullError extends FlexHarnessError {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export class FlexHarnessAbortError extends FlexHarnessError {
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
/** `code` names a more specific stop for a subclass, such as a rejected permission. */
|
|
116
|
+
constructor(message = 'The run was aborted.', options?: ErrorOptions, code = 'FLEX_ABORTED') {
|
|
117
|
+
super(code, message, options);
|
|
117
118
|
this.name = 'FlexHarnessAbortError';
|
|
118
119
|
}
|
|
119
120
|
}
|
|
@@ -143,10 +144,17 @@ export class FlexHarnessExternalError extends FlexHarnessError {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
/**
|
|
148
|
+
* A permission was rejected, by a `reject` response or by the session's permission policy. The
|
|
149
|
+
* rejection is a decision, not a fault: it stops the run like an owner's cancellation.
|
|
150
|
+
*/
|
|
151
|
+
export class FlexHarnessPermissionRejectedError extends FlexHarnessAbortError {
|
|
152
|
+
public readonly permissionId: string;
|
|
153
|
+
|
|
147
154
|
constructor(permissionId: string, options?: ErrorOptions) {
|
|
148
|
-
super(
|
|
155
|
+
super(`Permission "${permissionId}" was rejected.`, options, 'FLEX_PERMISSION_REJECTED');
|
|
149
156
|
this.name = 'FlexHarnessPermissionRejectedError';
|
|
157
|
+
this.permissionId = permissionId;
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -247,6 +255,7 @@ export function errorToInfo(error: unknown): IFlexErrorInfo {
|
|
|
247
255
|
message: error.message,
|
|
248
256
|
code: error.code,
|
|
249
257
|
...(error instanceof FlexHarnessExternalError && error.limit ? { limit: error.limit } : {}),
|
|
258
|
+
...(error instanceof FlexHarnessPermissionRejectedError ? { permissionId: error.permissionId } : {}),
|
|
250
259
|
};
|
|
251
260
|
}
|
|
252
261
|
if (error instanceof Error) {
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
TFlexPromptQueueStatus,
|
|
36
36
|
TFlexPendingReversion,
|
|
37
37
|
TFlexProjectionSnapshot,
|
|
38
|
+
TFlexSessionReversionBarrierCause,
|
|
38
39
|
TFlexSessionReversionGroupKind,
|
|
39
40
|
TFlexTurnReversionFinalizedOutcome,
|
|
40
41
|
} from './interfaces.js';
|
|
@@ -114,6 +115,8 @@ export interface IReversionGroup {
|
|
|
114
115
|
kind: TFlexSessionReversionGroupKind;
|
|
115
116
|
affectedWorkspaces: IFlexAffectedWorkspace[];
|
|
116
117
|
affectedWorkspacesTruncated: boolean;
|
|
118
|
+
barrierCauses: TFlexSessionReversionBarrierCause[];
|
|
119
|
+
reasonCodes: string[];
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
export type TNormalizedCaptureInspection =
|
package/ts/interfaces.ts
CHANGED
|
@@ -174,6 +174,8 @@ export interface IFlexToolMessagePart {
|
|
|
174
174
|
* did not run; its output is the earlier call's.
|
|
175
175
|
*/
|
|
176
176
|
deduplicatedFrom?: string;
|
|
177
|
+
/** For a failed call whose permission was rejected: the rejected request's `permissionId`. */
|
|
178
|
+
rejectedPermissionId?: string;
|
|
177
179
|
model?: IFlexModelIdentity;
|
|
178
180
|
/** ISO time the tool call began. Absent on parts recorded before part times existed. */
|
|
179
181
|
startedAt?: string;
|
|
@@ -216,6 +218,8 @@ export interface IFlexMessage {
|
|
|
216
218
|
error?: string;
|
|
217
219
|
/** The model limit that failed this run, next to its error. */
|
|
218
220
|
limit?: TFlexModelLimit;
|
|
221
|
+
/** The permission whose rejection stopped this run, next to its error. */
|
|
222
|
+
rejectedPermissionId?: string;
|
|
219
223
|
/** On a user message that steered its running run: the caller's steer identity. */
|
|
220
224
|
steerId?: string;
|
|
221
225
|
}
|
|
@@ -228,6 +232,8 @@ export interface IFlexSessionActivity {
|
|
|
228
232
|
error?: string;
|
|
229
233
|
/** The model limit that failed the last run, next to its error. */
|
|
230
234
|
limit?: TFlexModelLimit;
|
|
235
|
+
/** The permission whose rejection stopped the last run, next to its error. */
|
|
236
|
+
rejectedPermissionId?: string;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
239
|
export interface IFlexSession {
|
|
@@ -238,6 +244,13 @@ export interface IFlexSession {
|
|
|
238
244
|
/** Opaque immutable configuration revision bound to this exact session generation. */
|
|
239
245
|
configurationRef?: string;
|
|
240
246
|
title?: string;
|
|
247
|
+
/**
|
|
248
|
+
* The session's first prompt as one line, bounded like a title (`maxTitleBytes`), so a session
|
|
249
|
+
* nobody titled can still be named. The harness sets it when the first run with prompt text is
|
|
250
|
+
* admitted, and for a session stored before previews existed at its next run; it never changes
|
|
251
|
+
* afterwards and is never set by a caller. Absent until then.
|
|
252
|
+
*/
|
|
253
|
+
preview?: string;
|
|
241
254
|
createdAt: string;
|
|
242
255
|
updatedAt: string;
|
|
243
256
|
archivedAt?: string;
|
|
@@ -1696,12 +1709,30 @@ export type TFlexReversionPolicy = 'transcript-optional' | 'workspace-required';
|
|
|
1696
1709
|
|
|
1697
1710
|
export type TFlexSessionReversionGroupKind = 'candidate' | 'barrier' | 'no-change';
|
|
1698
1711
|
|
|
1712
|
+
/**
|
|
1713
|
+
* Why a group is a barrier: `pending` while a capture runs or its outcome is not final,
|
|
1714
|
+
* `nonrevertible` for a workspace outcome that cannot be reverted, `transcript-only` for legacy
|
|
1715
|
+
* transcript history without a workspace capture.
|
|
1716
|
+
*/
|
|
1717
|
+
export type TFlexSessionReversionBarrierCause = 'pending' | 'nonrevertible' | 'transcript-only';
|
|
1718
|
+
|
|
1719
|
+
/** The barrier causes in the order a group lists them. */
|
|
1720
|
+
export const FLEX_SESSION_REVERSION_BARRIER_CAUSES: readonly TFlexSessionReversionBarrierCause[] = [
|
|
1721
|
+
'pending',
|
|
1722
|
+
'nonrevertible',
|
|
1723
|
+
'transcript-only',
|
|
1724
|
+
];
|
|
1725
|
+
|
|
1699
1726
|
export interface IFlexSessionReversionGroup {
|
|
1700
1727
|
readonly runId: string;
|
|
1701
1728
|
readonly kind: TFlexSessionReversionGroupKind;
|
|
1702
1729
|
readonly visibility: 'visible' | 'hidden';
|
|
1703
1730
|
readonly affectedWorkspaces: readonly IFlexAffectedWorkspace[];
|
|
1704
1731
|
readonly affectedWorkspacesTruncated: boolean;
|
|
1732
|
+
/** Each cause of a barrier once, in the order of the type; empty for every other kind. */
|
|
1733
|
+
readonly barrierCauses: readonly TFlexSessionReversionBarrierCause[];
|
|
1734
|
+
/** The distinct reason codes of the group's nonrevertible segments, in segment order. */
|
|
1735
|
+
readonly reasonCodes: readonly string[];
|
|
1705
1736
|
}
|
|
1706
1737
|
|
|
1707
1738
|
export interface IFlexSessionReversionInfo {
|
|
@@ -2128,6 +2159,8 @@ export interface IFlexErrorInfo {
|
|
|
2128
2159
|
code?: string;
|
|
2129
2160
|
/** Set when a model usage or rate limit caused the error. */
|
|
2130
2161
|
limit?: TFlexModelLimit;
|
|
2162
|
+
/** Set when a rejected permission stopped the run: the rejected request's `permissionId`. */
|
|
2163
|
+
permissionId?: string;
|
|
2131
2164
|
}
|
|
2132
2165
|
|
|
2133
2166
|
export type TFlexHarnessEvent =
|
package/ts/readme.md
CHANGED
|
@@ -693,7 +693,7 @@ await harness.dispose();
|
|
|
693
693
|
|
|
694
694
|
Only one run may be active in a session. Additional prompts enter a bounded FIFO owned by FlexHarness, while different sessions can run concurrently. `enqueuePrompt()` resolves with `{ queueId, completion }` after the immutable prompt and options have been accepted into that runtime queue and `prompt.queued` has been emitted. `startPrompt()` keeps its durable-admission behavior: it waits for its FIFO turn and resolves with `{ queueId, runId, completion }` only after the canonical generation claim, run ID, and initial public audit messages have been durably reserved and the corresponding start events have been emitted. `prompt()` preserves the simpler behavior by awaiting completion internally.
|
|
695
695
|
|
|
696
|
-
Queue entries expose `queued`, `starting`, `scheduled`, `running`, `completed`, `failed`, and `cancelled` status through `getPromptQueueEntry()` and `listPromptQueueEntries()`. The list is ordered by process-local `queueSequence`. `getPromptQueueEntry()` throws `FlexHarnessNotFoundError` for an unknown or evicted ID. `cancelPrompt()` cancels one exact queue ID: a waiting entry leaves the FIFO and releases capacity immediately but remains queryable as `cancelled` until terminal retention evicts it; a promoted entry uses the canonical run cancellation path. Cancelling a terminal entry returns `false`, while an unknown ID throws. `abort()` remains scoped to the currently active run.
|
|
696
|
+
Queue entries expose `queued`, `starting`, `scheduled`, `running`, `completed`, `failed`, and `cancelled` status through `getPromptQueueEntry()` and `listPromptQueueEntries()`. The list is ordered by process-local `queueSequence`. `getPromptQueueEntry()` throws `FlexHarnessNotFoundError` for an unknown or evicted ID. `cancelPrompt()` cancels one exact queue ID: a waiting entry leaves the FIFO and releases capacity immediately but remains queryable as `cancelled` until terminal retention evicts it; a promoted entry uses the canonical run cancellation path and is stopped with the same `FlexHarnessAbortError` ("The run was aborted.") as `abort()`; only a waiting entry is rejected with "The queued prompt was cancelled.". A cancelled run, whether stopped or found interrupted on reload, puts its `error` on the assistant message only; its user message is `cancelled` without an `error`, while a failed run marks both messages. Cancelling a terminal entry returns `false`, while an unknown ID throws. `abort()` remains scoped to the currently active run.
|
|
697
697
|
|
|
698
698
|
The displayed queue limits are the defaults. Outstanding count and byte limits apply per session and include every non-terminal queued or active prompt until it settles. Pending-admission limits apply to the complete harness while scope aliases are unresolved. Terminal retention applies per session. Exceeding an admission limit throws `FlexHarnessQueueFullError`.
|
|
699
699
|
|
|
@@ -705,7 +705,7 @@ The reservation save is the admission point. A save failure produces no start ev
|
|
|
705
705
|
|
|
706
706
|
`listMessagePage()` returns the newest contiguous page in chronological order. `limit` must be an integer from 1 through 50 and defaults to 50. `nextCursor` is opaque, limited to 4096 UTF-8 bytes, bound to the resolved storage namespace and session, and remains stable when newer messages are appended. Mismatched and stale cursors fail validation. Each page carries `startIndex`, the zero-based index of its first message in the session's visible history (for an empty page, the index the page ends at), so a consumer can order and page by absolute position. Appending messages leaves every index unchanged; a branch after an undo reuses the positions of the history it replaces, so a consumer re-reads its pages after `session.history.changed`. `getMessage()` performs an exact lookup. Transfer identifiers are limited to 512 bytes, text and reasoning parts to 96 KiB, complete messages to 480 KiB, and complete page envelopes to 512 KiB. A page may therefore contain fewer messages than requested. Oversized text is truncated and an otherwise oversized parts collection is replaced with an explicit elision marker; metadata that still cannot fit fails validation. Canonical private Agent events are unchanged.
|
|
707
707
|
|
|
708
|
-
`updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Title-only updates remain available while prompts are queued or running, while permission is pending, and after archival. Requests containing `archived` are rejected while the session has any outstanding prompt or pending permission; a mixed title-and-archive request is rejected atomically without changing the title. Archived sessions expose `archivedAt`. Deleting a session cascades through its complete descendant subtree. One durable root-keyed tombstone group hides every newly affected live session, and the delete also joins any already-separate descendant cleanup groups without rewriting their roots. FlexHarness then cancels queued and active subtree work, emits terminal queue events, waits for admitted initialization, and purges runtime queue status while cleaning runtime and persisted domains child-first. The requested root tombstone is removed last after every domain confirms cleanup; project-management cleanup confirmation is a retained durable project tombstone rather than physical removal. An imported session can hold no project-management state, so its deletion writes no project tombstone. A successful live `deleteSession()` call emits `session.deleted` for each session it newly tombstoned; retries of an existing tombstone and automatic load, retirement, or disposal cleanup emit no deletion events. Direct deletion of a descendant cascades only through that descendant's subtree. Cleanup authority follows the resolved storage namespace, so scope aliases share the same groups. A partial failure retains durable ownership for retry by a later `deleteSession()`, namespace load, `retireScope()`, or `dispose()` call.
|
|
708
|
+
`updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Title-only updates remain available while prompts are queued or running, while permission is pending, and after archival. Requests containing `archived` are rejected while the session has any outstanding prompt or pending permission; a mixed title-and-archive request is rejected atomically without changing the title. Archived sessions expose `archivedAt`. Every session exposes `preview` once a run with prompt text was admitted: its first prompt as one line (whitespace runs collapsed, cut at a character boundary to `maxTitleBytes`), with no model call, so a host can name a session nobody titled (`title || preview`). It is set once, a session stored before previews existed takes it from its first prompt at its next run, and no caller sets or clears it. Deleting a session cascades through its complete descendant subtree. One durable root-keyed tombstone group hides every newly affected live session, and the delete also joins any already-separate descendant cleanup groups without rewriting their roots. FlexHarness then cancels queued and active subtree work, emits terminal queue events, waits for admitted initialization, and purges runtime queue status while cleaning runtime and persisted domains child-first. The requested root tombstone is removed last after every domain confirms cleanup; project-management cleanup confirmation is a retained durable project tombstone rather than physical removal. An imported session can hold no project-management state, so its deletion writes no project tombstone. A successful live `deleteSession()` call emits `session.deleted` for each session it newly tombstoned; retries of an existing tombstone and automatic load, retirement, or disposal cleanup emit no deletion events. Direct deletion of a descendant cascades only through that descendant's subtree. Cleanup authority follows the resolved storage namespace, so scope aliases share the same groups. A partial failure retains durable ownership for retry by a later `deleteSession()`, namespace load, `retireScope()`, or `dispose()` call.
|
|
709
709
|
|
|
710
710
|
`deleteSessionGenerationCohort()` is the generation-fenced destructive form for controllers. Its `root` and every `authorizedCohort` entry use the exact `{ sessionId, sessionGenerationId, sessionGenerationSequence }` shape. The cohort contains unique entries in strictly ascending `sessionId` order and is limited by `FLEX_SESSION_GENERATION_COHORT_MAX_ENTRIES` (2048). FlexHarness atomically validates the matching root, its complete live subtree, and every retained descendant tombstone group before reserving deletion. A missing or different root generation returns `{ matched: false }` without touching a newer generation; a missing or mismatched cascade entry throws before new tombstoning or cleanup. Extra valid entries do not expand the cascade. A matched partial cleanup remains retryable with the same authority and resolves to `{ matched: true }` when cleanup completes. The existing `deleteSession()` method remains available for callers that intentionally own the complete reusable-ID namespace.
|
|
711
711
|
|
|
@@ -872,11 +872,13 @@ for (const group of info.groups) {
|
|
|
872
872
|
group.visibility,
|
|
873
873
|
group.affectedWorkspaces,
|
|
874
874
|
group.affectedWorkspacesTruncated,
|
|
875
|
+
group.barrierCauses,
|
|
876
|
+
group.reasonCodes,
|
|
875
877
|
);
|
|
876
878
|
}
|
|
877
879
|
```
|
|
878
880
|
|
|
879
|
-
The immutable result exposes `undoAvailable`, `redoAvailable`, and groups classified as `candidate`, `barrier`, or `no-change`. Group metadata contains at most 64 unique affected workspaces; `affectedWorkspacesTruncated` is `true` when additional unique descriptors were omitted. It never includes capture IDs or provider references.
|
|
881
|
+
The immutable result exposes `undoAvailable`, `redoAvailable`, and groups classified as `candidate`, `barrier`, or `no-change`. Group metadata contains at most 64 unique affected workspaces; `affectedWorkspacesTruncated` is `true` when additional unique descriptors were omitted. `barrierCauses` states why a barrier blocks traversal, each cause once in this order: `pending` while a capture runs or its outcome is not final, `nonrevertible` for a workspace outcome that cannot be reverted, `transcript-only` for legacy transcript history without a workspace capture; it is empty for candidate and no-change groups (`FLEX_SESSION_REVERSION_BARRIER_CAUSES` lists the order). `reasonCodes` lists the distinct `reasonCode`s of the group's nonrevertible segments in segment order, so an application can say why a turn is not revertible even when its provider named no affected workspace. It never includes capture IDs or provider references.
|
|
880
882
|
|
|
881
883
|
`workspaceSnapshots` is application-owned. Every context contains `scopeId`, `scope`, `storageKey`, `sessionId`, the required canonical `sessionGenerationId` and `sessionGenerationSequence`, `runId`, deterministic `captureId`, and an `AbortSignal`. Apply contexts additionally contain the normalized `reference`, per-segment `operationId`, and `direction`; release contexts contain the reference. Deleted-session recovery retains the same generation from the durable tombstone, so providers can reject callbacks from an older same-ID generation.
|
|
882
884
|
|
|
@@ -923,7 +925,7 @@ Resolved model identity contains provider and model IDs plus optional display na
|
|
|
923
925
|
|
|
924
926
|
Public audit history is safe to send to controllers: attachment parts contain source and size metadata, never inline base64, data URLs, or remote URL payloads. Canonical private Agent events retain those values solely for subsequent model turns.
|
|
925
927
|
|
|
926
|
-
Sessions expose `idle`, `scheduled`, `running`, `waiting_permission`, `failed`, and `cancelled` status. Persisted non-terminal activity is repaired from canonical Agent generation outcomes after process restart; incomplete messages and parts normalize to `cancelled` unless an accepted hidden terminal stage can be promoted.
|
|
928
|
+
Sessions expose `idle`, `scheduled`, `running`, `waiting_permission`, `failed`, and `cancelled` status. A run stopped by `abort()`, `cancelPrompt()` or a rejected permission leaves its session `cancelled`; only a run that failed leaves it `failed`. Persisted non-terminal activity is repaired from canonical Agent generation outcomes after process restart; incomplete messages and parts normalize to `cancelled` unless an accepted hidden terminal stage can be promoted.
|
|
927
929
|
|
|
928
930
|
## Conversation Import
|
|
929
931
|
|
|
@@ -1023,6 +1025,8 @@ Pending requests are runtime-only and queryable with `listPendingPermissions()`.
|
|
|
1023
1025
|
|
|
1024
1026
|
`always` is invalid when the request has no `rememberKey`. Remembered decisions are persisted before the waiting tool resolves. If persistence fails, the key is rolled back and the request remains pending so the response can be retried. Concurrent response attempts are serialized and exactly one successful response settles a request.
|
|
1025
1027
|
|
|
1028
|
+
`reject` stops the run as a decision, not as a failure. The waiting tool call fails with `FlexHarnessPermissionRejectedError`, a `FlexHarnessAbortError` with code `FLEX_PERMISSION_REJECTED` and the rejected `permissionId`, and the run ends the way `abort()` or `cancelPrompt()` ends it; the model is not asked to recover. The run's completion rejects with that error. `run.finished` and the finished queue entry have status `cancelled` and an `error` whose `permissionId` names the rejected request. The run's terminal user and assistant messages are `cancelled`; the assistant message carries `rejectedPermissionId` next to its `error`, and so does the session's `activity`; the session's status is `cancelled`, as after a stop. The refused tool part fails with the rejection as its error and carries the same `rejectedPermissionId`. Other permissions still pending in the run are rejected with it, and runs it delegated stop with "The parent run was aborted.". As with a stop, a run rejected after its model produced output stays in future context, where the refused call's result says the permission was rejected. A permission policy `deny` ends the run the same way.
|
|
1029
|
+
|
|
1026
1030
|
## Tool Output Safety
|
|
1027
1031
|
|
|
1028
1032
|
FlexHarness wraps every provided tool `execute` method before AgentSession receives it. Direct outputs and every `AsyncIterable` yield are converted into bounded JSON-safe values. Circular references, functions, symbols, bigint values, dates, URLs, and binary values receive deterministic descriptions or records. Returned error objects and unreadable getter values receive fixed descriptions without their original messages. Thrown errors and iterator failures remain failures but are converted to the safe external-error projection before AgentSession observes them.
|
package/ts/utils.flexharness.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import {
|
|
3
3
|
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessPermissionRejectedError,
|
|
4
5
|
FlexHarnessRunError,
|
|
5
6
|
FlexHarnessValidationError,
|
|
6
7
|
} from './errors.js';
|
|
@@ -106,14 +107,27 @@ export function combineErrors(errors: unknown[]): unknown {
|
|
|
106
107
|
return Object.freeze(combined);
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
/** The error
|
|
110
|
-
export function terminalActivityError(
|
|
110
|
+
/** The error, model limit and rejected permission a finished run leaves on its session activity. */
|
|
111
|
+
export function terminalActivityError(
|
|
112
|
+
message: IFlexMessage,
|
|
113
|
+
): Pick<IFlexSessionActivity, 'error' | 'limit' | 'rejectedPermissionId'> {
|
|
111
114
|
return {
|
|
112
115
|
...(message.error ? { error: message.error } : {}),
|
|
113
116
|
...(message.limit ? { limit: message.limit } : {}),
|
|
117
|
+
...(message.rejectedPermissionId ? { rejectedPermissionId: message.rejectedPermissionId } : {}),
|
|
114
118
|
};
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The parent run's cancellation when the runs it delegated share it: a stop stops them alike, but a
|
|
123
|
+
* permission the parent rejected is the parent's own.
|
|
124
|
+
*/
|
|
125
|
+
export function delegatedCancellation(reason: unknown): FlexHarnessAbortError | undefined {
|
|
126
|
+
return reason instanceof FlexHarnessAbortError && !(reason instanceof FlexHarnessPermissionRejectedError)
|
|
127
|
+
? reason
|
|
128
|
+
: undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
117
131
|
export function isAbortError(error: unknown): boolean {
|
|
118
132
|
return error instanceof FlexHarnessAbortError
|
|
119
133
|
|| (error instanceof DOMException && error.name === 'AbortError')
|
package/ts/utils.json.ts
CHANGED
|
@@ -533,6 +533,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
533
533
|
'sessionGenerationSequence',
|
|
534
534
|
'configurationRef',
|
|
535
535
|
'title',
|
|
536
|
+
'preview',
|
|
536
537
|
'createdAt',
|
|
537
538
|
'updatedAt',
|
|
538
539
|
'archivedAt',
|
|
@@ -555,6 +556,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
555
556
|
throw new FlexHarnessStoreFormatError(`${path}.configurationRef is invalid.`);
|
|
556
557
|
}
|
|
557
558
|
requireOptionalString(session.title, `${path}.title`);
|
|
559
|
+
requireOptionalString(session.preview, `${path}.preview`);
|
|
558
560
|
requireString(session.createdAt, `${path}.createdAt`);
|
|
559
561
|
requireString(session.updatedAt, `${path}.updatedAt`);
|
|
560
562
|
requireOptionalString(session.archivedAt, `${path}.archivedAt`);
|
|
@@ -562,7 +564,11 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
562
564
|
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
563
565
|
}
|
|
564
566
|
const activity = requireRecord(session.activity, `${path}.activity`);
|
|
565
|
-
requireOnlyKeys(
|
|
567
|
+
requireOnlyKeys(
|
|
568
|
+
activity,
|
|
569
|
+
['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit', 'rejectedPermissionId'],
|
|
570
|
+
`${path}.activity`,
|
|
571
|
+
);
|
|
566
572
|
if (!activityStatuses.includes(activity.status as (typeof activityStatuses)[number])) {
|
|
567
573
|
throw new FlexHarnessStoreFormatError(`${path}.activity.status is invalid.`);
|
|
568
574
|
}
|
|
@@ -571,6 +577,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
571
577
|
requireOptionalString(activity.completedAt, `${path}.activity.completedAt`);
|
|
572
578
|
requireOptionalString(activity.error, `${path}.activity.error`);
|
|
573
579
|
requireOptionalModelLimit(activity.limit, `${path}.activity.limit`);
|
|
580
|
+
requireOptionalString(activity.rejectedPermissionId, `${path}.activity.rejectedPermissionId`);
|
|
574
581
|
const relationship = [
|
|
575
582
|
session.parentSessionId,
|
|
576
583
|
session.parentRunId,
|
|
@@ -767,6 +774,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
767
774
|
'usage',
|
|
768
775
|
'error',
|
|
769
776
|
'limit',
|
|
777
|
+
'rejectedPermissionId',
|
|
770
778
|
'steerId',
|
|
771
779
|
],
|
|
772
780
|
path,
|
|
@@ -784,6 +792,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
784
792
|
requireOptionalString(message.completedAt, `${path}.completedAt`);
|
|
785
793
|
requireOptionalString(message.error, `${path}.error`);
|
|
786
794
|
requireOptionalModelLimit(message.limit, `${path}.limit`);
|
|
795
|
+
requireOptionalString(message.rejectedPermissionId, `${path}.rejectedPermissionId`);
|
|
787
796
|
if (message.steerId !== undefined) {
|
|
788
797
|
requireString(message.steerId, `${path}.steerId`);
|
|
789
798
|
if (message.role !== 'user') {
|
|
@@ -834,6 +843,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
834
843
|
'childSessionId',
|
|
835
844
|
'parentToolCallId',
|
|
836
845
|
'deduplicatedFrom',
|
|
846
|
+
'rejectedPermissionId',
|
|
837
847
|
'model',
|
|
838
848
|
'startedAt',
|
|
839
849
|
'completedAt',
|
|
@@ -852,6 +862,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
852
862
|
requireOptionalString(part.childSessionId, `${partPath}.childSessionId`);
|
|
853
863
|
requireOptionalString(part.parentToolCallId, `${partPath}.parentToolCallId`);
|
|
854
864
|
requireOptionalString(part.deduplicatedFrom, `${partPath}.deduplicatedFrom`);
|
|
865
|
+
requireOptionalString(part.rejectedPermissionId, `${partPath}.rejectedPermissionId`);
|
|
855
866
|
if (part.model !== undefined) validateModel(part.model, `${partPath}.model`);
|
|
856
867
|
requireOptionalString(part.startedAt, `${partPath}.startedAt`);
|
|
857
868
|
requireOptionalString(part.completedAt, `${partPath}.completedAt`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FLEX_PROJECT_MANAGEMENT_LIMITS, type IFlexMessage } from './interfaces.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a session is about before anyone titles it: the text of its first prompt that has any, as
|
|
5
|
+
* one line (every run of whitespace becomes one space), cut to the title bound at a character
|
|
6
|
+
* boundary. Undefined while no prompt carries text.
|
|
7
|
+
*/
|
|
8
|
+
export function sessionPreview(messagesArg: readonly IFlexMessage[]): string | undefined {
|
|
9
|
+
for (const message of messagesArg) {
|
|
10
|
+
if (message.role !== 'user') continue;
|
|
11
|
+
const text = message.parts
|
|
12
|
+
.map((part) => (part.type === 'text' ? part.text : ''))
|
|
13
|
+
.join(' ')
|
|
14
|
+
.replace(/\s+/gu, ' ')
|
|
15
|
+
.trim();
|
|
16
|
+
if (text) return utf8Prefix(text, FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The longest prefix of whole characters within `maxBytesArg` UTF-8 bytes. */
|
|
22
|
+
function utf8Prefix(valueArg: string, maxBytesArg: number): string {
|
|
23
|
+
if (Buffer.byteLength(valueArg, 'utf8') <= maxBytesArg) return valueArg;
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
let end = 0;
|
|
26
|
+
for (const character of valueArg) {
|
|
27
|
+
bytes += Buffer.byteLength(character, 'utf8');
|
|
28
|
+
if (bytes > maxBytesArg) break;
|
|
29
|
+
end += character.length;
|
|
30
|
+
}
|
|
31
|
+
return valueArg.slice(0, end).trimEnd();
|
|
32
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
IFlexTypedRequestSurface,
|
|
7
7
|
} from './interfaces.js';
|
|
8
8
|
import { normalizeOmissionPaths, withoutRequestPaths, withRequestPaths } from './omissions.js';
|
|
9
|
+
import { typesReachedBy } from './references.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A typed request the router refused: the `text` and `data` of its error, as a client of the API
|
|
@@ -31,7 +32,9 @@ export class FlexTypedRequestError extends Error {
|
|
|
31
32
|
* `router.routeAndAddResponse()` in process, its request what `params` makes of the model's
|
|
32
33
|
* parameters with the omitted properties its request has as `request` fills them, and its result
|
|
33
34
|
* what `response` makes of the answer. A refusal rejects with `FlexTypedRequestError`. A method
|
|
34
|
-
* `readOnly` marks is declared read-only, including when `onCall` decides its calls.
|
|
35
|
+
* `readOnly` marks is declared read-only, including when `onCall` decides its calls. The catalog's
|
|
36
|
+
* types are the surface's types the included methods reach: one only methods left out use is
|
|
37
|
+
* neither declared nor described.
|
|
35
38
|
*/
|
|
36
39
|
export function catalogFromTypedRequests<TScope = unknown>(
|
|
37
40
|
surface: IFlexTypedRequestSurface,
|
|
@@ -108,8 +111,9 @@ export function catalogFromTypedRequests<TScope = unknown>(
|
|
|
108
111
|
? await options.response(method, answer.response as Record<string, unknown>, context)
|
|
109
112
|
: answer.response;
|
|
110
113
|
};
|
|
114
|
+
const included = surface.methods.filter((method) => include(method));
|
|
111
115
|
return {
|
|
112
|
-
methods:
|
|
116
|
+
methods: included.map((method): IFlexCatalogMethod<TScope> => ({
|
|
113
117
|
method: method.path,
|
|
114
118
|
summary: method.summary,
|
|
115
119
|
...(method.description ? { description: method.description } : {}),
|
|
@@ -123,7 +127,7 @@ export function catalogFromTypedRequests<TScope = unknown>(
|
|
|
123
127
|
? { onCall: (params: Record<string, unknown>, context: IFlexCatalogRunContext<TScope>) => options.onCall!(method, params, context) }
|
|
124
128
|
: {}),
|
|
125
129
|
})),
|
|
126
|
-
types: surface.types,
|
|
130
|
+
types: typesReachedBy(included, surface.types),
|
|
127
131
|
...(options.vocabulary ? { vocabulary: options.vocabulary } : {}),
|
|
128
132
|
...(options.onProposals ? { onProposals: options.onProposals } : {}),
|
|
129
133
|
};
|
|
@@ -7,14 +7,11 @@ import type {
|
|
|
7
7
|
TFlexTypedRequestSchema,
|
|
8
8
|
} from './interfaces.js';
|
|
9
9
|
import { normalizeOmissionPaths } from './omissions.js';
|
|
10
|
+
import { definitionsPrefix, isRecord, referencedType, referencesIn } from './references.js';
|
|
10
11
|
|
|
11
12
|
const componentsPrefix = '#/components/schemas/';
|
|
12
|
-
const definitionsPrefix = '#/$defs/';
|
|
13
13
|
const maxSummaryCharacters = 120;
|
|
14
14
|
|
|
15
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
16
|
-
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
-
|
|
18
15
|
/** A schema whose references name `#/$defs/<type>` instead of the OpenAPI components. */
|
|
19
16
|
const withDefinitionRefs = (value: unknown): unknown => {
|
|
20
17
|
if (Array.isArray(value)) return value.map(withDefinitionRefs);
|
|
@@ -27,12 +24,6 @@ const withDefinitionRefs = (value: unknown): unknown => {
|
|
|
27
24
|
]));
|
|
28
25
|
};
|
|
29
26
|
|
|
30
|
-
/** The type a `#/$defs/<type>` reference names. */
|
|
31
|
-
const referencedType = (schema: TFlexTypedRequestSchema): string | undefined =>
|
|
32
|
-
typeof schema.$ref === 'string' && schema.$ref.startsWith(definitionsPrefix)
|
|
33
|
-
? schema.$ref.slice(definitionsPrefix.length)
|
|
34
|
-
: undefined;
|
|
35
|
-
|
|
36
27
|
/**
|
|
37
28
|
* A request schema without the omitted properties: an object loses them, and a reference to a
|
|
38
29
|
* named type that holds one becomes the type's own schema without them.
|
|
@@ -167,18 +158,6 @@ const requiresNothing = (
|
|
|
167
158
|
|| schema.allOf.every((member) => !isRecord(member) || requiresNothing(member, types));
|
|
168
159
|
};
|
|
169
160
|
|
|
170
|
-
/** The named types a schema refers to. */
|
|
171
|
-
const referencesIn = (value: unknown, found: Set<string>): Set<string> => {
|
|
172
|
-
if (Array.isArray(value)) {
|
|
173
|
-
for (const entry of value) referencesIn(entry, found);
|
|
174
|
-
} else if (isRecord(value)) {
|
|
175
|
-
const target = referencedType(value);
|
|
176
|
-
if (target !== undefined) found.add(target);
|
|
177
|
-
for (const child of Object.values(value)) referencesIn(child, found);
|
|
178
|
-
}
|
|
179
|
-
return found;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
161
|
/** The first sentence of a contract's JSDoc, its tags left out, within the summary's length. */
|
|
183
162
|
const summaryOf = (documentation: string | undefined, method: string): string => {
|
|
184
163
|
const prose = (documentation ?? '').split(/\n(?=@)/)[0].replace(/\s+/g, ' ').trim();
|
|
@@ -125,7 +125,10 @@ export interface IFlexTypedRequestCatalogOptions<TScope = unknown> {
|
|
|
125
125
|
method: IFlexTypedRequestMethod,
|
|
126
126
|
context: IFlexCatalogRunContext<TScope>,
|
|
127
127
|
): Record<string, unknown> | PromiseLike<Record<string, unknown>>;
|
|
128
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* The methods the model may use. Default: every method that is not deprecated. The catalog keeps
|
|
130
|
+
* only the named types these methods reach.
|
|
131
|
+
*/
|
|
129
132
|
include?(method: IFlexTypedRequestMethod): boolean;
|
|
130
133
|
/**
|
|
131
134
|
* The methods that only read: a call has no effect but its result. Such a method is declared
|
|
@@ -41,7 +41,7 @@ The surface is plain JSON, so a build step can write it next to the server and t
|
|
|
41
41
|
- `request` fills the omitted properties. A call takes from what it returns only the omitted properties its method's request has, so one answer serves every method (`{ identity, csrfToken }`), and a method whose request has none is routed without asking it. A value the model passes for any omitted property never reaches the router.
|
|
42
42
|
- Method-specific fills copy only the selected leaves, retaining public siblings. Array wildcards fill existing elements at corresponding indexes without adding or removing elements. An absent object parent can be created for a supplied leaf; an explicit null or scalar parent is refused. Input objects are not mutated. A surface with additional paths requires `request`; a serialized global-only surface without `omittedPaths` remains valid.
|
|
43
43
|
- A refusal rejects with `FlexTypedRequestError`: the typed method, the error's `text` and `data`, as a client of the API would receive them. A handler's own failure arrives as the router's internal error. A host's `externalErrorProjector` decides what of it the model sees.
|
|
44
|
-
- `include` picks the methods the model may use, by default every method that is not deprecated
|
|
44
|
+
- `include` picks the methods the model may use, by default every method that is not deprecated, and the catalog's types are the surface's types those methods reach, through their schemas' `$ref`s and the names their declarations' code uses: a type only methods left out use is neither declared nor described, unless it holds a lower identifier of a kept type's name, which keeps that type's identifier as the whole surface declares it (`a_b` stays for `a-b`, declared `a_b_2`; `FlexToolCatalog.identifiersOf()`). `aliases` gives methods other names by typed method; `vocabulary`, `onCall` and `onProposals` are the catalog's own (`onCall` receives the typed method first).
|
|
45
45
|
- `readOnly` marks the methods that only read, including their policy hook. A marked method is declared `readOnly` and its `onCall` still decides every validated call, so reads run concurrently, may start while the model still streams, and a turn answers identical ones once after each call's hook allows it; every other method's calls, gated by `onCall` or not, run in the order the model made them. A policy that proposes changes must return `run` for the reads it allows, rather than relying on reads skipping the hook. Mark nothing that asks for permission.
|
|
46
46
|
- `params` makes the request a call routes of the model's validated parameters, such as the parameters with the short names the model was shown for records turned back into their IDs; by default the parameters themselves. It runs when the call runs, after `onCall` decided it where the method has one, so `onCall` and a proposal see the model's parameters. The omitted properties are dropped from what it returns as from the model's, and only `request` fills them. A throw refuses the call with its error, and so does a result that is no object.
|
|
47
47
|
- The same validation, policy, stripping and filling apply to `execute` and every call inside `run_code`. Private inputs are rejected before `onCall`; a proposed change does not run `params`, `request` or the router. Omissions cover inputs only: use `response` to remove secrets from answers before they reach the model.
|