@modelprofile.com/flexharness 14.0.0 → 15.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 (45) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.core.d.ts +8 -1
  3. package/dist_ts/classes.flexharness.core.js +31 -12
  4. package/dist_ts/classes.flexharness.d.ts +4 -4
  5. package/dist_ts/classes.flexharness.js +7 -7
  6. package/dist_ts/classes.flexharness.prompts.js +11 -3
  7. package/dist_ts/classes.flexharness.runs.d.ts +6 -1
  8. package/dist_ts/classes.flexharness.runs.js +45 -7
  9. package/dist_ts/classes.flexharness.sessions.d.ts +4 -5
  10. package/dist_ts/classes.flexharness.sessions.js +45 -12
  11. package/dist_ts/errors.d.ts +9 -0
  12. package/dist_ts/errors.js +14 -1
  13. package/dist_ts/index.d.ts +2 -0
  14. package/dist_ts/index.js +2 -1
  15. package/dist_ts/interfaces.d.ts +96 -2
  16. package/dist_ts/interfaces.flexharness.d.ts +2 -0
  17. package/dist_ts/interfaces.js +1 -1
  18. package/dist_ts/plugins.d.ts +3 -3
  19. package/dist_ts/plugins.js +3 -3
  20. package/dist_ts/utils.harnessoptions.js +6 -3
  21. package/dist_ts/utils.json.js +43 -1
  22. package/dist_ts/utils.sessionmetrics.d.ts +12 -1
  23. package/dist_ts/utils.sessionmetrics.js +18 -1
  24. package/dist_ts/utils.transfer.js +14 -1
  25. package/dist_ts/utils.validation.d.ts +7 -2
  26. package/dist_ts/utils.validation.js +28 -7
  27. package/package.json +3 -3
  28. package/readme.md +24 -4
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/classes.flexharness.core.ts +36 -9
  31. package/ts/classes.flexharness.prompts.ts +11 -1
  32. package/ts/classes.flexharness.runs.ts +48 -4
  33. package/ts/classes.flexharness.sessions.ts +54 -9
  34. package/ts/classes.flexharness.ts +15 -5
  35. package/ts/errors.ts +15 -0
  36. package/ts/index.ts +6 -0
  37. package/ts/interfaces.flexharness.ts +2 -0
  38. package/ts/interfaces.ts +99 -2
  39. package/ts/plugins.ts +12 -0
  40. package/ts/readme.md +24 -4
  41. package/ts/utils.harnessoptions.ts +9 -2
  42. package/ts/utils.json.ts +41 -0
  43. package/ts/utils.sessionmetrics.ts +23 -0
  44. package/ts/utils.transfer.ts +13 -0
  45. package/ts/utils.validation.ts +31 -5
