@modelprofile.com/flexharness 14.0.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.cachewarm.d.ts +2 -2
- package/dist_ts/classes.flexharness.cachewarm.js +1 -1
- package/dist_ts/classes.flexharness.core.d.ts +8 -1
- package/dist_ts/classes.flexharness.core.js +44 -31
- package/dist_ts/classes.flexharness.d.ts +5 -4
- package/dist_ts/classes.flexharness.js +10 -7
- package/dist_ts/classes.flexharness.prompts.d.ts +9 -5
- package/dist_ts/classes.flexharness.prompts.js +93 -18
- package/dist_ts/classes.flexharness.runs.d.ts +9 -4
- package/dist_ts/classes.flexharness.runs.js +70 -18
- package/dist_ts/classes.flexharness.sessions.d.ts +4 -5
- package/dist_ts/classes.flexharness.sessions.js +45 -12
- package/dist_ts/classes.flexharness.subagents.js +1 -1
- package/dist_ts/errors.d.ts +9 -0
- package/dist_ts/errors.js +14 -1
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +119 -11
- package/dist_ts/interfaces.flexharness.d.ts +12 -8
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.harnessoptions.js +6 -3
- package/dist_ts/utils.json.js +74 -18
- package/dist_ts/utils.sessionmetrics.d.ts +12 -1
- package/dist_ts/utils.sessionmetrics.js +18 -1
- package/dist_ts/utils.transfer.js +14 -1
- package/dist_ts/utils.validation.d.ts +7 -2
- package/dist_ts/utils.validation.js +28 -7
- package/package.json +3 -3
- package/readme.md +75 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.cachewarm.ts +2 -2
- package/ts/classes.flexharness.core.ts +49 -30
- package/ts/classes.flexharness.prompts.ts +124 -39
- package/ts/classes.flexharness.runs.ts +78 -24
- package/ts/classes.flexharness.sessions.ts +54 -9
- package/ts/classes.flexharness.subagents.ts +2 -1
- package/ts/classes.flexharness.ts +25 -5
- package/ts/errors.ts +15 -0
- package/ts/index.ts +6 -0
- package/ts/interfaces.flexharness.ts +12 -8
- package/ts/interfaces.ts +125 -11
- package/ts/plugins.ts +13 -0
- package/ts/readme.md +75 -6
- package/ts/utils.harnessoptions.ts +9 -2
- package/ts/utils.json.ts +75 -18
- package/ts/utils.sessionmetrics.ts +23 -0
- package/ts/utils.transfer.ts +13 -0
- package/ts/utils.validation.ts +31 -5
package/ts/utils.json.ts
CHANGED
|
@@ -473,6 +473,12 @@ function requireNonNegativeInteger(value: unknown, path: string): void {
|
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
function requirePositiveInteger(value: unknown, path: string): void {
|
|
477
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1) {
|
|
478
|
+
throw new FlexHarnessStoreFormatError(`${path} must be a positive integer.`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
476
482
|
function requireStringArray(value: unknown, path: string): string[] {
|
|
477
483
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
478
484
|
throw new FlexHarnessStoreFormatError(`${path} must be an array of strings.`);
|
|
@@ -545,10 +551,18 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
545
551
|
'agent',
|
|
546
552
|
'depth',
|
|
547
553
|
'origin',
|
|
554
|
+
'contextCompaction',
|
|
548
555
|
],
|
|
549
556
|
path,
|
|
550
557
|
);
|
|
551
558
|
requireString(session.scopeId, `${path}.scopeId`);
|
|
559
|
+
if (session.contextCompaction !== undefined) {
|
|
560
|
+
try {
|
|
561
|
+
plugins.assertAgentContextCompactionSetting(session.contextCompaction, `${path}.contextCompaction`);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
throw new FlexHarnessStoreFormatError(error instanceof Error ? error.message : String(error));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
552
566
|
if (session.origin !== undefined) validateSessionOrigin(session.origin, `${path}.origin`);
|
|
553
567
|
const sessionId = requireString(session.sessionId, `${path}.sessionId`);
|
|
554
568
|
const generation = validateOptionalSessionGeneration(session, path);
|
|
@@ -748,6 +762,7 @@ interface IValidatedMessageIdentity {
|
|
|
748
762
|
runId: string;
|
|
749
763
|
role: 'user' | 'assistant';
|
|
750
764
|
status: 'streaming' | 'completed' | 'failed' | 'cancelled';
|
|
765
|
+
steerId?: string;
|
|
751
766
|
}
|
|
752
767
|
|
|
753
768
|
interface IValidatedSessionIdentity {
|
|
@@ -781,6 +796,8 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
781
796
|
'limit',
|
|
782
797
|
'rejectedPermissionId',
|
|
783
798
|
'steerId',
|
|
799
|
+
'promptId',
|
|
800
|
+
'keptInContext',
|
|
784
801
|
'cacheWarm',
|
|
785
802
|
],
|
|
786
803
|
path,
|
|
@@ -805,6 +822,20 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
805
822
|
throw new FlexHarnessStoreFormatError(`${path}.steerId is only valid on a user message.`);
|
|
806
823
|
}
|
|
807
824
|
}
|
|
825
|
+
if (message.promptId !== undefined) {
|
|
826
|
+
requireString(message.promptId, `${path}.promptId`);
|
|
827
|
+
if (message.role !== 'user' || message.steerId !== undefined) {
|
|
828
|
+
throw new FlexHarnessStoreFormatError(`${path}.promptId is only valid on the user message that heads a run.`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (message.keptInContext !== undefined) {
|
|
832
|
+
if (typeof message.keptInContext !== 'boolean') {
|
|
833
|
+
throw new FlexHarnessStoreFormatError(`${path}.keptInContext must be boolean when present.`);
|
|
834
|
+
}
|
|
835
|
+
if (message.role !== 'user') {
|
|
836
|
+
throw new FlexHarnessStoreFormatError(`${path}.keptInContext is only valid on a user message.`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
808
839
|
if (message.cacheWarm !== undefined && message.cacheWarm !== true) {
|
|
809
840
|
throw new FlexHarnessStoreFormatError(`${path}.cacheWarm must be true when present.`);
|
|
810
841
|
}
|
|
@@ -902,6 +933,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
902
933
|
runId,
|
|
903
934
|
role: message.role,
|
|
904
935
|
status: message.status as IValidatedMessageIdentity['status'],
|
|
936
|
+
...(message.steerId === undefined ? {} : { steerId: message.steerId as string }),
|
|
905
937
|
};
|
|
906
938
|
}
|
|
907
939
|
|
|
@@ -1196,7 +1228,7 @@ export function assertFlexScopeSnapshot(value: unknown): asserts value is IFlexS
|
|
|
1196
1228
|
|
|
1197
1229
|
interface IValidatedProjectionBody {
|
|
1198
1230
|
messages: IValidatedMessageIdentity[];
|
|
1199
|
-
|
|
1231
|
+
stagedMessages: IValidatedMessageIdentity[];
|
|
1200
1232
|
}
|
|
1201
1233
|
|
|
1202
1234
|
function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedProjectionBody {
|
|
@@ -1210,7 +1242,7 @@ function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedPr
|
|
|
1210
1242
|
}
|
|
1211
1243
|
const runIds = new Set<string>();
|
|
1212
1244
|
const stagedMessageIds = new Set<string>();
|
|
1213
|
-
const
|
|
1245
|
+
const stagedMessages: IValidatedMessageIdentity[] = [];
|
|
1214
1246
|
for (let index = 0; index < snapshot.stagedTerminals.length; index++) {
|
|
1215
1247
|
const path = `$snapshot.stagedTerminals[${index}]`;
|
|
1216
1248
|
const terminal = requireRecord(snapshot.stagedTerminals[index], path);
|
|
@@ -1236,26 +1268,33 @@ function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedPr
|
|
|
1236
1268
|
if (!['completed', 'failed', 'cancelled'].includes(String(terminal.status))) {
|
|
1237
1269
|
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
1238
1270
|
}
|
|
1239
|
-
const user =
|
|
1271
|
+
const user = terminal.userMessage === undefined
|
|
1272
|
+
? undefined
|
|
1273
|
+
: validateMessage(terminal.userMessage, `${path}.userMessage`);
|
|
1240
1274
|
const assistant = validateMessage(terminal.assistantMessage, `${path}.assistantMessage`);
|
|
1241
|
-
if (user.role !== 'user' || assistant.role !== 'assistant') {
|
|
1275
|
+
if ((user && user.role !== 'user') || assistant.role !== 'assistant') {
|
|
1242
1276
|
throw new FlexHarnessStoreFormatError(`${path} has invalid terminal message roles.`);
|
|
1243
1277
|
}
|
|
1244
|
-
if (user
|
|
1278
|
+
if ((user && (user.runId !== runId || user.sessionId !== assistant.sessionId)) || assistant.runId !== runId) {
|
|
1245
1279
|
throw new FlexHarnessStoreFormatError(`${path} has uncorrelated terminal messages.`);
|
|
1246
1280
|
}
|
|
1247
1281
|
if (assistant.status !== terminal.status) {
|
|
1248
1282
|
throw new FlexHarnessStoreFormatError(`${path}.status does not match its assistant message.`);
|
|
1249
1283
|
}
|
|
1250
|
-
if (user
|
|
1284
|
+
if (!user && visibleMessages.some((message) =>
|
|
1285
|
+
message.runId === runId && message.role === 'user' && message.steerId === undefined)) {
|
|
1286
|
+
throw new FlexHarnessStoreFormatError(`${path} omits its projected prompt message.`);
|
|
1287
|
+
}
|
|
1288
|
+
if (user && user.status !== terminal.status) {
|
|
1251
1289
|
throw new FlexHarnessStoreFormatError(`${path}.status does not match its user message.`);
|
|
1252
1290
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1291
|
+
if (user) stagedMessages.push(user);
|
|
1292
|
+
stagedMessages.push(assistant);
|
|
1293
|
+
projectionSessionId ??= assistant.sessionId;
|
|
1294
|
+
if (assistant.sessionId !== projectionSessionId) {
|
|
1256
1295
|
throw new FlexHarnessStoreFormatError(`${path} belongs to another session.`);
|
|
1257
1296
|
}
|
|
1258
|
-
for (const messageId of [user.messageId, assistant.messageId]) {
|
|
1297
|
+
for (const messageId of [...(user ? [user.messageId] : []), assistant.messageId]) {
|
|
1259
1298
|
if (stagedMessageIds.has(messageId)) {
|
|
1260
1299
|
throw new FlexHarnessStoreFormatError(
|
|
1261
1300
|
`Snapshot staged terminals contain duplicate message "${messageId}".`,
|
|
@@ -1268,7 +1307,7 @@ function validateProjectionBody(snapshot: Record<string, unknown>): IValidatedPr
|
|
|
1268
1307
|
requireOptionalString(terminal.finishReason, `${path}.finishReason`);
|
|
1269
1308
|
if (terminal.steps !== undefined) requireNonNegativeInteger(terminal.steps, `${path}.steps`);
|
|
1270
1309
|
}
|
|
1271
|
-
return { messages: visibleMessages,
|
|
1310
|
+
return { messages: visibleMessages, stagedMessages };
|
|
1272
1311
|
}
|
|
1273
1312
|
|
|
1274
1313
|
export function assertFlexProjectionSnapshotV1(
|
|
@@ -1328,7 +1367,7 @@ export function assertFlexProjectionSnapshotV2(
|
|
|
1328
1367
|
'workspaceReference',
|
|
1329
1368
|
], path);
|
|
1330
1369
|
const runId = requireString(segment.runId, `${path}.runId`);
|
|
1331
|
-
const userMessageId =
|
|
1370
|
+
const userMessageId = requireOptionalString(segment.userMessageId, `${path}.userMessageId`);
|
|
1332
1371
|
if (segmentRunIds.has(runId)) {
|
|
1333
1372
|
throw new FlexHarnessStoreFormatError(`Snapshot contains duplicate reversion run "${runId}".`);
|
|
1334
1373
|
}
|
|
@@ -1384,12 +1423,19 @@ export function assertFlexProjectionSnapshotV2(
|
|
|
1384
1423
|
throw new FlexHarnessStoreFormatError(`${path}.workspaceReference exceeds its byte limit.`);
|
|
1385
1424
|
}
|
|
1386
1425
|
}
|
|
1387
|
-
const
|
|
1426
|
+
const projectedMessages = [...projectionBody.messages, ...projectionBody.stagedMessages];
|
|
1427
|
+
if (userMessageId === undefined && projectedMessages.some((message) =>
|
|
1428
|
+
message.runId === runId && message.role === 'user' && message.steerId === undefined)) {
|
|
1429
|
+
throw new FlexHarnessStoreFormatError(`${path} omits its projected prompt identity.`);
|
|
1430
|
+
}
|
|
1431
|
+
const correlatedMessages = new Set(projectedMessages
|
|
1388
1432
|
.filter((message) =>
|
|
1389
|
-
message.
|
|
1433
|
+
message.runId === runId && (userMessageId === undefined
|
|
1434
|
+
? message.role === 'assistant'
|
|
1435
|
+
: message.role === 'user' && message.messageId === userMessageId))
|
|
1390
1436
|
.map((message) => `${message.runId}:${message.messageId}`));
|
|
1391
|
-
if (
|
|
1392
|
-
throw new FlexHarnessStoreFormatError(`${path} does not match
|
|
1437
|
+
if (userMessageId === undefined ? correlatedMessages.size === 0 : correlatedMessages.size !== 1) {
|
|
1438
|
+
throw new FlexHarnessStoreFormatError(`${path} does not match its projected turn messages.`);
|
|
1393
1439
|
}
|
|
1394
1440
|
}
|
|
1395
1441
|
requireNonNegativeInteger(snapshot.revertCursor, '$snapshot.revertCursor');
|
|
@@ -1583,8 +1629,19 @@ function assertSessionMetrics(value: unknown): void {
|
|
|
1583
1629
|
requireOnlyKeys(metrics, [
|
|
1584
1630
|
'reportedLifetimeUsedTokens', 'reportedLifetimeUsedTokensExact', 'lifetimeUsageComplete',
|
|
1585
1631
|
'pendingGenerationId', 'pendingCompactionId', 'lastObservedInputTokens', 'lastObservedInputTokensAt',
|
|
1632
|
+
'contextWindow', 'contextCompactionTokens', 'contextCompactionOff',
|
|
1586
1633
|
...sessionMetricsBreakdownKeys,
|
|
1587
1634
|
], path);
|
|
1635
|
+
for (const key of ['contextWindow', 'contextCompactionTokens']) {
|
|
1636
|
+
if (Object.hasOwn(metrics, key)) requirePositiveInteger(metrics[key], `${path}.${key}`);
|
|
1637
|
+
}
|
|
1638
|
+
if (Object.hasOwn(metrics, 'contextCompactionOff')
|
|
1639
|
+
&& !['disabled', 'no-context-window', 'no-compactor'].includes(metrics.contextCompactionOff as string)) {
|
|
1640
|
+
throw new FlexHarnessStoreFormatError(`${path}.contextCompactionOff is invalid.`);
|
|
1641
|
+
}
|
|
1642
|
+
if (Object.hasOwn(metrics, 'contextCompactionOff') && Object.hasOwn(metrics, 'contextCompactionTokens')) {
|
|
1643
|
+
throw new FlexHarnessStoreFormatError(`${path} states both a compaction point and why there is none.`);
|
|
1644
|
+
}
|
|
1588
1645
|
requireNonNegativeInteger(metrics.reportedLifetimeUsedTokens, `${path}.reportedLifetimeUsedTokens`);
|
|
1589
1646
|
for (const key of ['reportedLifetimeUsedTokensExact', 'lifetimeUsageComplete']) {
|
|
1590
1647
|
if (typeof metrics[key] !== 'boolean') throw new FlexHarnessStoreFormatError(`${path}.${key} must be boolean.`);
|
|
@@ -1802,7 +1859,7 @@ export function assertFlexProjectionSnapshotV3(
|
|
|
1802
1859
|
const segment = entry as Record<string, unknown>;
|
|
1803
1860
|
return {
|
|
1804
1861
|
runId: segment.runId,
|
|
1805
|
-
userMessageId: segment.userMessageId,
|
|
1862
|
+
...(segment.userMessageId === undefined ? {} : { userMessageId: segment.userMessageId }),
|
|
1806
1863
|
status: segment.status,
|
|
1807
1864
|
contextAvailable: segment.contextAvailable,
|
|
1808
1865
|
eventIds: segment.eventIds,
|
|
@@ -1860,7 +1917,7 @@ function assertProjectionSession(
|
|
|
1860
1917
|
}
|
|
1861
1918
|
for (const terminal of snapshot.stagedTerminals) {
|
|
1862
1919
|
if (
|
|
1863
|
-
terminal.userMessage.sessionId !== sessionId
|
|
1920
|
+
(terminal.userMessage !== undefined && terminal.userMessage.sessionId !== sessionId)
|
|
1864
1921
|
|| terminal.assistantMessage.sessionId !== sessionId
|
|
1865
1922
|
) {
|
|
1866
1923
|
throw new FlexHarnessStoreFormatError(
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
IFlexSessionMetrics,
|
|
3
3
|
IFlexSessionMetricsState,
|
|
4
|
+
TFlexContextCompactionOff,
|
|
4
5
|
TFlexModelCallUsage,
|
|
5
6
|
} from './interfaces.js';
|
|
6
7
|
|
|
8
|
+
/** The compaction point a generation resolved for its model, recorded when it settles. */
|
|
9
|
+
export interface ISessionMetricsContextCompaction {
|
|
10
|
+
contextWindow?: number;
|
|
11
|
+
point: { tokens: number } | { off: TFlexContextCompactionOff };
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/** Fixed-size accumulator. One owner per generation / actual compactor invocation. */
|
|
8
15
|
export interface ISessionMetricsOperation {
|
|
9
16
|
id: string;
|
|
@@ -19,6 +26,8 @@ export interface ISessionMetricsOperation {
|
|
|
19
26
|
breakdownExact: boolean;
|
|
20
27
|
lastObservedInputTokens?: number;
|
|
21
28
|
lastObservedInputTokensAt?: string;
|
|
29
|
+
/** Set on a generation once its model is resolved. */
|
|
30
|
+
contextCompaction?: ISessionMetricsContextCompaction;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export const createSessionMetricsState = (complete: boolean): IFlexSessionMetricsState => ({
|
|
@@ -109,6 +118,15 @@ export function settleSessionMetricsOperation(
|
|
|
109
118
|
metrics.lastObservedInputTokens = operation.lastObservedInputTokens;
|
|
110
119
|
metrics.lastObservedInputTokensAt = operation.lastObservedInputTokensAt;
|
|
111
120
|
}
|
|
121
|
+
if (operation.contextCompaction !== undefined) {
|
|
122
|
+
const { contextWindow, point } = operation.contextCompaction;
|
|
123
|
+
delete metrics.contextWindow;
|
|
124
|
+
delete metrics.contextCompactionTokens;
|
|
125
|
+
delete metrics.contextCompactionOff;
|
|
126
|
+
if (contextWindow !== undefined) metrics.contextWindow = contextWindow;
|
|
127
|
+
if ('tokens' in point) metrics.contextCompactionTokens = point.tokens;
|
|
128
|
+
else metrics.contextCompactionOff = point.off;
|
|
129
|
+
}
|
|
112
130
|
delete metrics[key];
|
|
113
131
|
}
|
|
114
132
|
|
|
@@ -130,5 +148,10 @@ export function sessionMetricsView(metrics: IFlexSessionMetricsState): IFlexSess
|
|
|
130
148
|
lastObservedInputTokensAt: metrics.lastObservedInputTokensAt,
|
|
131
149
|
lastObservedInputTokensFreshness: 'historical' as const,
|
|
132
150
|
}),
|
|
151
|
+
...(metrics.contextWindow === undefined ? {} : { contextWindow: metrics.contextWindow }),
|
|
152
|
+
...(metrics.contextCompactionTokens === undefined
|
|
153
|
+
? {}
|
|
154
|
+
: { contextCompactionTokens: metrics.contextCompactionTokens }),
|
|
155
|
+
...(metrics.contextCompactionOff === undefined ? {} : { contextCompactionOff: metrics.contextCompactionOff }),
|
|
133
156
|
};
|
|
134
157
|
}
|
package/ts/utils.transfer.ts
CHANGED
|
@@ -82,6 +82,15 @@ export function normalizeModelIdentity(identity: IFlexModelIdentity): IFlexModel
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export function normalizeResolvedModel(resolvedModel: IFlexResolvedModel): IFlexResolvedModel {
|
|
85
|
+
for (const key of ['contextWindow', 'contextCompactionDefaultTokens'] as const) {
|
|
86
|
+
const tokens = resolvedModel[key];
|
|
87
|
+
if (tokens !== undefined && (!Number.isSafeInteger(tokens) || tokens < 1)) {
|
|
88
|
+
throw new FlexHarnessValidationError(`The resolved model's ${key} must be a positive integer.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (resolvedModel.contextCompactionDefaultTokens !== undefined && resolvedModel.contextWindow === undefined) {
|
|
92
|
+
throw new FlexHarnessValidationError('The resolved model states contextCompactionDefaultTokens without contextWindow.');
|
|
93
|
+
}
|
|
85
94
|
return {
|
|
86
95
|
model: resolvedModel.model,
|
|
87
96
|
identity: normalizeModelIdentity(resolvedModel.identity),
|
|
@@ -91,6 +100,10 @@ export function normalizeResolvedModel(resolvedModel: IFlexResolvedModel): IFlex
|
|
|
91
100
|
: { providerOptions: resolvedModel.providerOptions }),
|
|
92
101
|
...(resolvedModel.cache === undefined ? {} : { cache: resolvedModel.cache }),
|
|
93
102
|
...(resolvedModel.maxSteps === undefined ? {} : { maxSteps: resolvedModel.maxSteps }),
|
|
103
|
+
...(resolvedModel.contextWindow === undefined ? {} : { contextWindow: resolvedModel.contextWindow }),
|
|
104
|
+
...(resolvedModel.contextCompactionDefaultTokens === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { contextCompactionDefaultTokens: resolvedModel.contextCompactionDefaultTokens }),
|
|
94
107
|
};
|
|
95
108
|
}
|
|
96
109
|
|
package/ts/utils.validation.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
IFlexSlashCommandExecutionOptions,
|
|
17
17
|
IFlexUpdateProjectTaskInput,
|
|
18
18
|
IFlexUpdateSessionOptions,
|
|
19
|
+
TFlexContextCompactionSetting,
|
|
19
20
|
TFlexProjectTaskPriority,
|
|
20
21
|
TFlexProjectTaskStatus,
|
|
21
22
|
} from './interfaces.js';
|
|
@@ -228,13 +229,34 @@ export function validateProjectedErrorInfo(value: unknown): IFlexErrorInfo {
|
|
|
228
229
|
});
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Throws a `FlexHarnessValidationError` unless `value` is a valid compaction setting, and unless a
|
|
234
|
+
* setting other than `false` has a compactor to act with.
|
|
235
|
+
*/
|
|
236
|
+
export function validateContextCompactionSetting(
|
|
237
|
+
value: unknown,
|
|
238
|
+
hasCompactor: boolean,
|
|
239
|
+
path = 'contextCompaction',
|
|
240
|
+
): asserts value is TFlexContextCompactionSetting {
|
|
241
|
+
try {
|
|
242
|
+
plugins.assertAgentContextCompactionSetting(value, path);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw new FlexHarnessValidationError(error instanceof Error ? error.message : String(error));
|
|
245
|
+
}
|
|
246
|
+
if (value !== false && !hasCompactor) {
|
|
247
|
+
throw new FlexHarnessValidationError(`${path} requires a contextCompactor.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions, hasCompactor: boolean): void {
|
|
232
252
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
233
253
|
throw new FlexHarnessValidationError('updateSession options must be a plain object.');
|
|
234
254
|
}
|
|
235
255
|
const keys = Object.keys(options);
|
|
236
|
-
if (keys.length === 0)
|
|
237
|
-
|
|
256
|
+
if (keys.length === 0) {
|
|
257
|
+
throw new FlexHarnessValidationError('updateSession requires title, archived or contextCompaction.');
|
|
258
|
+
}
|
|
259
|
+
const unsupportedKey = keys.find((key) => key !== 'title' && key !== 'archived' && key !== 'contextCompaction');
|
|
238
260
|
if (unsupportedKey) throw new FlexHarnessValidationError(`updateSession does not support "${unsupportedKey}".`);
|
|
239
261
|
if (Object.prototype.hasOwnProperty.call(options, 'title') && options.title !== null) {
|
|
240
262
|
validateUtf8String(
|
|
@@ -247,6 +269,9 @@ export function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions)
|
|
|
247
269
|
if (Object.prototype.hasOwnProperty.call(options, 'archived') && typeof options.archived !== 'boolean') {
|
|
248
270
|
throw new FlexHarnessValidationError('archived must be a boolean.');
|
|
249
271
|
}
|
|
272
|
+
if (Object.prototype.hasOwnProperty.call(options, 'contextCompaction') && options.contextCompaction !== null) {
|
|
273
|
+
validateContextCompactionSetting(options.contextCompaction, hasCompactor);
|
|
274
|
+
}
|
|
250
275
|
}
|
|
251
276
|
|
|
252
277
|
export function validateProjectTaskStatus(value: unknown): asserts value is TFlexProjectTaskStatus {
|
|
@@ -317,10 +342,11 @@ export function validatePromptOptions(options: IFlexPromptOptions, scheduled: bo
|
|
|
317
342
|
throw new FlexHarnessValidationError('Prompt options must be a plain object.');
|
|
318
343
|
}
|
|
319
344
|
const supported = scheduled
|
|
320
|
-
? ['modelHint', 'system', 'maxSteps', 'debounceMs']
|
|
321
|
-
: ['modelHint', 'system', 'maxSteps'];
|
|
345
|
+
? ['promptId', 'modelHint', 'system', 'maxSteps', 'debounceMs']
|
|
346
|
+
: ['promptId', 'modelHint', 'system', 'maxSteps'];
|
|
322
347
|
const unsupported = Object.keys(options).find((key) => !supported.includes(key));
|
|
323
348
|
if (unsupported) throw new FlexHarnessValidationError(`Prompt options do not support "${unsupported}".`);
|
|
349
|
+
if (options.promptId !== undefined) validateIdentifier(options.promptId, 'promptId');
|
|
324
350
|
if (options.modelHint !== undefined) validateIdentifier(options.modelHint, 'modelHint');
|
|
325
351
|
if (options.system !== undefined) validateIdentifier(options.system, 'system');
|
|
326
352
|
if (options.maxSteps !== undefined && (!Number.isSafeInteger(options.maxSteps) || options.maxSteps < 1)) {
|