@modelprofile.com/flexharness 13.1.1 → 13.2.1
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 +64 -0
- package/dist_ts/classes.flexharness.cachewarm.js +353 -0
- package/dist_ts/classes.flexharness.callbacks.js +2 -1
- package/dist_ts/classes.flexharness.core.d.ts +9 -1
- package/dist_ts/classes.flexharness.core.js +55 -6
- package/dist_ts/classes.flexharness.d.ts +10 -1
- package/dist_ts/classes.flexharness.deletion.js +2 -1
- package/dist_ts/classes.flexharness.js +14 -1
- package/dist_ts/classes.flexharness.lifecycle.js +4 -1
- package/dist_ts/classes.flexharness.prompts.d.ts +1 -1
- package/dist_ts/classes.flexharness.prompts.js +25 -5
- package/dist_ts/classes.flexharness.reversions.d.ts +2 -0
- package/dist_ts/classes.flexharness.reversions.js +7 -2
- package/dist_ts/classes.flexharness.runs.js +23 -2
- package/dist_ts/interfaces.d.ts +87 -1
- package/dist_ts/interfaces.flexharness.d.ts +15 -0
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +1 -1
- package/dist_ts/plugins.js +1 -1
- package/dist_ts/utils.harnessoptions.d.ts +8 -1
- package/dist_ts/utils.harnessoptions.js +50 -6
- package/dist_ts/utils.json.js +11 -2
- package/dist_ts/utils.sessionpreview.d.ts +3 -3
- package/dist_ts/utils.sessionpreview.js +5 -5
- package/dist_ts_chat_harness/classes.transcript.js +17 -4
- package/package.json +3 -3
- package/readme.md +27 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.cachewarm.ts +433 -0
- package/ts/classes.flexharness.callbacks.ts +1 -0
- package/ts/classes.flexharness.core.ts +50 -4
- package/ts/classes.flexharness.deletion.ts +1 -0
- package/ts/classes.flexharness.lifecycle.ts +3 -0
- package/ts/classes.flexharness.prompts.ts +22 -3
- package/ts/classes.flexharness.reversions.ts +7 -1
- package/ts/classes.flexharness.runs.ts +19 -1
- package/ts/classes.flexharness.ts +21 -0
- package/ts/interfaces.flexharness.ts +16 -0
- package/ts/interfaces.ts +98 -1
- package/ts/plugins.ts +1 -0
- package/ts/readme.md +27 -0
- package/ts/utils.harnessoptions.ts +74 -5
- package/ts/utils.json.ts +10 -1
- package/ts/utils.sessionpreview.ts +4 -4
- package/ts_chat_harness/classes.transcript.ts +14 -3
- package/ts_chat_harness/readme.md +1 -0
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
normalizeAgentSessionPolicy,
|
|
58
58
|
normalizeAttachmentResolver,
|
|
59
59
|
normalizeBuiltInTools,
|
|
60
|
+
normalizeCacheWarmOptions,
|
|
60
61
|
normalizeSlashCommands,
|
|
61
62
|
normalizeToolCatalogOptions,
|
|
62
63
|
normalizeSubagents,
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
resolveReversionMaintenanceTimeoutMs,
|
|
69
70
|
resolveReversionLimits,
|
|
70
71
|
type INormalizedBuiltInTools,
|
|
72
|
+
type INormalizedCacheWarmOptions,
|
|
71
73
|
type TRegisteredSlashCommand,
|
|
72
74
|
} from './utils.harnessoptions.js';
|
|
73
75
|
import {
|
|
@@ -119,6 +121,7 @@ import { FlexHarnessSessionDeletion } from './classes.flexharness.deletion.js';
|
|
|
119
121
|
import { FlexHarnessLifecycle } from './classes.flexharness.lifecycle.js';
|
|
120
122
|
import { FlexHarnessRuns } from './classes.flexharness.runs.js';
|
|
121
123
|
import { FlexHarnessSessions } from './classes.flexharness.sessions.js';
|
|
124
|
+
import { FlexHarnessCacheWarm } from './classes.flexharness.cachewarm.js';
|
|
122
125
|
|
|
123
126
|
const defaultMaxSubagentDepth = 6;
|
|
124
127
|
const maximumMaxSubagentDepth = 8;
|
|
@@ -169,6 +172,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
169
172
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
170
173
|
public readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
171
174
|
public readonly builtInTools: INormalizedBuiltInTools;
|
|
175
|
+
public readonly cacheWarmOptions: INormalizedCacheWarmOptions;
|
|
172
176
|
public readonly toolCatalogOptions: Readonly<IFlexToolCatalogOptions>;
|
|
173
177
|
public readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
|
|
174
178
|
public readonly maxSubagentDepth: number;
|
|
@@ -198,6 +202,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
198
202
|
public readonly lifecycle = new FlexHarnessLifecycle<TScope>(this);
|
|
199
203
|
public readonly runs = new FlexHarnessRuns<TScope>(this);
|
|
200
204
|
public readonly sessions = new FlexHarnessSessions<TScope>(this);
|
|
205
|
+
public readonly cacheWarm = new FlexHarnessCacheWarm<TScope>(this);
|
|
201
206
|
|
|
202
207
|
constructor(options: IFlexHarnessOptions<TScope>) {
|
|
203
208
|
if (!options || typeof options !== 'object') {
|
|
@@ -243,6 +248,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
243
248
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
244
249
|
this.subagents = normalizeSubagents(options.subagents);
|
|
245
250
|
this.builtInTools = normalizeBuiltInTools(options.builtInTools);
|
|
251
|
+
this.cacheWarmOptions = normalizeCacheWarmOptions(options.cacheWarm);
|
|
246
252
|
const toolCatalogOptions = normalizeToolCatalogOptions(options.toolCatalog);
|
|
247
253
|
this.toolCatalogOptions = Object.freeze({
|
|
248
254
|
declarations: toolCatalogOptions.declarations,
|
|
@@ -963,32 +969,42 @@ export class FlexHarnessCore<TScope> {
|
|
|
963
969
|
...stored.messages.map((message) => message.runId),
|
|
964
970
|
...stored.stagedTerminals.map((terminal) => terminal.runId),
|
|
965
971
|
]);
|
|
972
|
+
// The session's state is the latest real turn's: a cache-warm turn's terminal never sets it.
|
|
966
973
|
let latestTerminal: IFlexTerminalProjection | undefined;
|
|
967
974
|
for (const runId of runIds) {
|
|
968
975
|
const outcome = outcomes.get(runId);
|
|
969
976
|
const stage = stages.get(runId);
|
|
970
977
|
if (outcome === 'accepted' && stage?.status === 'completed') {
|
|
971
978
|
this.runs.replaceVisibleRun(stored, stage);
|
|
972
|
-
latestTerminal = stage;
|
|
979
|
+
if (!stage.userMessage.cacheWarm) latestTerminal = stage;
|
|
973
980
|
} else if (outcome !== 'accepted') {
|
|
974
981
|
const visible = stored.messages.filter((message) => message.runId === runId);
|
|
975
|
-
|
|
982
|
+
// A run that failed or was stopped keeps its terminal and its error: the staged one, or the
|
|
983
|
+
// one it published. Only a run that never reached its terminal is repaired as interrupted.
|
|
984
|
+
const ended = stage && stage.status !== 'completed'
|
|
985
|
+
? stage
|
|
986
|
+
: stage ? undefined : this.publishedTerminal(runId, visible);
|
|
987
|
+
if (ended) {
|
|
988
|
+
if (ended === stage) this.runs.replaceVisibleRun(stored, stage);
|
|
989
|
+
if (!ended.userMessage.cacheWarm) latestTerminal = ended;
|
|
990
|
+
} else if (visible.length > 0 || stage) {
|
|
976
991
|
const status = outcome === 'rejected' ? 'failed' : 'cancelled';
|
|
977
992
|
const terminal = this.repairTerminal(stored.session.sessionId, runId, visible, stage, status);
|
|
978
993
|
this.runs.replaceVisibleRun(stored, terminal);
|
|
979
|
-
latestTerminal = terminal;
|
|
994
|
+
if (!terminal.userMessage.cacheWarm) latestTerminal = terminal;
|
|
980
995
|
}
|
|
981
996
|
} else {
|
|
982
997
|
const visibleSuccess = stored.messages.some((message) =>
|
|
983
998
|
message.runId === runId && message.status === 'completed');
|
|
984
999
|
if (visibleSuccess) {
|
|
985
|
-
|
|
1000
|
+
const terminal = this.repairTerminal(
|
|
986
1001
|
stored.session.sessionId,
|
|
987
1002
|
runId,
|
|
988
1003
|
stored.messages.filter((message) => message.runId === runId),
|
|
989
1004
|
undefined,
|
|
990
1005
|
'completed',
|
|
991
1006
|
);
|
|
1007
|
+
if (!terminal.userMessage.cacheWarm) latestTerminal = terminal;
|
|
992
1008
|
}
|
|
993
1009
|
}
|
|
994
1010
|
}
|
|
@@ -1027,12 +1043,18 @@ export class FlexHarnessCore<TScope> {
|
|
|
1027
1043
|
}
|
|
1028
1044
|
await this.reversions.recoverPendingCapture(state, stored, scopeId, scope, outcomes);
|
|
1029
1045
|
}
|
|
1046
|
+
// FlexHarness 13.2.0 marked a warm turn's activity with `cacheWarm`; no activity carries it now.
|
|
1047
|
+
delete (stored.session.activity as { cacheWarm?: true }).cacheWarm;
|
|
1030
1048
|
if (latestTerminal) {
|
|
1031
1049
|
const completedAt = latestTerminal.assistantMessage.completedAt ?? new Date().toISOString();
|
|
1050
|
+
const startedAt = stored.session.activity.runId === latestTerminal.runId
|
|
1051
|
+
? stored.session.activity.startedAt
|
|
1052
|
+
: undefined;
|
|
1032
1053
|
stored.session.status = latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status;
|
|
1033
1054
|
stored.session.activity = {
|
|
1034
1055
|
runId: latestTerminal.runId,
|
|
1035
1056
|
status: latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status,
|
|
1057
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
1036
1058
|
completedAt,
|
|
1037
1059
|
...terminalActivityError(latestTerminal.assistantMessage),
|
|
1038
1060
|
};
|
|
@@ -1076,6 +1098,30 @@ export class FlexHarnessCore<TScope> {
|
|
|
1076
1098
|
return beforeSession !== JSON.stringify(stored.session);
|
|
1077
1099
|
}
|
|
1078
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* The failed or cancelled terminal a run published, read from its projected messages. A run
|
|
1103
|
+
* still running has its prompt or its last assistant message streaming; it has none.
|
|
1104
|
+
*/
|
|
1105
|
+
private publishedTerminal(
|
|
1106
|
+
runId: string,
|
|
1107
|
+
visible: IFlexMessage[],
|
|
1108
|
+
): IFlexTerminalProjection | undefined {
|
|
1109
|
+
if (visible.some((message) => message.status === 'streaming')) return undefined;
|
|
1110
|
+
const userMessage = visible.find((message) => message.role === 'user' && message.steerId === undefined);
|
|
1111
|
+
const assistantMessage = [...visible].reverse().find((message) => message.role === 'assistant');
|
|
1112
|
+
if (!userMessage || !assistantMessage) return undefined;
|
|
1113
|
+
const status = assistantMessage.status;
|
|
1114
|
+
if (status !== 'failed' && status !== 'cancelled') return undefined;
|
|
1115
|
+
return {
|
|
1116
|
+
runId,
|
|
1117
|
+
status,
|
|
1118
|
+
userMessage,
|
|
1119
|
+
assistantMessage,
|
|
1120
|
+
...(assistantMessage.model ? { model: assistantMessage.model } : {}),
|
|
1121
|
+
...(assistantMessage.usage ? { usage: assistantMessage.usage } : {}),
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1079
1125
|
private repairTerminal(
|
|
1080
1126
|
sessionId: string,
|
|
1081
1127
|
runId: string,
|
|
@@ -299,6 +299,7 @@ export class FlexHarnessSessionDeletion<TScope> {
|
|
|
299
299
|
reservation: IReservedSessionDeletion,
|
|
300
300
|
): Promise<void> {
|
|
301
301
|
const { deletedSessions, descendantRoots } = reservation;
|
|
302
|
+
this.core.cacheWarm.onSessionsDeleted(state, deletedSessions.map((deleted) => deleted.sessionId));
|
|
302
303
|
const reason = this.core.trustInternalError(new FlexHarnessAbortError('The session subtree was deleted.'));
|
|
303
304
|
const configurationSettlement = this.core.configurations.abortSessionConfigurations(
|
|
304
305
|
state.storageKey,
|
|
@@ -57,6 +57,7 @@ export class FlexHarnessLifecycle<TScope> {
|
|
|
57
57
|
this.core.commands.assertNoAmbientSlashCommandTeardown();
|
|
58
58
|
if (this.disposePromise) return this.disposePromise;
|
|
59
59
|
this.core.closed = true;
|
|
60
|
+
this.core.cacheWarm.dispose();
|
|
60
61
|
let disposal!: Promise<void>;
|
|
61
62
|
disposal = this.disposeInternal().catch((error) => {
|
|
62
63
|
if (this.disposePromise === disposal) this.disposePromise = undefined;
|
|
@@ -78,6 +79,7 @@ export class FlexHarnessLifecycle<TScope> {
|
|
|
78
79
|
}
|
|
79
80
|
state.fenceInProgress = true;
|
|
80
81
|
state.lifecycle = 'fenced';
|
|
82
|
+
this.core.cacheWarm.onStateRetiring(state);
|
|
81
83
|
const reason = this.core.trustInternalError(new FlexHarnessAbortError('The session namespace was fenced.'));
|
|
82
84
|
if (!state.compactorLifecycleController.signal.aborted) {
|
|
83
85
|
state.compactorLifecycleController.abort(reason);
|
|
@@ -589,6 +591,7 @@ export class FlexHarnessLifecycle<TScope> {
|
|
|
589
591
|
return;
|
|
590
592
|
}
|
|
591
593
|
if (state.lifecycle !== 'fenced') state.lifecycle = 'retiring';
|
|
594
|
+
this.core.cacheWarm.onStateRetiring(state);
|
|
592
595
|
const configurationSettlement = this.core.configurations.abortSessionConfigurations(storageKey, reason);
|
|
593
596
|
if (configurationSettlement) await configurationSettlement;
|
|
594
597
|
for (const staged of [...state.sessionImports.values()]) {
|
|
@@ -259,8 +259,10 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
259
259
|
prompt: INormalizedFlexPrompt,
|
|
260
260
|
byteSize: number,
|
|
261
261
|
): IFlexSteerAdmission {
|
|
262
|
+
// A cache-warm turn runs no tools and caps its output, so it takes in no steer.
|
|
262
263
|
if (
|
|
263
|
-
run.
|
|
264
|
+
run.cacheWarm
|
|
265
|
+
|| run.phase === 'closing'
|
|
264
266
|
|| run.phase === 'finalizing'
|
|
265
267
|
|| run.phase === 'promoting'
|
|
266
268
|
|| run.callbacksClosed
|
|
@@ -323,6 +325,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
323
325
|
delegationSeed?: IDelegatedRunAdmissionSeed,
|
|
324
326
|
admissionSignal?: AbortSignal,
|
|
325
327
|
slashCommandAdmission = false,
|
|
328
|
+
cacheWarm = false,
|
|
326
329
|
): Promise<{
|
|
327
330
|
admission: IFlexPromptQueueAdmission;
|
|
328
331
|
started: Promise<IFlexPromptAdmission>;
|
|
@@ -381,6 +384,9 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
381
384
|
if (stored.pendingReversion) {
|
|
382
385
|
throw new FlexHarnessSessionBusyError(sessionId, 'has a pending reversion operation');
|
|
383
386
|
}
|
|
387
|
+
if (cacheWarm && (state.activeRuns.has(sessionId) || stored.outstandingPromptsById.size > 0)) {
|
|
388
|
+
throw new FlexHarnessSessionBusyError(sessionId, 'is active, so it needs no cache-warm turn');
|
|
389
|
+
}
|
|
384
390
|
if (stored.session.agent !== undefined && delegationSeed === undefined) {
|
|
385
391
|
throw new FlexHarnessValidationError(
|
|
386
392
|
'Subagent sessions can only be prompted through the foreground delegate tool.',
|
|
@@ -434,6 +440,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
434
440
|
status: 'queued',
|
|
435
441
|
queuedAt: new Date().toISOString(),
|
|
436
442
|
...(scheduleKey === undefined ? {} : { scheduleKey, debounceMs }),
|
|
443
|
+
...(cacheWarm ? { cacheWarm: true as const } : {}),
|
|
437
444
|
prompt: normalizedPrompt,
|
|
438
445
|
options: normalizedOptions,
|
|
439
446
|
...(delegationSeed === undefined ? {} : { delegationSeed }),
|
|
@@ -448,6 +455,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
448
455
|
stored.promptQueue.push(queued);
|
|
449
456
|
stored.outstandingPromptsById.set(queued.queueId, queued);
|
|
450
457
|
stored.outstandingPromptBytes += byteSize;
|
|
458
|
+
if (!cacheWarm) this.core.cacheWarm.onActivity(state.storageKey, sessionId);
|
|
451
459
|
this.emitPromptQueueEvent(queued, 'prompt.queued');
|
|
452
460
|
this.schedulePromptQueueDrain(state, stored);
|
|
453
461
|
return {
|
|
@@ -593,7 +601,8 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
593
601
|
}
|
|
594
602
|
run.stored.metrics.pendingGenerationId = run.runId;
|
|
595
603
|
run.stored.messages.push(reservation.userMessage, reservation.assistantMessage);
|
|
596
|
-
|
|
604
|
+
// A cache-warm turn is no turn of the user's: it gets no reversion segment of its own.
|
|
605
|
+
if (run.stored.session.agent === undefined && !run.cacheWarm) {
|
|
597
606
|
this.core.reversions.pruneReversionState(run.stored, true);
|
|
598
607
|
const captureId = this.core.turnReversionProvider
|
|
599
608
|
? this.core.reversions.reversionId('capture', run.state.storageKey, run.sessionId, run.runId)
|
|
@@ -633,6 +642,9 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
633
642
|
if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
634
643
|
throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
|
|
635
644
|
}
|
|
645
|
+
// A cache-warm turn leaves the session's status, activity, updatedAt and preview as the
|
|
646
|
+
// last real turn left them; the active run alone keeps the session busy.
|
|
647
|
+
if (run.cacheWarm) return;
|
|
636
648
|
run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
|
|
637
649
|
run.stored.session.activity = {
|
|
638
650
|
runId: run.runId,
|
|
@@ -683,6 +695,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
683
695
|
runId: run.runId,
|
|
684
696
|
messageId: run.userMessageId,
|
|
685
697
|
session: publicSnapshot(run.stored.session),
|
|
698
|
+
...(run.cacheWarm ? { cacheWarm: true } : {}),
|
|
686
699
|
});
|
|
687
700
|
for (const message of [run.reservedUserMessage!, run.reservedAssistantMessage!]) {
|
|
688
701
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
@@ -697,6 +710,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
697
710
|
}
|
|
698
711
|
|
|
699
712
|
queued.status = run.scheduleKey ? 'scheduled' : 'starting';
|
|
713
|
+
this.core.cacheWarm.onRunStarted(run, queued);
|
|
700
714
|
this.emitPromptQueueEvent(queued, 'prompt.started');
|
|
701
715
|
queued.resolveStarted(Object.freeze({
|
|
702
716
|
queueId: queued.queueId,
|
|
@@ -712,7 +726,10 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
712
726
|
this.core.runs.prepareGeneration(run, options, context.abortSignal),
|
|
713
727
|
pendingInput: run.steering.input,
|
|
714
728
|
activateOnBackgroundJobs: true,
|
|
715
|
-
onCacheDispatch: (dispatch: plugins.ISmartAiCacheDispatch) => {
|
|
729
|
+
onCacheDispatch: (dispatch: plugins.ISmartAiCacheDispatch) => {
|
|
730
|
+
run.cacheDispatch = dispatch;
|
|
731
|
+
this.core.cacheWarm.onModelDispatch(run, dispatch);
|
|
732
|
+
},
|
|
716
733
|
onInputApplied: (input: plugins.IAgentAppliedInput) => this.core.runs.applyRunSteer(run, input),
|
|
717
734
|
beforeFinish: (context: { abortSignal: AbortSignal }) =>
|
|
718
735
|
this.core.subagentTasks.awaitNextBackgroundSubagent(run, context.abortSignal),
|
|
@@ -745,6 +762,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
745
762
|
queuedAt: queued.queuedAt,
|
|
746
763
|
...(queued.runId ? { runId: queued.runId } : {}),
|
|
747
764
|
...(queued.scheduleKey ? { scheduleKey: queued.scheduleKey } : {}),
|
|
765
|
+
...(queued.cacheWarm ? { cacheWarm: true } : {}),
|
|
748
766
|
...(queued.startedAt ? { startedAt: queued.startedAt } : {}),
|
|
749
767
|
};
|
|
750
768
|
}
|
|
@@ -815,6 +833,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
815
833
|
queued.rejectStarted(rejection);
|
|
816
834
|
queued.rejectCompletion(rejection);
|
|
817
835
|
}
|
|
836
|
+
this.core.cacheWarm.onPromptFinished(queued, status, error, result);
|
|
818
837
|
}
|
|
819
838
|
|
|
820
839
|
public cancelQueuedPrompt(
|
|
@@ -961,7 +961,8 @@ export class FlexHarnessReversions<TScope> {
|
|
|
961
961
|
}
|
|
962
962
|
|
|
963
963
|
public async prepareRunReversion(run: IActiveRun, signal: AbortSignal): Promise<void> {
|
|
964
|
-
|
|
964
|
+
// A cache-warm turn changes no workspace, so it captures none.
|
|
965
|
+
if (run.stored.session.agent !== undefined || run.cacheWarm || !this.core.turnReversionProvider) return;
|
|
965
966
|
const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
|
|
966
967
|
const captureId = segment?.captureId;
|
|
967
968
|
if (!segment || !captureId) {
|
|
@@ -1163,6 +1164,11 @@ export class FlexHarnessReversions<TScope> {
|
|
|
1163
1164
|
delete segment.workspaceReference;
|
|
1164
1165
|
}
|
|
1165
1166
|
|
|
1167
|
+
/** Whether an undo hid turns that the next prompt would drop with their redo history. */
|
|
1168
|
+
public hasHiddenReversionSegments(stored: IStoredSessionState): boolean {
|
|
1169
|
+
return this.hiddenReversionSegments(stored).length > 0;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1166
1172
|
private hiddenReversionSegments(stored: IStoredSessionState): IFlexReversionSegment[] {
|
|
1167
1173
|
const firstHidden = this.reversionGroups(stored)[stored.revertCursor]?.segments[0];
|
|
1168
1174
|
if (!firstHidden) return [];
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type TFlexMessagePart,
|
|
28
28
|
} from './interfaces.js';
|
|
29
29
|
import { FlexToolCatalog, assertNoCatalogCollisions } from './classes.toolcatalog.js';
|
|
30
|
+
import { refuseToolExecution } from './classes.flexharness.cachewarm.js';
|
|
30
31
|
import { cloneSerializable } from './utils.json.js';
|
|
31
32
|
import type { INormalizedFlexPrompt } from './utils.prompt.js';
|
|
32
33
|
import {
|
|
@@ -106,6 +107,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
106
107
|
...(queued.scheduleKey === undefined
|
|
107
108
|
? {}
|
|
108
109
|
: { scheduleKey: queued.scheduleKey, debounceMs: queued.debounceMs }),
|
|
110
|
+
...(queued.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
109
111
|
callbackEventCount: 0,
|
|
110
112
|
callbackOutputBytes: 0,
|
|
111
113
|
callbackParts: [],
|
|
@@ -527,6 +529,8 @@ export class FlexHarnessRuns<TScope> {
|
|
|
527
529
|
run.toolCatalog = admittedCatalog;
|
|
528
530
|
Object.assign(admittedTools, this.core.toolDispatch.createCatalogTools(run, admittedCatalog));
|
|
529
531
|
}
|
|
532
|
+
// A cache-warm turn offers the same tools, as they are part of the cached prefix, but runs none.
|
|
533
|
+
if (run.cacheWarm) refuseToolExecution(admittedTools);
|
|
530
534
|
tools = Object.keys(admittedTools).length > 0
|
|
531
535
|
? this.core.toolDispatch.trackToolCalls(run, wrapToolSet(
|
|
532
536
|
admittedTools as TFlexAgentToolSet,
|
|
@@ -569,6 +573,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
569
573
|
...(model!.providerOptions ? { providerOptions: model!.providerOptions } : {}),
|
|
570
574
|
...(model!.cache === undefined ? {} : { cache: model!.cache }),
|
|
571
575
|
...(options.maxSteps ?? model!.maxSteps ? { maxSteps: options.maxSteps ?? model!.maxSteps } : {}),
|
|
576
|
+
...(run.cacheWarm ? { maxOutputTokens: this.core.cacheWarmOptions.maxOutputTokens } : {}),
|
|
572
577
|
...(close ? { close } : {}),
|
|
573
578
|
};
|
|
574
579
|
} catch (error) {
|
|
@@ -593,6 +598,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
593
598
|
names.add('delegate_send');
|
|
594
599
|
}
|
|
595
600
|
if (this.core.builtInTools.renameSession) names.add('rename_session');
|
|
601
|
+
if (this.core.builtInTools.cacheWarm && run.stored.session.agent === undefined) names.add('keep_cache_warm');
|
|
596
602
|
const projectManagement = this.core.builtInTools.projectManagement;
|
|
597
603
|
if (projectManagement?.task) names.add('task');
|
|
598
604
|
if (projectManagement?.goal) names.add('goal');
|
|
@@ -618,6 +624,9 @@ export class FlexHarnessRuns<TScope> {
|
|
|
618
624
|
tools.delegate_send = this.core.subagentTasks.createSubagentSendTool(run);
|
|
619
625
|
}
|
|
620
626
|
if (this.core.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
|
|
627
|
+
if (this.core.builtInTools.cacheWarm && run.stored.session.agent === undefined) {
|
|
628
|
+
tools.keep_cache_warm = this.core.cacheWarm.createKeepCacheWarmTool(run);
|
|
629
|
+
}
|
|
621
630
|
const projectManagement = this.core.builtInTools.projectManagement;
|
|
622
631
|
if (projectManagement?.task) tools.task = this.core.projects.createProjectTaskTool(run);
|
|
623
632
|
if (projectManagement?.goal) tools.goal = this.core.projects.createProjectGoalTool(run);
|
|
@@ -685,6 +694,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
685
694
|
status: 'streaming',
|
|
686
695
|
createdAt: timestamp,
|
|
687
696
|
parts: this.userMessageParts(prompt),
|
|
697
|
+
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
688
698
|
};
|
|
689
699
|
const assistantMessage: IFlexMessage = {
|
|
690
700
|
messageId: run.assistantMessageId,
|
|
@@ -694,6 +704,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
694
704
|
status: 'streaming',
|
|
695
705
|
createdAt: timestamp,
|
|
696
706
|
parts: [],
|
|
707
|
+
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
697
708
|
};
|
|
698
709
|
return { userMessage, assistantMessage };
|
|
699
710
|
}
|
|
@@ -897,6 +908,10 @@ export class FlexHarnessRuns<TScope> {
|
|
|
897
908
|
part.completedAt = timestamp;
|
|
898
909
|
}
|
|
899
910
|
}
|
|
911
|
+
if (run.cacheWarm) {
|
|
912
|
+
userMessage.cacheWarm = true;
|
|
913
|
+
assistantMessage.cacheWarm = true;
|
|
914
|
+
}
|
|
900
915
|
userMessage.status = status;
|
|
901
916
|
userMessage.completedAt = timestamp;
|
|
902
917
|
assistantMessage.status = status;
|
|
@@ -950,7 +965,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
950
965
|
this.replaceVisibleRun(run.stored, terminal);
|
|
951
966
|
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
952
967
|
}, true);
|
|
953
|
-
if (!this.core.isTombstoned(run)) {
|
|
968
|
+
if (!this.core.isTombstoned(run) && !run.cacheWarm) {
|
|
954
969
|
await this.core.mutateScope(run.state, () => {
|
|
955
970
|
if (run.state.sessions.get(run.sessionId) !== run.stored) return;
|
|
956
971
|
const timestamp = terminal.assistantMessage.completedAt!;
|
|
@@ -974,6 +989,8 @@ export class FlexHarnessRuns<TScope> {
|
|
|
974
989
|
this.replaceVisibleRun(run.stored, terminal);
|
|
975
990
|
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
976
991
|
}, true);
|
|
992
|
+
// A failed cache-warm turn reports through `cache.warm.stopped`; the session keeps its state.
|
|
993
|
+
if (run.cacheWarm) return;
|
|
977
994
|
await this.core.mutateScope(run.state, () => {
|
|
978
995
|
if (run.state.sessions.get(run.sessionId) !== run.stored) return;
|
|
979
996
|
const timestamp = terminal.assistantMessage.completedAt!;
|
|
@@ -1099,6 +1116,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1099
1116
|
...(error === undefined ? {} : { error: errorToInfo(error) }),
|
|
1100
1117
|
keptInContext: steering.keptInContext,
|
|
1101
1118
|
unappliedSteerIds: [...steering.unappliedSteerIds],
|
|
1119
|
+
...(run.cacheWarm ? { cacheWarm: true } : {}),
|
|
1102
1120
|
});
|
|
1103
1121
|
}
|
|
1104
1122
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
IFlexBackgroundExecution,
|
|
3
3
|
IFlexBeginSessionImportOptions,
|
|
4
|
+
IFlexCacheWarmInput,
|
|
5
|
+
IFlexCacheWarmState,
|
|
4
6
|
IFlexCompactSessionOptions,
|
|
5
7
|
IFlexCreateProjectTaskInput,
|
|
6
8
|
IFlexCreateSessionOptions,
|
|
@@ -54,6 +56,7 @@ export {
|
|
|
54
56
|
createFlexResourceToolNamespace,
|
|
55
57
|
createFlexResourceToolName,
|
|
56
58
|
} from './utils.flexharness.js';
|
|
59
|
+
export { FLEX_CACHE_WARM_PROMPT } from './classes.flexharness.cachewarm.js';
|
|
57
60
|
|
|
58
61
|
export class FlexHarness<TScope = unknown> {
|
|
59
62
|
private readonly core: FlexHarnessCore<TScope>;
|
|
@@ -384,6 +387,24 @@ export class FlexHarness<TScope = unknown> {
|
|
|
384
387
|
return this.core.prompts.steerPrompt(scopeId, sessionId, queueId, steer);
|
|
385
388
|
}
|
|
386
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Keeps the prompt cache of a root session warm until `durationMs` from now, capped at
|
|
392
|
+
* `cacheWarm.maxDurationMs`; a further call replaces the deadline and `0` stops it. While the
|
|
393
|
+
* session is idle, a marked cache-warm prompt reads the cached prefix again shortly before it
|
|
394
|
+
* would expire. The `keep_cache_warm` built-in tool gives the agent the same control.
|
|
395
|
+
*/
|
|
396
|
+
public keepCacheWarm(
|
|
397
|
+
scopeId: string,
|
|
398
|
+
sessionId: string,
|
|
399
|
+
input: IFlexCacheWarmInput,
|
|
400
|
+
): Promise<IFlexCacheWarmState> {
|
|
401
|
+
return this.core.cacheWarm.keepCacheWarm(scopeId, sessionId, input);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
public getCacheWarm(scopeId: string, sessionId: string): Promise<IFlexCacheWarmState> {
|
|
405
|
+
return this.core.cacheWarm.getCacheWarm(scopeId, sessionId);
|
|
406
|
+
}
|
|
407
|
+
|
|
387
408
|
/** Sends follow-up input only to an exact running direct child owned by this parent run. */
|
|
388
409
|
public sendSubagentInput(
|
|
389
410
|
scopeId: string,
|
|
@@ -102,6 +102,18 @@ export interface IStoredSessionState {
|
|
|
102
102
|
terminalPromptQueueEntries: Map<string, IFlexPromptQueueEntry>;
|
|
103
103
|
outstandingPromptBytes: number;
|
|
104
104
|
promptQueueDrain?: Promise<void>;
|
|
105
|
+
/** The last model call of the session in this process, on the cache keeper's clock. */
|
|
106
|
+
lastCacheDispatch?: ICacheDispatchMark;
|
|
107
|
+
/** What the last root prompt asked of the model, which its cache-warm turns repeat. */
|
|
108
|
+
cacheWarmPromptOptions?: Pick<IFlexPromptOptions, 'modelHint' | 'system'>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ICacheDispatchMark {
|
|
112
|
+
at: number;
|
|
113
|
+
/** How long after `at` a cache-warm turn is due: the cache's TTL less a minute. */
|
|
114
|
+
idleMs: number;
|
|
115
|
+
/** What the TTL rests on; absent for the five-minute default. */
|
|
116
|
+
basis?: plugins.TSmartAiCacheBudgetBasis;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
export interface IRetainedSessionCleanup {
|
|
@@ -220,6 +232,8 @@ export interface IActiveRun {
|
|
|
220
232
|
transaction?: plugins.IAgentGenerationHandle;
|
|
221
233
|
scheduleKey?: string;
|
|
222
234
|
debounceMs?: number;
|
|
235
|
+
/** A cache-warm turn of the session's cache keeper; see `keepCacheWarm()`. */
|
|
236
|
+
cacheWarm?: true;
|
|
223
237
|
ownerCancellation?: FlexHarnessAbortError;
|
|
224
238
|
internalFailure?: unknown;
|
|
225
239
|
callbackError?: unknown;
|
|
@@ -389,6 +403,8 @@ export interface IQueuedPrompt {
|
|
|
389
403
|
runId?: string;
|
|
390
404
|
scheduleKey?: string;
|
|
391
405
|
debounceMs?: number;
|
|
406
|
+
/** A cache-warm prompt of the session's cache keeper; see `keepCacheWarm()`. */
|
|
407
|
+
cacheWarm?: true;
|
|
392
408
|
startedAt?: string;
|
|
393
409
|
prompt?: INormalizedFlexPrompt;
|
|
394
410
|
options?: IFlexPromptOptions;
|
package/ts/interfaces.ts
CHANGED
|
@@ -222,6 +222,11 @@ export interface IFlexMessage {
|
|
|
222
222
|
rejectedPermissionId?: string;
|
|
223
223
|
/** On a user message that steered its running run: the caller's steer identity. */
|
|
224
224
|
steerId?: string;
|
|
225
|
+
/**
|
|
226
|
+
* On both messages of a cache-warm turn: the harness sent the prompt only to keep the model's
|
|
227
|
+
* prompt cache warm. Hosts leave these out of what they show and report as work.
|
|
228
|
+
*/
|
|
229
|
+
cacheWarm?: true;
|
|
225
230
|
}
|
|
226
231
|
|
|
227
232
|
export interface IFlexSessionActivity {
|
|
@@ -523,6 +528,8 @@ export interface IFlexPromptQueueEntry {
|
|
|
523
528
|
queuedAt: string;
|
|
524
529
|
runId?: string;
|
|
525
530
|
scheduleKey?: string;
|
|
531
|
+
/** The entry is a cache-warm prompt; see {@link IFlexMessage.cacheWarm}. */
|
|
532
|
+
cacheWarm?: true;
|
|
526
533
|
startedAt?: string;
|
|
527
534
|
finishedAt?: string;
|
|
528
535
|
error?: IFlexErrorInfo;
|
|
@@ -1897,8 +1904,50 @@ export interface IFlexProjectManagementBuiltInToolsOptions {
|
|
|
1897
1904
|
export interface IFlexBuiltInToolsOptions {
|
|
1898
1905
|
renameSession?: boolean;
|
|
1899
1906
|
projectManagement?: IFlexProjectManagementBuiltInToolsOptions;
|
|
1907
|
+
/** The `keep_cache_warm` tool of root sessions; see `FlexHarness.keepCacheWarm()`. */
|
|
1908
|
+
cacheWarm?: boolean;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/** The clock the cache keeper reads and sets its timers on. Tests pass a fake one. */
|
|
1912
|
+
export interface IFlexCacheWarmClock {
|
|
1913
|
+
now(): number;
|
|
1914
|
+
setTimeout(callback: () => void, delayMs: number): unknown;
|
|
1915
|
+
clearTimeout(handle: unknown): void;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
export interface IFlexCacheWarmOptions {
|
|
1919
|
+
/** The longest warm period one call sets, in milliseconds. Defaults to 4 hours. */
|
|
1920
|
+
maxDurationMs?: number;
|
|
1921
|
+
/** Caps the output tokens of each model call of a cache-warm turn. Defaults to 1024. */
|
|
1922
|
+
maxOutputTokens?: number;
|
|
1923
|
+
clock?: IFlexCacheWarmClock;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/** What `keepCacheWarm()` sets. */
|
|
1927
|
+
export interface IFlexCacheWarmInput {
|
|
1928
|
+
/** How long from now to keep the cache warm, in milliseconds; 0 stops. Capped at the maximum. */
|
|
1929
|
+
durationMs: number;
|
|
1900
1930
|
}
|
|
1901
1931
|
|
|
1932
|
+
/** A session's cache keeper. Process-local: it ends with the harness. */
|
|
1933
|
+
export interface IFlexCacheWarmState {
|
|
1934
|
+
sessionId: string;
|
|
1935
|
+
/** Present while the keeper runs: when it stops, as an ISO timestamp. */
|
|
1936
|
+
warmUntil?: string;
|
|
1937
|
+
/** Present while a warm turn is due: when, as an ISO timestamp. */
|
|
1938
|
+
nextWarmAt?: string;
|
|
1939
|
+
/** The idle time after a model call at which a warm turn is due, in milliseconds. */
|
|
1940
|
+
idleMs?: number;
|
|
1941
|
+
/**
|
|
1942
|
+
* What `idleMs` rests on, from the last model call: the observed cache lifetime of its provider,
|
|
1943
|
+
* model and endpoint (see `TSmartAiCacheBudgetBasis` of `@modelprofile.com/flexharness-models`).
|
|
1944
|
+
* Absent while no call was observed, when the five-minute default applies.
|
|
1945
|
+
*/
|
|
1946
|
+
basis?: TFlexCacheWarmBasis;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
export type TFlexCacheWarmBasis = plugins.TSmartAiCacheBudgetBasis;
|
|
1950
|
+
|
|
1902
1951
|
/** What an attachment resolver is asked for: the attachment `url` names, for one session. */
|
|
1903
1952
|
export interface IFlexAttachmentRequest<TScope = unknown> extends IFlexAgentContextInvocation<TScope> {
|
|
1904
1953
|
readonly url: URL;
|
|
@@ -1955,6 +2004,8 @@ export interface IFlexHarnessOptions<TScope> {
|
|
|
1955
2004
|
/** Maximum child depth (root is 0). Defaults to 6; configurable from 1 through 8. */
|
|
1956
2005
|
maxSubagentDepth?: number;
|
|
1957
2006
|
maxSubagentCallsPerRun?: number;
|
|
2007
|
+
/** Bounds and the clock of the cache keeper; see `keepCacheWarm()`. */
|
|
2008
|
+
cacheWarm?: IFlexCacheWarmOptions;
|
|
1958
2009
|
}
|
|
1959
2010
|
|
|
1960
2011
|
export interface IFlexUncertainToolExecution {
|
|
@@ -2021,6 +2072,8 @@ export interface IFlexRunStartedEvent extends IFlexEventBase {
|
|
|
2021
2072
|
readonly runId: string;
|
|
2022
2073
|
readonly messageId: string;
|
|
2023
2074
|
readonly session: IFlexSession;
|
|
2075
|
+
/** The run is a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
2076
|
+
readonly cacheWarm?: true;
|
|
2024
2077
|
}
|
|
2025
2078
|
|
|
2026
2079
|
export interface IFlexMessageChangedEvent extends IFlexEventBase {
|
|
@@ -2099,6 +2152,8 @@ export interface IFlexRunFinishedEvent extends IFlexEventBase {
|
|
|
2099
2152
|
* steer it accepted.
|
|
2100
2153
|
*/
|
|
2101
2154
|
readonly unappliedSteerIds: readonly string[];
|
|
2155
|
+
/** The run is a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
2156
|
+
readonly cacheWarm?: true;
|
|
2102
2157
|
}
|
|
2103
2158
|
|
|
2104
2159
|
/** The running run accepted a steer; it is taken in at the run's next step boundary. */
|
|
@@ -2138,6 +2193,8 @@ export type TFlexModelUsageEvent = IFlexEventBase & {
|
|
|
2138
2193
|
readonly type: 'model.usage';
|
|
2139
2194
|
readonly runId?: string;
|
|
2140
2195
|
readonly source: 'generation' | 'compaction';
|
|
2196
|
+
/** The call belongs to a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
2197
|
+
readonly cacheWarm?: true;
|
|
2141
2198
|
} & Readonly<TFlexObservedModelCallUsage>;
|
|
2142
2199
|
|
|
2143
2200
|
export interface IFlexPromptQueueEvent extends IFlexEventBase {
|
|
@@ -2153,6 +2210,43 @@ export interface IFlexSessionHistoryChangedEvent extends IFlexEventBase {
|
|
|
2153
2210
|
readonly runId?: string;
|
|
2154
2211
|
}
|
|
2155
2212
|
|
|
2213
|
+
/**
|
|
2214
|
+
* The session's cache keeper changed: `warmUntil` is when it stops, absent once it stopped.
|
|
2215
|
+
* `source` is who set it: the host through `keepCacheWarm()` or the agent through its tool.
|
|
2216
|
+
*/
|
|
2217
|
+
export interface IFlexCacheWarmChangedEvent extends IFlexEventBase {
|
|
2218
|
+
readonly type: 'cache.warm.changed';
|
|
2219
|
+
readonly source: 'host' | 'agent';
|
|
2220
|
+
readonly warmUntil?: string;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/** A cache-warm turn completed; `usage` shows whether it read the cached prefix. */
|
|
2224
|
+
export interface IFlexCacheWarmCompletedEvent extends IFlexEventBase {
|
|
2225
|
+
readonly type: 'cache.warm.completed';
|
|
2226
|
+
readonly runId: string;
|
|
2227
|
+
/**
|
|
2228
|
+
* The turn's usage. `cacheReadTokens` and `cacheWriteTokens` are the provider's own counts:
|
|
2229
|
+
* OpenAI's `input_tokens_details.cached_tokens` and `cache_write_tokens`, Anthropic's
|
|
2230
|
+
* `cache_read_input_tokens` and `cache_creation_input_tokens`.
|
|
2231
|
+
*/
|
|
2232
|
+
readonly usage: IFlexUsage;
|
|
2233
|
+
/** How long the session had been idle since its previous model call when the turn was due. */
|
|
2234
|
+
readonly idleForMs?: number;
|
|
2235
|
+
/** What the keeper's timing rested on; see {@link IFlexCacheWarmState.basis}. */
|
|
2236
|
+
readonly basis?: TFlexCacheWarmBasis;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* The session's cache keeper stopped: its deadline passed, the host or agent cleared it, a warm
|
|
2241
|
+
* turn failed, or the session can no longer be warmed.
|
|
2242
|
+
*/
|
|
2243
|
+
export interface IFlexCacheWarmStoppedEvent extends IFlexEventBase {
|
|
2244
|
+
readonly type: 'cache.warm.stopped';
|
|
2245
|
+
readonly reason: 'deadline' | 'cleared' | 'failed' | 'unavailable';
|
|
2246
|
+
readonly runId?: string;
|
|
2247
|
+
readonly error?: IFlexErrorInfo;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2156
2250
|
export interface IFlexErrorInfo {
|
|
2157
2251
|
name: string;
|
|
2158
2252
|
message: string;
|
|
@@ -2178,6 +2272,9 @@ export type TFlexHarnessEvent =
|
|
|
2178
2272
|
| IFlexPromptQueueEvent
|
|
2179
2273
|
| IFlexSteerAcceptedEvent
|
|
2180
2274
|
| IFlexSteerAppliedEvent
|
|
2181
|
-
| IFlexSessionHistoryChangedEvent
|
|
2275
|
+
| IFlexSessionHistoryChangedEvent
|
|
2276
|
+
| IFlexCacheWarmChangedEvent
|
|
2277
|
+
| IFlexCacheWarmCompletedEvent
|
|
2278
|
+
| IFlexCacheWarmStoppedEvent;
|
|
2182
2279
|
|
|
2183
2280
|
export type TFlexHarnessEventListener = (event: TFlexHarnessEvent) => void;
|