@modelprofile.com/flexharness 13.3.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.callbacks.d.ts +3 -4
- package/dist_ts/classes.flexharness.callbacks.js +5 -14
- package/dist_ts/classes.flexharness.core.d.ts +8 -1
- package/dist_ts/classes.flexharness.core.js +31 -12
- package/dist_ts/classes.flexharness.d.ts +4 -4
- package/dist_ts/classes.flexharness.js +7 -7
- package/dist_ts/classes.flexharness.prompts.js +11 -3
- package/dist_ts/classes.flexharness.reversions.js +9 -3
- package/dist_ts/classes.flexharness.runs.d.ts +6 -1
- package/dist_ts/classes.flexharness.runs.js +45 -7
- package/dist_ts/classes.flexharness.sessions.d.ts +4 -5
- package/dist_ts/classes.flexharness.sessions.js +45 -12
- 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 +101 -5
- package/dist_ts/interfaces.flexharness.d.ts +2 -0
- 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 +43 -1
- 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 +33 -11
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.callbacks.ts +4 -12
- package/ts/classes.flexharness.core.ts +36 -9
- package/ts/classes.flexharness.prompts.ts +11 -1
- package/ts/classes.flexharness.reversions.ts +10 -2
- package/ts/classes.flexharness.runs.ts +48 -4
- package/ts/classes.flexharness.sessions.ts +54 -9
- package/ts/classes.flexharness.ts +15 -5
- package/ts/errors.ts +15 -0
- package/ts/index.ts +6 -0
- package/ts/interfaces.flexharness.ts +2 -0
- package/ts/interfaces.ts +104 -5
- package/ts/plugins.ts +12 -0
- package/ts/readme.md +33 -11
- package/ts/utils.harnessoptions.ts +9 -2
- package/ts/utils.json.ts +41 -0
- package/ts/utils.sessionmetrics.ts +23 -0
- package/ts/utils.transfer.ts +13 -0
- package/ts/utils.validation.ts +31 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import {
|
|
3
3
|
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessDuplicatePromptError,
|
|
4
5
|
FlexHarnessNotFoundError,
|
|
5
6
|
FlexHarnessQueueFullError,
|
|
6
7
|
FlexHarnessSessionBusyError,
|
|
@@ -372,6 +373,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
372
373
|
queued?: IQueuedPrompt;
|
|
373
374
|
}> {
|
|
374
375
|
this.core.assertOpen();
|
|
376
|
+
if (options.promptId !== undefined) requireTransferIdentifier(options.promptId, 'promptId');
|
|
375
377
|
const normalizedPrompt = normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols);
|
|
376
378
|
const normalizedOptions = cloneSerializable(options);
|
|
377
379
|
const byteSize = this.promptAdmissionByteSize(prompt, options, scheduleKey, debounceMs);
|
|
@@ -459,6 +461,12 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
459
461
|
) {
|
|
460
462
|
throw new FlexHarnessSessionBusyError(sessionId, `already has scheduled key "${scheduleKey}"`);
|
|
461
463
|
}
|
|
464
|
+
if (
|
|
465
|
+
options.promptId !== undefined
|
|
466
|
+
&& [...stored.outstandingPromptsById.values()].some((entry) => entry.promptId === options.promptId)
|
|
467
|
+
) {
|
|
468
|
+
throw new FlexHarnessDuplicatePromptError(options.promptId);
|
|
469
|
+
}
|
|
462
470
|
let resolveCompletion!: (result: IFlexPromptResult) => void;
|
|
463
471
|
let rejectCompletion!: (error: unknown) => void;
|
|
464
472
|
const completion = new Promise<IFlexPromptResult>((resolve, reject) => {
|
|
@@ -485,6 +493,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
485
493
|
queuedAt: new Date().toISOString(),
|
|
486
494
|
...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
|
|
487
495
|
...(cacheWarm ? { cacheWarm: true as const } : {}),
|
|
496
|
+
...(options.promptId === undefined ? {} : { promptId: options.promptId }),
|
|
488
497
|
prompt: normalizedPrompt,
|
|
489
498
|
options: normalizedOptions,
|
|
490
499
|
...(delegationSeed === undefined ? {} : { delegationSeed }),
|
|
@@ -641,7 +650,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
641
650
|
),
|
|
642
651
|
);
|
|
643
652
|
this.assertPromptPromotion(queued, run);
|
|
644
|
-
const reservation = this.core.runs.createReservation(run, prompt);
|
|
653
|
+
const reservation = this.core.runs.createReservation(run, prompt, queued.promptId);
|
|
645
654
|
closeReservationPublication = this.core.openPublication(run.state, run.sessionId);
|
|
646
655
|
run.metricsOperation = createSessionMetricsOperation(run.runId, 'generation');
|
|
647
656
|
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
@@ -818,6 +827,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
818
827
|
status: queued.status,
|
|
819
828
|
queuedAt: queued.queuedAt,
|
|
820
829
|
...(queued.runId ? { runId: queued.runId } : {}),
|
|
830
|
+
...(queued.promptId === undefined ? {} : { promptId: queued.promptId }),
|
|
821
831
|
...(queued.scheduleKey ? { scheduleKey: queued.scheduleKey } : {}),
|
|
822
832
|
...(queued.cacheWarm ? { cacheWarm: true } : {}),
|
|
823
833
|
...(queued.startedAt ? { startedAt: queued.startedAt } : {}),
|
|
@@ -1224,8 +1224,16 @@ export class FlexHarnessReversions<TScope> {
|
|
|
1224
1224
|
const nextCandidate = candidates[1];
|
|
1225
1225
|
let end = 0;
|
|
1226
1226
|
if (!firstCandidate) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1227
|
+
// Unsuccessful turns with output still contribute to model context. Keep their
|
|
1228
|
+
// segments so the next completed turn can group (and undo) that leading history.
|
|
1229
|
+
const firstVisibleSegment = dropNonUndoablePrefix
|
|
1230
|
+
? stored.reversionSegments.findIndex((segment) => plugins.isGenerationInModelContext(
|
|
1231
|
+
stored.agentSession.getEvents(), segment.runId,
|
|
1232
|
+
))
|
|
1233
|
+
: -1;
|
|
1234
|
+
if (dropNonUndoablePrefix && firstVisibleSegment !== 0) {
|
|
1235
|
+
end = firstVisibleSegment < 0 ? stored.reversionSegments.length : firstVisibleSegment;
|
|
1236
|
+
} else if (overLimit) {
|
|
1229
1237
|
end = Math.max(
|
|
1230
1238
|
1,
|
|
1231
1239
|
stored.reversionSegments.length + (reserveSegment ? 1 : 0)
|
|
@@ -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
|
-
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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)
|
|
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.
|
|
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(
|
|
250
|
-
|
|
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(
|
|
254
|
-
|
|
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,16 +452,28 @@ 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;
|
|
476
|
+
/** Optional positive safe integer step cap; no limit unless configured. */
|
|
407
477
|
maxSteps?: number;
|
|
408
478
|
}
|
|
409
479
|
|
|
@@ -527,6 +597,8 @@ export interface IFlexPromptQueueEntry {
|
|
|
527
597
|
status: TFlexPromptQueueStatus;
|
|
528
598
|
queuedAt: string;
|
|
529
599
|
runId?: string;
|
|
600
|
+
/** The prompt's `promptId`, when it named one. */
|
|
601
|
+
promptId?: string;
|
|
530
602
|
scheduleKey?: string;
|
|
531
603
|
/** The entry is a cache-warm prompt; see {@link IFlexMessage.cacheWarm}. */
|
|
532
604
|
cacheWarm?: true;
|
|
@@ -605,7 +677,16 @@ export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
|
|
|
605
677
|
scheduleKey: string;
|
|
606
678
|
}
|
|
607
679
|
|
|
608
|
-
|
|
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 {
|
|
609
690
|
limit?: number;
|
|
610
691
|
before?: string;
|
|
611
692
|
}
|
|
@@ -666,7 +747,21 @@ export interface IFlexResolvedModel {
|
|
|
666
747
|
system?: string;
|
|
667
748
|
providerOptions?: TFlexAgentProviderOptions;
|
|
668
749
|
cache?: TFlexAgentCacheSetting;
|
|
750
|
+
/** Optional positive safe integer step cap; no limit unless configured. */
|
|
669
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;
|
|
670
765
|
}
|
|
671
766
|
|
|
672
767
|
export interface IFlexModelResolver<TScope> {
|
|
@@ -1889,7 +1984,11 @@ export type TFlexAgentContextCompactor<TScope = unknown> = (
|
|
|
1889
1984
|
export interface IFlexAgentSessionPolicy<TScope = unknown> {
|
|
1890
1985
|
contextBuilder?: plugins.TAgentContextBuilder;
|
|
1891
1986
|
contextCompactor?: TFlexAgentContextCompactor<TScope>;
|
|
1892
|
-
|
|
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;
|
|
1893
1992
|
eventRetention?: TFlexAgentSessionOptions['eventRetention'];
|
|
1894
1993
|
changeListenerTimeoutMs?: number;
|
|
1895
1994
|
maxPendingSessionChanges?: number;
|
|
@@ -1903,6 +2002,7 @@ export interface IFlexSubagentDefinition {
|
|
|
1903
2002
|
description: string;
|
|
1904
2003
|
modelHint?: string;
|
|
1905
2004
|
system?: string;
|
|
2005
|
+
/** Optional positive safe integer step cap; no limit unless configured. */
|
|
1906
2006
|
maxSteps?: number;
|
|
1907
2007
|
/** Exact model-facing names permitted for this agent; omitted means unrestricted. */
|
|
1908
2008
|
allowedTools?: readonly string[];
|
|
@@ -2120,9 +2220,8 @@ export interface IFlexPartDeltaEvent extends IFlexPartEventBase {
|
|
|
2120
2220
|
}
|
|
2121
2221
|
|
|
2122
2222
|
/**
|
|
2123
|
-
* A
|
|
2124
|
-
*
|
|
2125
|
-
* after it move up by one.
|
|
2223
|
+
* A part left its message. `partIndex` is its position before it left; the parts after it
|
|
2224
|
+
* move up by one. Failed-inference observations are retained, not removed for a retry.
|
|
2126
2225
|
*/
|
|
2127
2226
|
export interface IFlexPartRemovedEvent extends IFlexPartEventBase {
|
|
2128
2227
|
readonly type: 'part.removed';
|