@modelprofile.com/flexharness 14.0.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.cachewarm.d.ts +2 -2
- package/dist_ts/classes.flexharness.cachewarm.js +1 -1
- package/dist_ts/classes.flexharness.core.d.ts +8 -1
- package/dist_ts/classes.flexharness.core.js +44 -31
- package/dist_ts/classes.flexharness.d.ts +5 -4
- package/dist_ts/classes.flexharness.js +10 -7
- package/dist_ts/classes.flexharness.prompts.d.ts +9 -5
- package/dist_ts/classes.flexharness.prompts.js +93 -18
- package/dist_ts/classes.flexharness.runs.d.ts +9 -4
- package/dist_ts/classes.flexharness.runs.js +70 -18
- package/dist_ts/classes.flexharness.sessions.d.ts +4 -5
- package/dist_ts/classes.flexharness.sessions.js +45 -12
- package/dist_ts/classes.flexharness.subagents.js +1 -1
- package/dist_ts/errors.d.ts +9 -0
- package/dist_ts/errors.js +14 -1
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +119 -11
- package/dist_ts/interfaces.flexharness.d.ts +12 -8
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.harnessoptions.js +6 -3
- package/dist_ts/utils.json.js +74 -18
- package/dist_ts/utils.sessionmetrics.d.ts +12 -1
- package/dist_ts/utils.sessionmetrics.js +18 -1
- package/dist_ts/utils.transfer.js +14 -1
- package/dist_ts/utils.validation.d.ts +7 -2
- package/dist_ts/utils.validation.js +28 -7
- package/package.json +3 -3
- package/readme.md +75 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.cachewarm.ts +2 -2
- package/ts/classes.flexharness.core.ts +49 -30
- package/ts/classes.flexharness.prompts.ts +124 -39
- package/ts/classes.flexharness.runs.ts +78 -24
- package/ts/classes.flexharness.sessions.ts +54 -9
- package/ts/classes.flexharness.subagents.ts +2 -1
- package/ts/classes.flexharness.ts +25 -5
- package/ts/errors.ts +15 -0
- package/ts/index.ts +6 -0
- package/ts/interfaces.flexharness.ts +12 -8
- package/ts/interfaces.ts +125 -11
- package/ts/plugins.ts +13 -0
- package/ts/readme.md +75 -6
- package/ts/utils.harnessoptions.ts +9 -2
- package/ts/utils.json.ts +75 -18
- package/ts/utils.sessionmetrics.ts +23 -0
- package/ts/utils.transfer.ts +13 -0
- package/ts/utils.validation.ts +31 -5
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
type IFlexMessage,
|
|
17
17
|
type IFlexPermissionRequestInput,
|
|
18
18
|
type IFlexPromptOptions,
|
|
19
|
-
type
|
|
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
|
|
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<
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
938
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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(
|
|
@@ -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(
|
|
250
|
-
|
|
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(
|
|
254
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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<
|
|
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<
|
|
428
|
-
resolveCompletion: (result:
|
|
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
|
|