@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
@@ -16,7 +16,8 @@ import {
16
16
  type IFlexMessage,
17
17
  type IFlexPermissionRequestInput,
18
18
  type IFlexPromptOptions,
19
- type IFlexPromptResult,
19
+ type IFlexContinuePromptResult,
20
+ type TFlexAgentPrompt,
20
21
  type IFlexResolvedModel,
21
22
  type IFlexRunFinishedEvent,
22
23
  type IFlexSession,
@@ -94,9 +95,7 @@ export class FlexHarnessRuns<TScope> {
94
95
  for (const steer of steers) {
95
96
  steering.input.add({
96
97
  inputId: steer.steerId,
97
- content: cloneSerializable(steer.prompt.modelMessage.content) as Parameters<
98
- plugins.IAgentSession['beginGeneration']
99
- >[0],
98
+ content: cloneSerializable(steer.prompt.modelMessage.content) as TFlexAgentPrompt,
100
99
  });
101
100
  steering.accepted.set(steer.steerId, { prompt: steer.prompt, byteSize: steer.byteSize });
102
101
  }
@@ -123,7 +122,7 @@ export class FlexHarnessRuns<TScope> {
123
122
  sessionId: queued.sessionId,
124
123
  queueId: queued.queueId,
125
124
  runId: queued.runId!,
126
- userMessageId: plugins.crypto.randomUUID(),
125
+ ...(queued.continuation ? { continuation: true as const } : { userMessageId: plugins.crypto.randomUUID() }),
127
126
  assistantMessageId: plugins.crypto.randomUUID(),
128
127
  controller: new AbortController(),
129
128
  ...(queued.scheduleKey === undefined
@@ -162,7 +161,7 @@ export class FlexHarnessRuns<TScope> {
162
161
  public async executeRun(
163
162
  run: IActiveRun,
164
163
  generation: Promise<plugins.IAgentGenerateResult>,
165
- ): Promise<IFlexPromptResult> {
164
+ ): Promise<IFlexContinuePromptResult> {
166
165
  let generated = false;
167
166
  try {
168
167
  // The transactional generation settles only after its model calls are reported, so
@@ -186,7 +185,7 @@ export class FlexHarnessRuns<TScope> {
186
185
  if (!run.modelResolution) throw new FlexHarnessValidationError('Generation completed without a resolved model.');
187
186
  const closeReversion = await this.core.reversions.prepareRunReversionClose(run, 'completed');
188
187
  await this.core.admission.closeRunDelegatedRunAdmission(run);
189
- const terminal = this.buildTerminal(run, 'completed', result);
188
+ let terminal = this.buildTerminal(run, 'completed', result);
190
189
  try {
191
190
  // The reversion segment closes in the save that stages the terminal.
192
191
  await this.core.mutateProjection(run.state, run.stored, () => {
@@ -209,6 +208,7 @@ export class FlexHarnessRuns<TScope> {
209
208
  throw projected;
210
209
  }
211
210
  run.steering.keptInContext = true;
211
+ terminal = this.withKeptInContext(terminal, true);
212
212
  this.core.reversions.refreshAutomaticArchiveHorizon(run.stored);
213
213
  run.phase = 'promoting';
214
214
  const closePublication = this.core.openPublication(run.state, run.sessionId);
@@ -235,7 +235,7 @@ export class FlexHarnessRuns<TScope> {
235
235
  return {
236
236
  runId: run.runId,
237
237
  sessionId: run.sessionId,
238
- userMessage: publicSnapshot(terminal.userMessage),
238
+ ...(terminal.userMessage ? { userMessage: publicSnapshot(terminal.userMessage) } : {}),
239
239
  assistantMessage: publicSnapshot(terminal.assistantMessage),
240
240
  model: publicSnapshot(terminal.model!),
241
241
  usage: publicSnapshot(terminal.usage!),
@@ -351,7 +351,10 @@ export class FlexHarnessRuns<TScope> {
351
351
  const terminalError = publicStatus === 'cancelled'
352
352
  ? run.ownerCancellation ?? this.core.trustInternalError(new FlexHarnessAbortError())
353
353
  : safeError;
354
- const terminal = this.buildTerminal(run, publicStatus, undefined, terminalError);
354
+ const terminal = this.withKeptInContext(
355
+ this.buildTerminal(run, publicStatus, undefined, terminalError),
356
+ run.steering.keptInContext === true,
357
+ );
355
358
  if (!this.core.isTombstoned(run)) {
356
359
  const closePublication = this.core.openPublication(run.state, run.sessionId);
357
360
  try {
@@ -564,6 +567,21 @@ export class FlexHarnessRuns<TScope> {
564
567
  } catch (error) {
565
568
  throw this.core.projectExternalError(run, error, 'toolProvider');
566
569
  }
570
+ const contextCompaction = run.stored.session.contextCompaction ?? this.core.agentSessionPolicy.contextCompaction;
571
+ if (run.metricsOperation) {
572
+ run.metricsOperation.contextCompaction = {
573
+ ...(model!.contextWindow === undefined ? {} : { contextWindow: model!.contextWindow }),
574
+ point: this.core.agentSessionPolicy.contextCompactor
575
+ ? plugins.resolveAgentContextCompactionPoint({
576
+ ...(model!.contextWindow === undefined ? {} : { contextWindow: model!.contextWindow }),
577
+ ...(model!.contextCompactionDefaultTokens === undefined
578
+ ? {}
579
+ : { contextCompactionDefaultTokens: model!.contextCompactionDefaultTokens }),
580
+ ...(contextCompaction === undefined ? {} : { setting: contextCompaction }),
581
+ })
582
+ : { off: 'no-compactor' },
583
+ };
584
+ }
567
585
  let closed = false;
568
586
  let closePromise: Promise<void> | undefined;
569
587
  const close = toolHandle?.close
@@ -596,6 +614,11 @@ export class FlexHarnessRuns<TScope> {
596
614
  ...(model!.cache === undefined ? {} : { cache: model!.cache }),
597
615
  ...(options.maxSteps ?? model!.maxSteps ? { maxSteps: options.maxSteps ?? model!.maxSteps } : {}),
598
616
  ...(run.cacheWarm ? { maxOutputTokens: this.core.cacheWarmOptions.maxOutputTokens } : {}),
617
+ ...(model!.contextWindow === undefined ? {} : { contextWindow: model!.contextWindow }),
618
+ ...(model!.contextCompactionDefaultTokens === undefined
619
+ ? {}
620
+ : { contextCompactionDefaultTokens: model!.contextCompactionDefaultTokens }),
621
+ ...(contextCompaction === undefined ? {} : { contextCompaction }),
599
622
  ...(close ? { close } : {}),
600
623
  };
601
624
  } catch (error) {
@@ -705,17 +728,19 @@ export class FlexHarnessRuns<TScope> {
705
728
 
706
729
  public createReservation(
707
730
  run: IActiveRun,
708
- prompt: INormalizedFlexPrompt,
709
- ): { userMessage: IFlexMessage; assistantMessage: IFlexMessage } {
731
+ prompt: INormalizedFlexPrompt | undefined,
732
+ promptId?: string,
733
+ ): { userMessage?: IFlexMessage; assistantMessage: IFlexMessage } {
710
734
  const timestamp = new Date().toISOString();
711
- const userMessage: IFlexMessage = {
712
- messageId: run.userMessageId,
735
+ const userMessage: IFlexMessage | undefined = prompt === undefined ? undefined : {
736
+ messageId: run.userMessageId!,
713
737
  sessionId: run.sessionId,
714
738
  runId: run.runId,
715
739
  role: 'user',
716
740
  status: 'streaming',
717
741
  createdAt: timestamp,
718
742
  parts: this.userMessageParts(prompt),
743
+ ...(promptId === undefined ? {} : { promptId }),
719
744
  ...(run.cacheWarm ? { cacheWarm: true as const } : {}),
720
745
  };
721
746
  const assistantMessage: IFlexMessage = {
@@ -728,7 +753,7 @@ export class FlexHarnessRuns<TScope> {
728
753
  parts: [],
729
754
  ...(run.cacheWarm ? { cacheWarm: true as const } : {}),
730
755
  };
731
- return { userMessage, assistantMessage };
756
+ return { ...(userMessage ? { userMessage } : {}), assistantMessage };
732
757
  }
733
758
 
734
759
  private userMessageParts(prompt: INormalizedFlexPrompt): TFlexMessagePart[] {
@@ -889,11 +914,11 @@ export class FlexHarnessRuns<TScope> {
889
914
  error?: Error,
890
915
  ): IFlexTerminalProjection {
891
916
  const timestamp = new Date().toISOString();
892
- const userMessage: IFlexMessage = cloneSerializable(
917
+ const userMessage = run.continuation ? undefined : cloneSerializable(
893
918
  run.reservedUserMessage
894
919
  ?? run.stored.messages.find((message) => message.messageId === run.userMessageId)
895
920
  ?? {
896
- messageId: run.userMessageId,
921
+ messageId: run.userMessageId!,
897
922
  sessionId: run.sessionId,
898
923
  runId: run.runId,
899
924
  role: 'user',
@@ -931,11 +956,13 @@ export class FlexHarnessRuns<TScope> {
931
956
  }
932
957
  }
933
958
  if (run.cacheWarm) {
934
- userMessage.cacheWarm = true;
959
+ if (userMessage) userMessage.cacheWarm = true;
935
960
  assistantMessage.cacheWarm = true;
936
961
  }
937
- userMessage.status = status;
938
- userMessage.completedAt = timestamp;
962
+ if (userMessage) {
963
+ userMessage.status = status;
964
+ userMessage.completedAt = timestamp;
965
+ }
939
966
  assistantMessage.status = status;
940
967
  assistantMessage.completedAt = timestamp;
941
968
  if (run.modelResolution) assistantMessage.model = cloneSerializable(run.modelResolution.identity);
@@ -958,6 +985,7 @@ export class FlexHarnessRuns<TScope> {
958
985
  // A failed turn fails its prompt too. A stopped turn is no fault of its prompt, so the prompt
959
986
  // stays an ordinary user message and only the answer it stopped carries the stop.
960
987
  for (const failedMessage of status === 'cancelled' ? [assistantMessage] : [userMessage, assistantMessage]) {
988
+ if (!failedMessage) continue;
961
989
  failedMessage.error = message;
962
990
  if (limit) failedMessage.limit = cloneSerializable(limit);
963
991
  if (rejectedPermissionId) failedMessage.rejectedPermissionId = rejectedPermissionId;
@@ -966,7 +994,7 @@ export class FlexHarnessRuns<TScope> {
966
994
  return {
967
995
  runId: run.runId,
968
996
  status,
969
- userMessage,
997
+ ...(userMessage ? { userMessage } : {}),
970
998
  assistantMessage,
971
999
  ...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
972
1000
  ...(status === 'completed' && result
@@ -1029,6 +1057,16 @@ export class FlexHarnessRuns<TScope> {
1029
1057
  }, true);
1030
1058
  }
1031
1059
 
1060
+ /**
1061
+ * The terminal with its prompt's `keptInContext`: whether the run's result kept the prompt and
1062
+ * the steers it took in in the model context. `replaceVisibleRun()` writes it onto the steers.
1063
+ */
1064
+ public withKeptInContext(terminal: IFlexTerminalProjection, keptInContext: boolean): IFlexTerminalProjection {
1065
+ return terminal.userMessage
1066
+ ? { ...terminal, userMessage: { ...terminal.userMessage, keptInContext } }
1067
+ : terminal;
1068
+ }
1069
+
1032
1070
  private replaceStagedTerminal(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
1033
1071
  stored.stagedTerminals = stored.stagedTerminals.filter((entry) => entry.runId !== terminal.runId);
1034
1072
  stored.stagedTerminals.push(cloneSerializable(terminal));
@@ -1042,20 +1080,29 @@ export class FlexHarnessRuns<TScope> {
1042
1080
  */
1043
1081
  public replaceVisibleRun(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
1044
1082
  const replacements = new Map([
1045
- [terminal.userMessage.messageId, terminal.userMessage],
1083
+ ...(terminal.userMessage ? [[terminal.userMessage.messageId, terminal.userMessage] as const] : []),
1046
1084
  [terminal.assistantMessage.messageId, terminal.assistantMessage],
1047
1085
  ]);
1048
1086
  const placed = new Set<string>();
1087
+ const keptInContext = terminal.userMessage?.keptInContext
1088
+ ?? (terminal.status === 'completed'
1089
+ || plugins.isGenerationInModelContext(stored.agentSession.getEvents(), terminal.runId));
1049
1090
  stored.messages = stored.messages.map((message) => {
1050
1091
  const replacement = message.runId === terminal.runId
1051
1092
  ? replacements.get(message.messageId)
1052
1093
  : undefined;
1053
- if (!replacement) return message;
1094
+ if (!replacement) {
1095
+ // The steers the run took in share its prompt's outcome.
1096
+ return keptInContext !== undefined && message.runId === terminal.runId && message.role === 'user'
1097
+ && message.keptInContext !== keptInContext
1098
+ ? { ...cloneSerializable(message), keptInContext }
1099
+ : message;
1100
+ }
1054
1101
  placed.add(replacement.messageId);
1055
1102
  return cloneSerializable(replacement);
1056
1103
  });
1057
1104
  const firstIndex = stored.messages.findIndex((message) => message.runId === terminal.runId);
1058
- if (!placed.has(terminal.userMessage.messageId)) {
1105
+ if (terminal.userMessage && !placed.has(terminal.userMessage.messageId)) {
1059
1106
  stored.messages.splice(
1060
1107
  firstIndex < 0 ? stored.messages.length : firstIndex,
1061
1108
  0,
@@ -1089,7 +1136,14 @@ export class FlexHarnessRuns<TScope> {
1089
1136
  this.core.emitPartEvent(run, 'part.completed', part);
1090
1137
  }
1091
1138
  }
1092
- for (const message of [terminal.userMessage, terminal.assistantMessage]) {
1139
+ const retentionKnown = terminal.userMessage
1140
+ ? terminal.userMessage.keptInContext !== undefined
1141
+ : run.steering.keptInContext !== undefined;
1142
+ const steerMessages = retentionKnown
1143
+ ? run.stored.messages.filter((message) => message.runId === run.runId && message.steerId !== undefined)
1144
+ : [];
1145
+ for (const message of [terminal.userMessage, ...steerMessages, terminal.assistantMessage]) {
1146
+ if (!message) continue;
1093
1147
  this.core.emitEvent(run.scopeId, run.stored.session, {
1094
1148
  type: 'message.updated',
1095
1149
  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(
@@ -13,6 +13,7 @@ import type {
13
13
  IFlexSteerAdmission,
14
14
  IFlexSteerInput,
15
15
  IFlexPromptAdmission,
16
+ IFlexContinuePromptResult,
16
17
  IFlexPromptOptions,
17
18
  IFlexSession,
18
19
  IFlexSubagentDefinition,
@@ -378,7 +379,7 @@ export class FlexHarnessSubagents<TScope> {
378
379
  let child: IStoredSessionState | undefined;
379
380
  let childCreated = false;
380
381
  let queued: Awaited<ReturnType<typeof this.core.prompts.enqueuePromptInternal>> | undefined;
381
- let admission: IFlexPromptAdmission | undefined;
382
+ let admission: IFlexPromptAdmission<IFlexContinuePromptResult> | undefined;
382
383
  const abortChild = () => {
383
384
  const reason = delegatedCancellation(run.controller.signal.reason)
384
385
  ?? this.core.trustInternalError(new FlexHarnessAbortError('The parent run was aborted.'));
@@ -4,6 +4,8 @@ import type {
4
4
  IFlexCacheWarmInput,
5
5
  IFlexCacheWarmState,
6
6
  IFlexCompactSessionOptions,
7
+ IFlexContinuePromptOptions,
8
+ IFlexContinuePromptResult,
7
9
  IFlexCreateProjectTaskInput,
8
10
  IFlexCreateSessionOptions,
9
11
  IFlexDeleteSessionGenerationCohortInput,
@@ -15,6 +17,7 @@ import type {
15
17
  IFlexMessage,
16
18
  IFlexMessagePage,
17
19
  IFlexMessagePageOptions,
20
+ IFlexMessageReadOptions,
18
21
  IFlexPermissionRequest,
19
22
  IFlexProjectGoalResult,
20
23
  IFlexSteerAdmission,
@@ -246,12 +249,20 @@ export class FlexHarness<TScope = unknown> {
246
249
  * with the instance's event sequence they include. It waits until no queued projection or scope
247
250
  * write and no change that emits events for the session is in progress.
248
251
  */
249
- public getSessionView(scopeId: string, sessionId: string): Promise<IFlexSessionView> {
250
- return this.core.sessions.getSessionView(scopeId, sessionId);
252
+ public getSessionView(
253
+ scopeId: string,
254
+ sessionId: string,
255
+ options?: IFlexMessageReadOptions,
256
+ ): Promise<IFlexSessionView> {
257
+ return this.core.sessions.getSessionView(scopeId, sessionId, options);
251
258
  }
252
259
 
253
- public getMessages(scopeId: string, sessionId: string): Promise<IFlexMessage[]> {
254
- return this.core.sessions.getMessages(scopeId, sessionId);
260
+ public getMessages(
261
+ scopeId: string,
262
+ sessionId: string,
263
+ options?: IFlexMessageReadOptions,
264
+ ): Promise<IFlexMessage[]> {
265
+ return this.core.sessions.getMessages(scopeId, sessionId, options);
255
266
  }
256
267
 
257
268
  public listMessagePage(
@@ -266,8 +277,9 @@ export class FlexHarness<TScope = unknown> {
266
277
  scopeId: string,
267
278
  sessionId: string,
268
279
  messageId: string,
280
+ options?: IFlexMessageReadOptions,
269
281
  ): Promise<IFlexMessage> {
270
- return this.core.sessions.getMessage(scopeId, sessionId, messageId);
282
+ return this.core.sessions.getMessage(scopeId, sessionId, messageId, options);
271
283
  }
272
284
 
273
285
  public listSlashCommands(
@@ -310,6 +322,14 @@ export class FlexHarness<TScope = unknown> {
310
322
  return this.core.prompts.prompt(scopeId, sessionId, prompt, options);
311
323
  }
312
324
 
325
+ public continuePrompt(
326
+ scopeId: string,
327
+ sessionId: string,
328
+ options: IFlexContinuePromptOptions = {},
329
+ ): Promise<IFlexPromptQueueAdmission<IFlexContinuePromptResult>> {
330
+ return this.core.prompts.continuePrompt(scopeId, sessionId, options);
331
+ }
332
+
313
333
  public startPrompt(
314
334
  scopeId: string,
315
335
  sessionId: string,
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,
@@ -15,7 +15,7 @@ import type {
15
15
  IFlexPromptAdmission,
16
16
  IFlexPromptQueueEntry,
17
17
  IFlexPromptOptions,
18
- IFlexPromptResult,
18
+ IFlexContinuePromptResult,
19
19
  IFlexReversionSegment,
20
20
  IFlexPendingReversionRelease,
21
21
  IFlexResolvedModel,
@@ -88,7 +88,7 @@ export interface IStoredSessionState {
88
88
  permissionRevision: number;
89
89
  permissionQueue: Promise<void>;
90
90
  eventStore: plugins.IAgentEventStore;
91
- agentSession: plugins.IAgentSession;
91
+ agentSession: plugins.IAgentContinuationSession;
92
92
  agentSessionAbortCompleted: boolean;
93
93
  agentSessionCloseCompleted: boolean;
94
94
  agentEventStoreReleased: boolean;
@@ -226,7 +226,8 @@ export interface IActiveRun {
226
226
  sessionId: string;
227
227
  queueId: string;
228
228
  runId: string;
229
- userMessageId: string;
229
+ userMessageId?: string;
230
+ continuation?: true;
230
231
  assistantMessageId: string;
231
232
  controller: AbortController;
232
233
  transaction?: plugins.IAgentGenerationHandle;
@@ -292,7 +293,7 @@ export interface IActiveRun {
292
293
  permissionPolicy?: IFlexResolvedSessionConfiguration<unknown>['permissionPolicy'];
293
294
  delegatedRunAdmissionOwner?: IDelegatedRunAdmissionOwner;
294
295
  delegatedRunAdmissionClose?: Promise<void>;
295
- completion: Promise<IFlexPromptResult>;
296
+ completion: Promise<IFlexContinuePromptResult>;
296
297
  steering: IRunSteering;
297
298
  /** Stops the run's generation for its owner, so what the model produced stays in context. */
298
299
  stop: AbortController;
@@ -412,7 +413,10 @@ export interface IQueuedPrompt {
412
413
  debounceMs?: number;
413
414
  /** A cache-warm prompt of the session's cache keeper; see `keepCacheWarm()`. */
414
415
  cacheWarm?: true;
416
+ /** The prompt's `promptId`; kept after its payload is released. */
417
+ promptId?: string;
415
418
  startedAt?: string;
419
+ continuation?: true;
416
420
  prompt?: INormalizedFlexPrompt;
417
421
  options?: IFlexPromptOptions;
418
422
  delegationSeed?: IDelegatedRunAdmissionSeed;
@@ -424,11 +428,11 @@ export interface IQueuedPrompt {
424
428
  steering?: IRunSteering;
425
429
  /** The steers admitted with the prompt, in their given order. */
426
430
  admittedSteerIds?: readonly string[];
427
- completion: Promise<IFlexPromptResult>;
428
- resolveCompletion: (result: IFlexPromptResult) => void;
431
+ completion: Promise<IFlexContinuePromptResult>;
432
+ resolveCompletion: (result: IFlexContinuePromptResult) => void;
429
433
  rejectCompletion: (error: unknown) => void;
430
- started: Promise<IFlexPromptAdmission>;
431
- resolveStarted: (admission: IFlexPromptAdmission) => void;
434
+ started: Promise<IFlexPromptAdmission<IFlexContinuePromptResult>>;
435
+ resolveStarted: (admission: IFlexPromptAdmission<IFlexContinuePromptResult>) => void;
432
436
  rejectStarted: (error: unknown) => void;
433
437
  }
434
438