@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.
Files changed (51) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.cachewarm.d.ts +2 -2
  3. package/dist_ts/classes.flexharness.cachewarm.js +1 -1
  4. package/dist_ts/classes.flexharness.core.d.ts +8 -1
  5. package/dist_ts/classes.flexharness.core.js +44 -31
  6. package/dist_ts/classes.flexharness.d.ts +5 -4
  7. package/dist_ts/classes.flexharness.js +10 -7
  8. package/dist_ts/classes.flexharness.prompts.d.ts +9 -5
  9. package/dist_ts/classes.flexharness.prompts.js +93 -18
  10. package/dist_ts/classes.flexharness.runs.d.ts +9 -4
  11. package/dist_ts/classes.flexharness.runs.js +70 -18
  12. package/dist_ts/classes.flexharness.sessions.d.ts +4 -5
  13. package/dist_ts/classes.flexharness.sessions.js +45 -12
  14. package/dist_ts/classes.flexharness.subagents.js +1 -1
  15. package/dist_ts/errors.d.ts +9 -0
  16. package/dist_ts/errors.js +14 -1
  17. package/dist_ts/index.d.ts +2 -0
  18. package/dist_ts/index.js +2 -1
  19. package/dist_ts/interfaces.d.ts +119 -11
  20. package/dist_ts/interfaces.flexharness.d.ts +12 -8
  21. package/dist_ts/interfaces.js +1 -1
  22. package/dist_ts/plugins.d.ts +3 -3
  23. package/dist_ts/plugins.js +3 -3
  24. package/dist_ts/utils.harnessoptions.js +6 -3
  25. package/dist_ts/utils.json.js +74 -18
  26. package/dist_ts/utils.sessionmetrics.d.ts +12 -1
  27. package/dist_ts/utils.sessionmetrics.js +18 -1
  28. package/dist_ts/utils.transfer.js +14 -1
  29. package/dist_ts/utils.validation.d.ts +7 -2
  30. package/dist_ts/utils.validation.js +28 -7
  31. package/package.json +3 -3
  32. package/readme.md +75 -6
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/classes.flexharness.cachewarm.ts +2 -2
  35. package/ts/classes.flexharness.core.ts +49 -30
  36. package/ts/classes.flexharness.prompts.ts +124 -39
  37. package/ts/classes.flexharness.runs.ts +78 -24
  38. package/ts/classes.flexharness.sessions.ts +54 -9
  39. package/ts/classes.flexharness.subagents.ts +2 -1
  40. package/ts/classes.flexharness.ts +25 -5
  41. package/ts/errors.ts +15 -0
  42. package/ts/index.ts +6 -0
  43. package/ts/interfaces.flexharness.ts +12 -8
  44. package/ts/interfaces.ts +125 -11
  45. package/ts/plugins.ts +13 -0
  46. package/ts/readme.md +75 -6
  47. package/ts/utils.harnessoptions.ts +9 -2
  48. package/ts/utils.json.ts +75 -18
  49. package/ts/utils.sessionmetrics.ts +23 -0
  50. package/ts/utils.transfer.ts +13 -0
  51. 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
- stagedUsers: IValidatedMessageIdentity[];
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 stagedUsers: IValidatedMessageIdentity[] = [];
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 = validateMessage(terminal.userMessage, `${path}.userMessage`);
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.runId !== runId || assistant.runId !== runId || user.sessionId !== assistant.sessionId) {
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.status !== terminal.status) {
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
- stagedUsers.push(user);
1254
- projectionSessionId ??= user.sessionId;
1255
- if (user.sessionId !== projectionSessionId) {
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, stagedUsers };
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 = requireString(segment.userMessageId, `${path}.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 correlatedUsers = new Set([...projectionBody.messages, ...projectionBody.stagedUsers]
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.role === 'user' && message.runId === runId && message.messageId === userMessageId)
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 (correlatedUsers.size !== 1) {
1392
- throw new FlexHarnessStoreFormatError(`${path} does not match exactly one projected user message.`);
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
  }
@@ -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
 
@@ -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
- export function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions): void {
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) throw new FlexHarnessValidationError('updateSession requires title or archived.');
237
- const unsupportedKey = keys.find((key) => key !== 'title' && key !== 'archived');
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)) {