@@ -186,7 +186,7 @@ export class FlexHarnessRuns<TScope> {
186
186
  if (!run.modelResolution) throw new FlexHarnessValidationError('Generation completed without a resolved model.');
187
187
  const closeReversion = await this.core.reversions.prepareRunReversionClose(run, 'completed');
188
188
  await this.core.admission.closeRunDelegatedRunAdmission(run);
189
- const terminal = this.buildTerminal(run, 'completed', result);
189
+ let terminal = this.buildTerminal(run, 'completed', result);
190
190
  try {
191
191
  // The reversion segment closes in the save that stages the terminal.
192
192
  await this.core.mutateProjection(run.state, run.stored, () => {
@@ -209,6 +209,7 @@ export class FlexHarnessRuns<TScope> {
209
209
  throw projected;
210
210
  }
211
211
  run.steering.keptInContext = true;
212
+ terminal = this.withKeptInContext(terminal, true);
212
213
  this.core.reversions.refreshAutomaticArchiveHorizon(run.stored);
213
214
  run.phase = 'promoting';
214
215
  const closePublication = this.core.openPublication(run.state, run.sessionId);
@@ -351,7 +352,10 @@ export class FlexHarnessRuns<TScope> {
351
352
  const terminalError = publicStatus === 'cancelled'
352
353
  ? run.ownerCancellation ?? this.core.trustInternalError(new FlexHarnessAbortError())
353
354
  : safeError;
354
- const terminal = this.buildTerminal(run, publicStatus, undefined, terminalError);
355
+ const terminal = this.withKeptInContext(
356
+ this.buildTerminal(run, publicStatus, undefined, terminalError),
357
+ run.steering.keptInContext === true,
358
+ );
355
359
  if (!this.core.isTombstoned(run)) {
356
360
  const closePublication = this.core.openPublication(run.state, run.sessionId);
357
361
  try {
@@ -564,6 +568,21 @@ export class FlexHarnessRuns<TScope> {
564
568
  } catch (error) {
565
569
  throw this.core.projectExternalError(run, error, 'toolProvider');
566
570
  }
571
+ const contextCompaction = run.stored.session.contextCompaction ?? this.core.agentSessionPolicy.contextCompaction;
572
+ if (run.metricsOperation) {
573
+ run.metricsOperation.contextCompaction = {
574
+ ...(model!.contextWindow === undefined ? {} : { contextWindow: model!.contextWindow }),
575
+ point: this.core.agentSessionPolicy.contextCompactor
576
+ ? plugins.resolveAgentContextCompactionPoint({
577
+ ...(model!.contextWindow === undefined ? {} : { contextWindow: model!.contextWindow }),
578
+ ...(model!.contextCompactionDefaultTokens === undefined
579
+ ? {}
580
+ : { contextCompactionDefaultTokens: model!.contextCompactionDefaultTokens }),
581
+ ...(contextCompaction === undefined ? {} : { setting: contextCompaction }),
582
+ })
583
+ : { off: 'no-compactor' },
584
+ };
585
+ }
567
586
  let closed = false;
568
587
  let closePromise: Promise<void> | undefined;
569
588
  const close = toolHandle?.close
@@ -596,6 +615,11 @@ export class FlexHarnessRuns<TScope> {
596
615
  ...(model!.cache === undefined ? {} : { cache: model!.cache }),
597
616
  ...(options.maxSteps ?? model!.maxSteps ? { maxSteps: options.maxSteps ?? model!.maxSteps } : {}),
598
617
  ...(run.cacheWarm ? { maxOutputTokens: this.core.cacheWarmOptions.maxOutputTokens } : {}),
618
+ ...(model!.contextWindow === undefined ? {} : { contextWindow: model!.contextWindow }),
619
+ ...(model!.contextCompactionDefaultTokens === undefined
620
+ ? {}
621
+ : { contextCompactionDefaultTokens: model!.contextCompactionDefaultTokens }),
622
+ ...(contextCompaction === undefined ? {} : { contextCompaction }),
599
623
  ...(close ? { close } : {}),
600
624
  };
601
625
  } catch (error) {
@@ -706,6 +730,7 @@ export class FlexHarnessRuns<TScope> {
706
730
  public createReservation(
707
731
  run: IActiveRun,
708
732
  prompt: INormalizedFlexPrompt,
733
+ promptId?: string,
709
734
  ): { userMessage: IFlexMessage; assistantMessage: IFlexMessage } {
710
735
  const timestamp = new Date().toISOString();
711
736
  const userMessage: IFlexMessage = {
@@ -716,6 +741,7 @@ export class FlexHarnessRuns<TScope> {
716
741
  status: 'streaming',
717
742
  createdAt: timestamp,
718
743
  parts: this.userMessageParts(prompt),
744
+ ...(promptId === undefined ? {} : { promptId }),
719
745
  ...(run.cacheWarm ? { cacheWarm: true as const } : {}),
720
746
  };
721
747
  const assistantMessage: IFlexMessage = {
@@ -1029,6 +1055,14 @@ export class FlexHarnessRuns<TScope> {
1029
1055
  }, true);
1030
1056
  }
1031
1057
 
1058
+ /**
1059
+ * The terminal with its prompt's `keptInContext`: whether the run's result kept the prompt and
1060
+ * the steers it took in in the model context. `replaceVisibleRun()` writes it onto the steers.
1061
+ */
1062
+ public withKeptInContext(terminal: IFlexTerminalProjection, keptInContext: boolean): IFlexTerminalProjection {
1063
+ return { ...terminal, userMessage: { ...terminal.userMessage, keptInContext } };
1064
+ }
1065
+
1032
1066
  private replaceStagedTerminal(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
1033
1067
  stored.stagedTerminals = stored.stagedTerminals.filter((entry) => entry.runId !== terminal.runId);
1034
1068
  stored.stagedTerminals.push(cloneSerializable(terminal));
@@ -1046,11 +1080,18 @@ export class FlexHarnessRuns<TScope> {
1046
1080
  [terminal.assistantMessage.messageId, terminal.assistantMessage],
1047
1081
  ]);
1048
1082
  const placed = new Set<string>();
1083
+ const keptInContext = terminal.userMessage.keptInContext;
1049
1084
  stored.messages = stored.messages.map((message) => {
1050
1085
  const replacement = message.runId === terminal.runId
1051
1086
  ? replacements.get(message.messageId)
1052
1087
  : undefined;
1053
- if (!replacement) return message;
1088
+ if (!replacement) {
1089
+ // The steers the run took in share its prompt's outcome.
1090
+ return keptInContext !== undefined && message.runId === terminal.runId && message.role === 'user'
1091
+ && message.keptInContext !== keptInContext
1092
+ ? { ...cloneSerializable(message), keptInContext }
1093
+ : message;
1094
+ }
1054
1095
  placed.add(replacement.messageId);
1055
1096
  return cloneSerializable(replacement);
1056
1097
  });
@@ -1089,7 +1130,10 @@ export class FlexHarnessRuns<TScope> {
1089
1130
  this.core.emitPartEvent(run, 'part.completed', part);
1090
1131
  }
1091
1132
  }
1092
- for (const message of [terminal.userMessage, terminal.assistantMessage]) {
1133
+ const steerMessages = terminal.userMessage.keptInContext === undefined
1134
+ ? []
1135
+ : run.stored.messages.filter((message) => message.runId === run.runId && message.steerId !== undefined);
1136
+ for (const message of [terminal.userMessage, ...steerMessages, terminal.assistantMessage]) {
1093
1137
  this.core.emitEvent(run.scopeId, run.stored.session, {
1094
1138
  type: 'message.updated',
1095
1139
  runId: run.runId,
@@ -11,6 +11,7 @@ import {
11
11
  type IFlexMessage,
12
12
  type IFlexMessagePage,
13
13
  type IFlexMessagePageOptions,
14
+ type IFlexMessageReadOptions,
14
15
  type IFlexSession,
15
16
  type IFlexSessionView,
16
17
  type IFlexSessionMetrics,
@@ -29,6 +30,7 @@ import {
29
30
  requireTransferIdentifier,
30
31
  } from './utils.transfer.js';
31
32
  import {
33
+ validateContextCompactionSetting,
32
34
  validateIdentifier,
33
35
  validateSessionGenerationId,
34
36
  validateSessionConfigurationRef,
@@ -51,6 +53,19 @@ import type { FlexHarnessCore } from './classes.flexharness.core.js';
51
53
  const maxSessionViewPasses = 64;
52
54
 
53
55
  /** Sessions and their messages: listing, creation, updates, views and message pages. */
56
+ /** Validates transcript read options and states whether undone messages are included. */
57
+ function readIncludeUndone(options: IFlexMessageReadOptions, name: string): boolean {
58
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
59
+ throw new FlexHarnessValidationError(`${name} must be a plain object.`);
60
+ }
61
+ const unsupported = Object.keys(options).find((key) => key !== 'includeUndone');
62
+ if (unsupported) throw new FlexHarnessValidationError(`${name} do not support "${unsupported}".`);
63
+ if (options.includeUndone !== undefined && typeof options.includeUndone !== 'boolean') {
64
+ throw new FlexHarnessValidationError('includeUndone must be a boolean.');
65
+ }
66
+ return options.includeUndone === true;
67
+ }
68
+
54
69
  export class FlexHarnessSessions<TScope> {
55
70
  constructor(private readonly core: FlexHarnessCore<TScope>) {}
56
71
 
@@ -73,7 +88,7 @@ export class FlexHarnessSessions<TScope> {
73
88
  throw new FlexHarnessValidationError('createSession options must be a plain object.');
74
89
  }
75
90
  const unsupported = Object.keys(options).find(
76
- (key) => !['sessionId', 'sessionGenerationId', 'configurationRef', 'title'].includes(key),
91
+ (key) => !['sessionId', 'sessionGenerationId', 'configurationRef', 'title', 'contextCompaction'].includes(key),
77
92
  );
78
93
  if (unsupported) throw new FlexHarnessValidationError(`createSession does not support "${unsupported}".`);
79
94
  const requestedSessionGenerationId = options.sessionGenerationId;
@@ -81,6 +96,12 @@ export class FlexHarnessSessions<TScope> {
81
96
  validateSessionGenerationId(requestedSessionGenerationId);
82
97
  }
83
98
  if (options.configurationRef !== undefined) validateSessionConfigurationRef(options.configurationRef);
99
+ if (options.contextCompaction !== undefined) {
100
+ validateContextCompactionSetting(
101
+ options.contextCompaction,
102
+ this.core.agentSessionPolicy.contextCompactor !== undefined,
103
+ );
104
+ }
84
105
  const resolved = await this.core.resolveState(scopeId);
85
106
  const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
86
107
  validateIdentifier(sessionId, 'sessionId');
@@ -131,6 +152,9 @@ export class FlexHarnessSessions<TScope> {
131
152
  sessionGenerationSequence: resolved.state.revision + 1,
132
153
  ...(options.configurationRef === undefined ? {} : { configurationRef: options.configurationRef }),
133
154
  ...(options.title ? { title: options.title } : {}),
155
+ ...(options.contextCompaction === undefined
156
+ ? {}
157
+ : { contextCompaction: cloneSerializable(options.contextCompaction) }),
134
158
  createdAt: timestamp,
135
159
  updatedAt: timestamp,
136
160
  status: 'idle',
@@ -257,7 +281,7 @@ export class FlexHarnessSessions<TScope> {
257
281
  sessionId: string,
258
282
  options: IFlexUpdateSessionOptions,
259
283
  ): Promise<IFlexSession> {
260
- validateUpdateSessionOptions(options);
284
+ validateUpdateSessionOptions(options, this.core.agentSessionPolicy.contextCompactor !== undefined);
261
285
  const { state } = await this.core.resolveState(scopeId);
262
286
  let result!: IFlexSession;
263
287
  await this.core.mutateScope(state, () => {
@@ -269,6 +293,10 @@ export class FlexHarnessSessions<TScope> {
269
293
  if (options.title === null) delete stored.session.title;
270
294
  else stored.session.title = options.title!;
271
295
  }
296
+ if (Object.prototype.hasOwnProperty.call(options, 'contextCompaction')) {
297
+ if (options.contextCompaction === null) delete stored.session.contextCompaction;
298
+ else stored.session.contextCompaction = cloneSerializable(options.contextCompaction!);
299
+ }
272
300
  if (options.archived === true) stored.session.archivedAt ??= timestamp;
273
301
  else if (options.archived === false) delete stored.session.archivedAt;
274
302
  stored.session.updatedAt = timestamp;
@@ -278,7 +306,12 @@ export class FlexHarnessSessions<TScope> {
278
306
  return result;
279
307
  }
280
308
 
281
- public async getSessionView(scopeId: string, sessionId: string): Promise<IFlexSessionView> {
309
+ public async getSessionView(
310
+ scopeId: string,
311
+ sessionId: string,
312
+ options: IFlexMessageReadOptions = {},
313
+ ): Promise<IFlexSessionView> {
314
+ const includeUndone = readIncludeUndone(options, 'Session view options');
282
315
  const { state } = await this.core.resolveState(scopeId);
283
316
  let stored = this.core.requireSession(state, sessionId);
284
317
  for (let pass = 0; ; pass++) {
@@ -301,7 +334,7 @@ export class FlexHarnessSessions<TScope> {
301
334
  ) break;
302
335
  }
303
336
  const run = state.activeRuns.get(sessionId);
304
- const messages = this.core.visibleMessages(stored).map((message) => (
337
+ const messages = this.core.transcriptMessages(stored, includeUndone).map((message) => (
305
338
  run && run.stored === stored && message.messageId === run.assistantMessageId
306
339
  && message.status === 'streaming'
307
340
  ? { ...message, parts: run.callbackParts }
@@ -318,12 +351,17 @@ export class FlexHarnessSessions<TScope> {
318
351
  });
319
352
  }
320
353
 
321
- public async getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
354
+ public async getMessages(
355
+ scopeId: string,
356
+ sessionId: string,
357
+ options: IFlexMessageReadOptions = {},
358
+ ): Promise<IFlexMessage[]> {
359
+ const includeUndone = readIncludeUndone(options, 'Message read options');
322
360
  const { state } = await this.core.resolveState(scopeId);
323
361
  const stored = this.core.requireSession(state, sessionId);
324
362
  await stored.projectionQueue;
325
363
  this.core.assertStateAcceptingWork(state);
326
- return publicSnapshot(this.core.visibleMessages(stored));
364
+ return publicSnapshot(this.core.transcriptMessages(stored, includeUndone));
327
365
  }
328
366
 
329
367
  public async listMessagePage(
@@ -334,8 +372,13 @@ export class FlexHarnessSessions<TScope> {
334
372
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
335
373
  throw new FlexHarnessValidationError('Message page options must be a plain object.');
336
374
  }
337
- const unsupported = Object.keys(options).find((key) => key !== 'limit' && key !== 'before');
375
+ const unsupported = Object.keys(options)
376
+ .find((key) => key !== 'limit' && key !== 'before' && key !== 'includeUndone');
338
377
  if (unsupported) throw new FlexHarnessValidationError(`Message page options do not support "${unsupported}".`);
378
+ if (options.includeUndone !== undefined && typeof options.includeUndone !== 'boolean') {
379
+ throw new FlexHarnessValidationError('includeUndone must be a boolean.');
380
+ }
381
+ const includeUndone = options.includeUndone === true;
339
382
  const limit = options.limit ?? maxMessagePageSize;
340
383
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > maxMessagePageSize) {
341
384
  throw new FlexHarnessValidationError(`Message page limit must be an integer from 1 through ${maxMessagePageSize}.`);
@@ -353,7 +396,7 @@ export class FlexHarnessSessions<TScope> {
353
396
  await stored.projectionQueue;
354
397
  this.core.assertStateAcceptingWork(state);
355
398
  const namespace = this.core.messageCursorNamespace(state.storageKey);
356
- const visibleMessages = this.core.visibleMessages(stored);
399
+ const visibleMessages = this.core.transcriptMessages(stored, includeUndone);
357
400
  let end = visibleMessages.length;
358
401
  if (options.before !== undefined) {
359
402
  const cursor = this.core.parseMessageCursor(options.before);
@@ -394,14 +437,16 @@ export class FlexHarnessSessions<TScope> {
394
437
  scopeId: string,
395
438
  sessionId: string,
396
439
  messageId: string,
440
+ options: IFlexMessageReadOptions = {},
397
441
  ): Promise<IFlexMessage> {
442
+ const includeUndone = readIncludeUndone(options, 'Message read options');
398
443
  validateIdentifier(messageId, 'messageId');
399
444
  requireTransferIdentifier(messageId, 'messageId');
400
445
  const { state } = await this.core.resolveState(scopeId);
401
446
  const stored = this.core.requireSession(state, sessionId);
402
447
  await stored.projectionQueue;
403
448
  this.core.assertStateAcceptingWork(state);
404
- return publicSnapshot(createBoundedTransferMessage(this.core.requireMessage(stored, messageId)));
449
+ return publicSnapshot(createBoundedTransferMessage(this.core.requireMessage(stored, messageId, includeUndone)));
405
450
  }
406
451
 
407
452
  public async pushRuntimeEvent(
@@ -15,6 +15,7 @@ import type {
15
15
  IFlexMessage,
16
16
  IFlexMessagePage,
17
17
  IFlexMessagePageOptions,
18
+ IFlexMessageReadOptions,
18
19
  IFlexPermissionRequest,
19
20
  IFlexProjectGoalResult,
20
21
  IFlexSteerAdmission,
@@ -246,12 +247,20 @@ export class FlexHarness<TScope = unknown> {
246
247
  * with the instance's event sequence they include. It waits until no queued projection or scope
247
248
  * write and no change that emits events for the session is in progress.
248
249
  */
249
- public getSessionView(scopeId: string, sessionId: string): Promise<IFlexSessionView> {
250
- return this.core.sessions.getSessionView(scopeId, sessionId);
250
+ public getSessionView(
251
+ scopeId: string,
252
+ sessionId: string,
253
+ options?: IFlexMessageReadOptions,
254
+ ): Promise<IFlexSessionView> {
255
+ return this.core.sessions.getSessionView(scopeId, sessionId, options);
251
256
  }
252
257
 
253
- public getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
254
- return this.core.sessions.getMessages(scopeId, sessionId);
258
+ public getMessages(
259
+ scopeId: string,
260
+ sessionId: string,
261
+ options?: IFlexMessageReadOptions,
262
+ ): Promise<IFlexMessage[]> {
263
+ return this.core.sessions.getMessages(scopeId, sessionId, options);
255
264
  }
256
265
 
257
266
  public listMessagePage(
@@ -266,8 +275,9 @@ export class FlexHarness<TScope = unknown> {
266
275
  scopeId: string,
267
276
  sessionId: string,
268
277
  messageId: string,
278
+ options?: IFlexMessageReadOptions,
269
279
  ): Promise<IFlexMessage> {
270
- return this.core.sessions.getMessage(scopeId, sessionId, messageId);
280
+ return this.core.sessions.getMessage(scopeId, sessionId, messageId, options);
271
281
  }
272
282
 
273
283
  public listSlashCommands(
package/ts/errors.ts CHANGED
@@ -68,6 +68,21 @@ export class FlexHarnessSteerRejectedError extends FlexHarnessError {
68
68
  }
69
69
  }
70
70
 
71
+ /**
72
+ * A prompt names a `promptId` that an outstanding prompt of the same session, queued or running,
73
+ * already holds. A finished prompt's id is free again: its user message in the transcript shows
74
+ * that it ran.
75
+ */
76
+ export class FlexHarnessDuplicatePromptError extends FlexHarnessError {
77
+ public readonly promptId: string;
78
+
79
+ constructor(promptId: string) {
80
+ super('FLEX_PROMPT_DUPLICATE', `Prompt "${promptId}" is already outstanding in this session.`);
81
+ this.name = 'FlexHarnessDuplicatePromptError';
82
+ this.promptId = promptId;
83
+ }
84
+ }
85
+
71
86
  /** An operation would change the history or model context of an imported, read-only session. */
72
87
  export class FlexHarnessImportedSessionError extends FlexHarnessError {
73
88
  public readonly sessionId: string;
package/ts/index.ts CHANGED
@@ -3,6 +3,12 @@ export * from './classes.stores.js';
3
3
  export { FlexToolCatalog } from './classes.toolcatalog.js';
4
4
  export * from './errors.js';
5
5
  export * from './interfaces.js';
6
+ export {
7
+ AGENT_CONTEXT_COMPACTION_CEILING_PERCENT,
8
+ getAgentContextCompactionCeiling,
9
+ resolveAgentContextCompactionPoint,
10
+ } from './plugins.js';
11
+ export type { IAgentContextCompactionInput, TAgentContextCompactionPoint } from './plugins.js';
6
12
  export {
7
13
  createFlexToolPermissionRequester,
8
14
  type IFlexToolPermissionRequesterOptions,
@@ -412,6 +412,8 @@ export interface IQueuedPrompt {
412
412
  debounceMs?: number;
413
413
  /** A cache-warm prompt of the session's cache keeper; see `keepCacheWarm()`. */
414
414
  cacheWarm?: true;
415
+ /** The prompt's `promptId`; kept after its payload is released. */
416
+ promptId?: string;
415
417
  startedAt?: string;
416
418
  prompt?: INormalizedFlexPrompt;
417
419
  options?: IFlexPromptOptions;
package/ts/interfaces.ts CHANGED
@@ -92,8 +92,36 @@ export interface IFlexSessionMetricsState {
92
92
  reportedLifetimeCacheWriteTokens?: number;
93
93
  /** The breakdown counts every call since the session began, without saturation. */
94
94
  lifetimeTokenBreakdownComplete?: boolean;
95
+ /** The served context window of the last settled run's model; see `IFlexSessionMetrics`. */
96
+ contextWindow?: number;
97
+ /** The last settled run's compaction point; exclusive with `contextCompactionOff`. */
98
+ contextCompactionTokens?: number;
99
+ /** Why the last settled run had no compaction point; exclusive with `contextCompactionTokens`. */
100
+ contextCompactionOff?: TFlexContextCompactionOff;
95
101
  }
96
102
 
103
+ /**
104
+ * A session's choice of its proactive compaction point, in tokens of the model in use.
105
+ *
106
+ * - `{ tokens }`: a positive integer, clamped at run time to the ceiling of the run's model (90%
107
+ * of its served context window). Without a known served window it applies unclamped.
108
+ * - `{ percent }`: an integer from 1 through 90 of the served window. Without a known served
109
+ * window the run has no proactive compaction.
110
+ * - `false`: no proactive compaction.
111
+ *
112
+ * Omitted, the model's default applies: the catalog's automatic compaction point
113
+ * (`IFlexResolvedModel.contextCompactionDefaultTokens`) when stated, at most the ceiling,
114
+ * otherwise the ceiling itself.
115
+ */
116
+ export type TFlexContextCompactionSetting = plugins.TAgentContextCompactionSetting;
117
+
118
+ /**
119
+ * Why a run had no proactive compaction point: the setting is `false` (`disabled`), the run's
120
+ * model states no served context window and the setting needs one (`no-context-window`), or the
121
+ * harness has no `contextCompactor` (`no-compactor`).
122
+ */
123
+ export type TFlexContextCompactionOff = plugins.TAgentContextCompactionOff | 'no-compactor';
124
+
97
125
  /** No member estimates current context size or includes child-session / tool-owned model calls. */
98
126
  export interface IFlexSessionMetrics {
99
127
  /** Sum of durably settled per-call reports; a lower bound when incomplete. */
@@ -120,6 +148,19 @@ export interface IFlexSessionMetrics {
120
148
  * the session's calls were counted by input and cache since it began.
121
149
  */
122
150
  lifetimeTokenBreakdownComplete: boolean;
151
+ /**
152
+ * The served context window, in tokens, of the model the last settled run resolved; absent when
153
+ * that model states none or before the first run.
154
+ */
155
+ contextWindow?: number;
156
+ /**
157
+ * The proactive compaction point the last settled run used, in tokens. Exactly one of it and
158
+ * `contextCompactionOff` is present once a run settled; neither before. A changed session
159
+ * setting shows from the next run; `resolveAgentContextCompactionPoint` states it before.
160
+ */
161
+ contextCompactionTokens?: number;
162
+ /** Why the last settled run had no proactive compaction point. */
163
+ contextCompactionOff?: TFlexContextCompactionOff;
123
164
  }
124
165
 
125
166
  export interface IFlexModelIdentity {
@@ -222,6 +263,21 @@ export interface IFlexMessage {
222
263
  rejectedPermissionId?: string;
223
264
  /** On a user message that steered its running run: the caller's steer identity. */
224
265
  steerId?: string;
266
+ /** On the user message that heads a run: the caller's prompt identity, when the prompt named one. */
267
+ promptId?: string;
268
+ /**
269
+ * On a user message, once its run reached its terminal state: whether the run's result kept this
270
+ * message in the model context of later turns, the value the run's `keptInContext` reports. It
271
+ * records that outcome and is never rewritten afterwards: an undo is reported by `undone`, and a
272
+ * later compaction summarizes the message, which was still delivered. Absent on a message whose
273
+ * run has not ended, and on messages written before FlexHarness 15.0.0.
274
+ */
275
+ keptInContext?: boolean;
276
+ /**
277
+ * Set only by a read with `includeUndone`: the message belongs to an undone turn that a redo can
278
+ * bring back. Never stored.
279
+ */
280
+ undone?: true;
225
281
  /**
226
282
  * On both messages of a cache-warm turn: the harness sent the prompt only to keep the model's
227
283
  * prompt cache warm. Hosts leave these out of what they show and report as work.
@@ -268,6 +324,8 @@ export interface IFlexSession {
268
324
  depth?: number;
269
325
  /** Set only on a session the staged session import created; such a session is read-only. */
270
326
  origin?: IFlexSessionOrigin;
327
+ /** The session's compaction setting; absent for the harness default. */
328
+ contextCompaction?: TFlexContextCompactionSetting;
271
329
  }
272
330
 
273
331
  /** The position of an imported session when one source conversation was split into several sessions. */
@@ -394,14 +452,25 @@ export interface IFlexCreateSessionOptions {
394
452
  sessionGenerationId?: string;
395
453
  configurationRef?: string;
396
454
  title?: string;
455
+ /** The session's compaction setting; omitted for the harness default. */
456
+ contextCompaction?: TFlexContextCompactionSetting;
397
457
  }
398
458
 
399
459
  export interface IFlexUpdateSessionOptions {
400
460
  title?: string | null;
401
461
  archived?: boolean;
462
+ /** The session's compaction setting; `null` returns the session to the harness default. */
463
+ contextCompaction?: TFlexContextCompactionSetting | null;
402
464
  }
403
465
 
404
466
  export interface IFlexPromptOptions {
467
+ /**
468
+ * The caller's identity for this prompt, a non-empty string of at most 512 UTF-8 bytes. It is on
469
+ * the prompt's queue entry and on the user message that heads its run, so a caller can tell
470
+ * exactly whether the prompt was taken in. An outstanding prompt of the session holding the same
471
+ * id rejects the admission with `FlexHarnessDuplicatePromptError`.
472
+ */
473
+ promptId?: string;
405
474
  modelHint?: string;
406
475
  system?: string;
407
476
  /** Optional positive safe integer step cap; no limit unless configured. */
@@ -528,6 +597,8 @@ export interface IFlexPromptQueueEntry {
528
597
  status: TFlexPromptQueueStatus;
529
598
  queuedAt: string;
530
599
  runId?: string;
600
+ /** The prompt's `promptId`, when it named one. */
601
+ promptId?: string;
531
602
  scheduleKey?: string;
532
603
  /** The entry is a cache-warm prompt; see {@link IFlexMessage.cacheWarm}. */
533
604
  cacheWarm?: true;
@@ -606,7 +677,16 @@ export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
606
677
  scheduleKey: string;
607
678
  }
608
679
 
609
- export interface IFlexMessagePageOptions {
680
+ /** Options of the transcript reads. */
681
+ export interface IFlexMessageReadOptions {
682
+ /**
683
+ * Also return the messages of undone turns, each marked `undone: true`, in their place after the
684
+ * visible ones. Messages a committed branch removed are gone and never returned.
685
+ */
686
+ includeUndone?: boolean;
687
+ }
688
+
689
+ export interface IFlexMessagePageOptions extends IFlexMessageReadOptions {
610
690
  limit?: number;
611
691
  before?: string;
612
692
  }
@@ -669,6 +749,19 @@ export interface IFlexResolvedModel {
669
749
  cache?: TFlexAgentCacheSetting;
670
750
  /** Optional positive safe integer step cap; no limit unless configured. */
671
751
  maxSteps?: number;
752
+ /**
753
+ * The model's served context window in tokens, from the provider's catalog: the window the
754
+ * provider serves by default, never an extended maximum (for a Codex catalog `context_window`,
755
+ * not `max_context_window`; for Anthropic's Models API `max_input_tokens`). Omitted when the
756
+ * catalog states none: the run then has a compaction point only from a `{ tokens }` setting.
757
+ */
758
+ contextWindow?: number;
759
+ /**
760
+ * The catalog's own automatic compaction point in tokens, when it states one (a Codex catalog's
761
+ * `auto_compact_token_limit`). It lowers the default point, never the ceiling. Requires
762
+ * `contextWindow`.
763
+ */
764
+ contextCompactionDefaultTokens?: number;
672
765
  }
673
766
 
674
767
  export interface IFlexModelResolver<TScope> {
@@ -1891,7 +1984,11 @@ export type TFlexAgentContextCompactor<TScope = unknown> = (
1891
1984
  export interface IFlexAgentSessionPolicy<TScope = unknown> {
1892
1985
  contextBuilder?: plugins.TAgentContextBuilder;
1893
1986
  contextCompactor?: TFlexAgentContextCompactor<TScope>;
1894
- contextCompactionBytes?: plugins.IAgentSessionOptions['contextCompactionBytes'];
1987
+ /**
1988
+ * The harness default compaction setting, for sessions without their own. Omitted, the model's
1989
+ * default applies. Requires `contextCompactor` unless `false`.
1990
+ */
1991
+ contextCompaction?: TFlexContextCompactionSetting;
1895
1992
  eventRetention?: TFlexAgentSessionOptions['eventRetention'];
1896
1993
  changeListenerTimeoutMs?: number;
1897
1994
  maxPendingSessionChanges?: number;
package/ts/plugins.ts CHANGED
@@ -92,6 +92,7 @@ export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> =>
92
92
 
93
93
  // @push.rocks scope
94
94
  import {
95
+ AGENT_CONTEXT_COMPACTION_CEILING_PERCENT,
95
96
  AgentEventStoreConflictError,
96
97
  AgentPendingInputQueue,
97
98
  applyAgentEventChange,
@@ -100,13 +101,16 @@ import {
100
101
  AgentPendingInputRejectedError,
101
102
  AgentSession,
102
103
  AgentToolCallScheduler,
104
+ assertAgentContextCompactionSetting,
103
105
  buildModelMessages,
106
+ getAgentContextCompactionCeiling,
104
107
  ToolJobStoreConflictError,
105
108
  createAgentEvent,
106
109
  filterModelVisibleAgentEvents,
107
110
  getAgentGenerationTransactions,
108
111
  isGenerationInModelContext,
109
112
  modelMessagesToAgentEvents,
113
+ resolveAgentContextCompactionPoint,
110
114
  tool,
111
115
  validateAgentEventArchive,
112
116
  validateAgentEventSnapshot,
@@ -114,6 +118,7 @@ import {
114
118
  } from '@modelprofile.com/flexharness-agent';
115
119
 
116
120
  export {
121
+ AGENT_CONTEXT_COMPACTION_CEILING_PERCENT,
117
122
  AgentEventStoreConflictError,
118
123
  AgentPendingInputQueue,
119
124
  applyAgentEventChange,
@@ -122,13 +127,16 @@ export {
122
127
  AgentPendingInputRejectedError,
123
128
  AgentSession,
124
129
  AgentToolCallScheduler,
130
+ assertAgentContextCompactionSetting,
125
131
  buildModelMessages,
132
+ getAgentContextCompactionCeiling,
126
133
  ToolJobStoreConflictError,
127
134
  createAgentEvent,
128
135
  filterModelVisibleAgentEvents,
129
136
  getAgentGenerationTransactions,
130
137
  isGenerationInModelContext,
131
138
  modelMessagesToAgentEvents,
139
+ resolveAgentContextCompactionPoint,
132
140
  tool,
133
141
  validateAgentEventArchive,
134
142
  validateAgentEventSnapshot,
@@ -141,6 +149,7 @@ export type {
141
149
  TSmartAiCacheBudgetBasis,
142
150
  IAgentEventArchive,
143
151
  IAgentEventChange,
152
+ IAgentContextCompactionInput,
144
153
  IAgentContextCompactionOptions,
145
154
  IAgentEventSnapshot,
146
155
  IAgentEventStore,
@@ -165,6 +174,9 @@ export type {
165
174
  IToolJobStore,
166
175
  IToolPermissionRequest,
167
176
  TAgentContextBuilder,
177
+ TAgentContextCompactionOff,
178
+ TAgentContextCompactionPoint,
179
+ TAgentContextCompactionSetting,
168
180
  TAgentContextCompactor,
169
181
  TAgentEvent,
170
182
  TAgentGenerationOutcome,