@modelprofile.com/flexharness 13.1.1 → 13.2.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 +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 +4 -1
- package/dist_ts/classes.flexharness.core.js +9 -3
- 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 +28 -9
- 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 +25 -3
- package/dist_ts/interfaces.d.ts +89 -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 +9 -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/dist_ts_chat_harness/functions.mapping.d.ts +1 -0
- package/dist_ts_chat_harness/functions.mapping.js +5 -2
- 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 +8 -1
- package/ts/classes.flexharness.deletion.ts +1 -0
- package/ts/classes.flexharness.lifecycle.ts +3 -0
- package/ts/classes.flexharness.prompts.ts +25 -6
- package/ts/classes.flexharness.reversions.ts +7 -1
- package/ts/classes.flexharness.runs.ts +20 -2
- package/ts/classes.flexharness.ts +21 -0
- package/ts/interfaces.flexharness.ts +16 -0
- package/ts/interfaces.ts +100 -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 +8 -1
- package/ts/utils.sessionpreview.ts +4 -4
- package/ts_chat_harness/classes.transcript.ts +14 -3
- package/ts_chat_harness/functions.mapping.ts +3 -1
- package/ts_chat_harness/readme.md +1 -0
|
@@ -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;
|
|
@@ -960,8 +975,9 @@ export class FlexHarnessRuns<TScope> {
|
|
|
960
975
|
status: 'idle',
|
|
961
976
|
startedAt: run.stored.session.activity.startedAt,
|
|
962
977
|
completedAt: timestamp,
|
|
978
|
+
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
963
979
|
};
|
|
964
|
-
run.stored.session.updatedAt = timestamp;
|
|
980
|
+
if (!run.cacheWarm) run.stored.session.updatedAt = timestamp;
|
|
965
981
|
}, true);
|
|
966
982
|
}
|
|
967
983
|
}
|
|
@@ -985,8 +1001,9 @@ export class FlexHarnessRuns<TScope> {
|
|
|
985
1001
|
startedAt: run.stored.session.activity.startedAt,
|
|
986
1002
|
completedAt: timestamp,
|
|
987
1003
|
...terminalActivityError(terminal.assistantMessage),
|
|
1004
|
+
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
988
1005
|
};
|
|
989
|
-
run.stored.session.updatedAt = timestamp;
|
|
1006
|
+
if (!run.cacheWarm) run.stored.session.updatedAt = timestamp;
|
|
990
1007
|
}, true);
|
|
991
1008
|
}
|
|
992
1009
|
|
|
@@ -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 {
|
|
@@ -234,6 +239,8 @@ export interface IFlexSessionActivity {
|
|
|
234
239
|
limit?: TFlexModelLimit;
|
|
235
240
|
/** The permission whose rejection stopped the last run, next to its error. */
|
|
236
241
|
rejectedPermissionId?: string;
|
|
242
|
+
/** The run is or was a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
243
|
+
cacheWarm?: true;
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
export interface IFlexSession {
|
|
@@ -523,6 +530,8 @@ export interface IFlexPromptQueueEntry {
|
|
|
523
530
|
queuedAt: string;
|
|
524
531
|
runId?: string;
|
|
525
532
|
scheduleKey?: string;
|
|
533
|
+
/** The entry is a cache-warm prompt; see {@link IFlexMessage.cacheWarm}. */
|
|
534
|
+
cacheWarm?: true;
|
|
526
535
|
startedAt?: string;
|
|
527
536
|
finishedAt?: string;
|
|
528
537
|
error?: IFlexErrorInfo;
|
|
@@ -1897,8 +1906,50 @@ export interface IFlexProjectManagementBuiltInToolsOptions {
|
|
|
1897
1906
|
export interface IFlexBuiltInToolsOptions {
|
|
1898
1907
|
renameSession?: boolean;
|
|
1899
1908
|
projectManagement?: IFlexProjectManagementBuiltInToolsOptions;
|
|
1909
|
+
/** The `keep_cache_warm` tool of root sessions; see `FlexHarness.keepCacheWarm()`. */
|
|
1910
|
+
cacheWarm?: boolean;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/** The clock the cache keeper reads and sets its timers on. Tests pass a fake one. */
|
|
1914
|
+
export interface IFlexCacheWarmClock {
|
|
1915
|
+
now(): number;
|
|
1916
|
+
setTimeout(callback: () => void, delayMs: number): unknown;
|
|
1917
|
+
clearTimeout(handle: unknown): void;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
export interface IFlexCacheWarmOptions {
|
|
1921
|
+
/** The longest warm period one call sets, in milliseconds. Defaults to 4 hours. */
|
|
1922
|
+
maxDurationMs?: number;
|
|
1923
|
+
/** Caps the output tokens of each model call of a cache-warm turn. Defaults to 1024. */
|
|
1924
|
+
maxOutputTokens?: number;
|
|
1925
|
+
clock?: IFlexCacheWarmClock;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/** What `keepCacheWarm()` sets. */
|
|
1929
|
+
export interface IFlexCacheWarmInput {
|
|
1930
|
+
/** How long from now to keep the cache warm, in milliseconds; 0 stops. Capped at the maximum. */
|
|
1931
|
+
durationMs: number;
|
|
1900
1932
|
}
|
|
1901
1933
|
|
|
1934
|
+
/** A session's cache keeper. Process-local: it ends with the harness. */
|
|
1935
|
+
export interface IFlexCacheWarmState {
|
|
1936
|
+
sessionId: string;
|
|
1937
|
+
/** Present while the keeper runs: when it stops, as an ISO timestamp. */
|
|
1938
|
+
warmUntil?: string;
|
|
1939
|
+
/** Present while a warm turn is due: when, as an ISO timestamp. */
|
|
1940
|
+
nextWarmAt?: string;
|
|
1941
|
+
/** The idle time after a model call at which a warm turn is due, in milliseconds. */
|
|
1942
|
+
idleMs?: number;
|
|
1943
|
+
/**
|
|
1944
|
+
* What `idleMs` rests on, from the last model call: the observed cache lifetime of its provider,
|
|
1945
|
+
* model and endpoint (see `TSmartAiCacheBudgetBasis` of `@modelprofile.com/flexharness-models`).
|
|
1946
|
+
* Absent while no call was observed, when the five-minute default applies.
|
|
1947
|
+
*/
|
|
1948
|
+
basis?: TFlexCacheWarmBasis;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
export type TFlexCacheWarmBasis = plugins.TSmartAiCacheBudgetBasis;
|
|
1952
|
+
|
|
1902
1953
|
/** What an attachment resolver is asked for: the attachment `url` names, for one session. */
|
|
1903
1954
|
export interface IFlexAttachmentRequest<TScope = unknown> extends IFlexAgentContextInvocation<TScope> {
|
|
1904
1955
|
readonly url: URL;
|
|
@@ -1955,6 +2006,8 @@ export interface IFlexHarnessOptions<TScope> {
|
|
|
1955
2006
|
/** Maximum child depth (root is 0). Defaults to 6; configurable from 1 through 8. */
|
|
1956
2007
|
maxSubagentDepth?: number;
|
|
1957
2008
|
maxSubagentCallsPerRun?: number;
|
|
2009
|
+
/** Bounds and the clock of the cache keeper; see `keepCacheWarm()`. */
|
|
2010
|
+
cacheWarm?: IFlexCacheWarmOptions;
|
|
1958
2011
|
}
|
|
1959
2012
|
|
|
1960
2013
|
export interface IFlexUncertainToolExecution {
|
|
@@ -2021,6 +2074,8 @@ export interface IFlexRunStartedEvent extends IFlexEventBase {
|
|
|
2021
2074
|
readonly runId: string;
|
|
2022
2075
|
readonly messageId: string;
|
|
2023
2076
|
readonly session: IFlexSession;
|
|
2077
|
+
/** The run is a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
2078
|
+
readonly cacheWarm?: true;
|
|
2024
2079
|
}
|
|
2025
2080
|
|
|
2026
2081
|
export interface IFlexMessageChangedEvent extends IFlexEventBase {
|
|
@@ -2099,6 +2154,8 @@ export interface IFlexRunFinishedEvent extends IFlexEventBase {
|
|
|
2099
2154
|
* steer it accepted.
|
|
2100
2155
|
*/
|
|
2101
2156
|
readonly unappliedSteerIds: readonly string[];
|
|
2157
|
+
/** The run is a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
2158
|
+
readonly cacheWarm?: true;
|
|
2102
2159
|
}
|
|
2103
2160
|
|
|
2104
2161
|
/** The running run accepted a steer; it is taken in at the run's next step boundary. */
|
|
@@ -2138,6 +2195,8 @@ export type TFlexModelUsageEvent = IFlexEventBase & {
|
|
|
2138
2195
|
readonly type: 'model.usage';
|
|
2139
2196
|
readonly runId?: string;
|
|
2140
2197
|
readonly source: 'generation' | 'compaction';
|
|
2198
|
+
/** The call belongs to a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
2199
|
+
readonly cacheWarm?: true;
|
|
2141
2200
|
} & Readonly<TFlexObservedModelCallUsage>;
|
|
2142
2201
|
|
|
2143
2202
|
export interface IFlexPromptQueueEvent extends IFlexEventBase {
|
|
@@ -2153,6 +2212,43 @@ export interface IFlexSessionHistoryChangedEvent extends IFlexEventBase {
|
|
|
2153
2212
|
readonly runId?: string;
|
|
2154
2213
|
}
|
|
2155
2214
|
|
|
2215
|
+
/**
|
|
2216
|
+
* The session's cache keeper changed: `warmUntil` is when it stops, absent once it stopped.
|
|
2217
|
+
* `source` is who set it: the host through `keepCacheWarm()` or the agent through its tool.
|
|
2218
|
+
*/
|
|
2219
|
+
export interface IFlexCacheWarmChangedEvent extends IFlexEventBase {
|
|
2220
|
+
readonly type: 'cache.warm.changed';
|
|
2221
|
+
readonly source: 'host' | 'agent';
|
|
2222
|
+
readonly warmUntil?: string;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/** A cache-warm turn completed; `usage` shows whether it read the cached prefix. */
|
|
2226
|
+
export interface IFlexCacheWarmCompletedEvent extends IFlexEventBase {
|
|
2227
|
+
readonly type: 'cache.warm.completed';
|
|
2228
|
+
readonly runId: string;
|
|
2229
|
+
/**
|
|
2230
|
+
* The turn's usage. `cacheReadTokens` and `cacheWriteTokens` are the provider's own counts:
|
|
2231
|
+
* OpenAI's `input_tokens_details.cached_tokens` and `cache_write_tokens`, Anthropic's
|
|
2232
|
+
* `cache_read_input_tokens` and `cache_creation_input_tokens`.
|
|
2233
|
+
*/
|
|
2234
|
+
readonly usage: IFlexUsage;
|
|
2235
|
+
/** How long the session had been idle since its previous model call when the turn was due. */
|
|
2236
|
+
readonly idleForMs?: number;
|
|
2237
|
+
/** What the keeper's timing rested on; see {@link IFlexCacheWarmState.basis}. */
|
|
2238
|
+
readonly basis?: TFlexCacheWarmBasis;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* The session's cache keeper stopped: its deadline passed, the host or agent cleared it, a warm
|
|
2243
|
+
* turn failed, or the session can no longer be warmed.
|
|
2244
|
+
*/
|
|
2245
|
+
export interface IFlexCacheWarmStoppedEvent extends IFlexEventBase {
|
|
2246
|
+
readonly type: 'cache.warm.stopped';
|
|
2247
|
+
readonly reason: 'deadline' | 'cleared' | 'failed' | 'unavailable';
|
|
2248
|
+
readonly runId?: string;
|
|
2249
|
+
readonly error?: IFlexErrorInfo;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2156
2252
|
export interface IFlexErrorInfo {
|
|
2157
2253
|
name: string;
|
|
2158
2254
|
message: string;
|
|
@@ -2178,6 +2274,9 @@ export type TFlexHarnessEvent =
|
|
|
2178
2274
|
| IFlexPromptQueueEvent
|
|
2179
2275
|
| IFlexSteerAcceptedEvent
|
|
2180
2276
|
| IFlexSteerAppliedEvent
|
|
2181
|
-
| IFlexSessionHistoryChangedEvent
|
|
2277
|
+
| IFlexSessionHistoryChangedEvent
|
|
2278
|
+
| IFlexCacheWarmChangedEvent
|
|
2279
|
+
| IFlexCacheWarmCompletedEvent
|
|
2280
|
+
| IFlexCacheWarmStoppedEvent;
|
|
2182
2281
|
|
|
2183
2282
|
export type TFlexHarnessEventListener = (event: TFlexHarnessEvent) => void;
|
package/ts/plugins.ts
CHANGED
package/ts/readme.md
CHANGED
|
@@ -418,6 +418,7 @@ The tools use strict action-discriminated inputs:
|
|
|
418
418
|
- `goal`: `get`, `set`, or `clear`.
|
|
419
419
|
- `scratchpad`: `get`, `set`, `append`, or `clear`. Append concatenates the supplied content exactly.
|
|
420
420
|
- `rename_session`: sets the active session title and returns the authoritative session.
|
|
421
|
+
- `keep_cache_warm` (`cacheWarm: true`, root sessions only): keeps the session's prompt cache warm; see [Keeping the Prompt Cache Warm](#keeping-the-prompt-cache-warm).
|
|
421
422
|
|
|
422
423
|
Every project action returns the authoritative revision and state; task mutations also return the affected task, and clear returns the removed tasks. Reads never save. A set, clear, append, update, idempotent create, or empty task clear that makes no state change returns the current revision without writing. Mutations load once, apply once, validate the complete next snapshot, and issue one compare-and-swap save at `revision + 1`. FlexHarness never retries or merges an external conflict.
|
|
423
424
|
|
|
@@ -729,6 +730,32 @@ The run takes a steer in at its next step boundary: after the tool results of it
|
|
|
729
730
|
|
|
730
731
|
`steerPrompt()` resolves with `{ steerId, queueId, runId }` once the run accepted the steer and `prompt.steer.accepted` was emitted; `prompt.steer.applied` carries the `messageId` of the user message the steer became. Every accepted steer ends in exactly one of two ways: it is in the model context of later turns, or it is listed in `unappliedSteerIds`, in arrival order, on `IFlexPromptResult` and `run.finished`, and on the finished queue entry when that list is not empty. A steer is listed when the run did not take it in before it ended, because its last model step was already running or because it was stopped or failed, and also when the run itself does not stay in context. `keptInContext` on `run.finished` and on the finished queue entry says whether it does: it is `false` for a failed run and for a run stopped before any model output, and such a run lists every steer it accepted, including the ones it took in and reported with `prompt.steer.applied`. Send its prompt and its unapplied steers again to have them answered. The run stops taking steers atomically when its generation ends or `abort()`/`cancelPrompt()` stops it. A steer for a prompt without a running run, or whose run is ending, is refused with `FlexHarnessSteerRejectedError` reason `not-running`; the caller sends it as the next turn instead. Reusing a `steerId` within a run is refused with reason `duplicate`, an unknown `queueId` throws `FlexHarnessNotFoundError`, and child sessions reject this general API (their exact parent run uses `sendSubagentInput()` or `delegate_send` instead). A run holds at most `maxOutstandingPromptsPerSession` pending steers, and their bytes count against the session's outstanding prompt byte limit until the run takes them in or ends, so `startPrompt()` and `enqueuePrompt()` are refused with `FlexHarnessQueueFullError` while pending steers fill it. They are process-local like queue entries: a process restart ends the run and drops its pending steers, while steers it took in are durable parts of the turn.
|
|
731
732
|
|
|
733
|
+
### Keeping the Prompt Cache Warm
|
|
734
|
+
|
|
735
|
+
A provider's prompt cache expires when nothing reads it for its TTL, and the next turn then writes the whole prefix again. `keepCacheWarm()` keeps a root session's cache warm until a deadline:
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
await harness.keepCacheWarm(scopeId, sessionId, { durationMs: 60 * 60 * 1000 }); // warm until 1 h from now
|
|
739
|
+
await harness.keepCacheWarm(scopeId, sessionId, { durationMs: 0 }); // stop
|
|
740
|
+
const state = await harness.getCacheWarm(scopeId, sessionId); // { sessionId, warmUntil?, nextWarmAt?, idleMs?, basis? }
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
Each call replaces the deadline with `durationMs` from now, capped at `cacheWarm.maxDurationMs` (default 4 h, at most 24 h); `0` stops it. While the deadline has not passed and the session is idle, the harness sends a cache-warm prompt once the session has had no model call for the cache TTL less one minute: 4 minutes for a 5-minute cache, 29 for a 30-minute one, 59 for a 1-hour one. The TTL comes from the request the provider actually sent last, its `basis` in `getCacheWarm()`:
|
|
744
|
+
|
|
745
|
+
| `basis` | TTL | Sent request |
|
|
746
|
+
| --- | --- | --- |
|
|
747
|
+
| `anthropic-5m`, `anthropic-1h` | 5 min, 1 h | Anthropic: the shortest `cache_control` lifetime of the request |
|
|
748
|
+
| `openai-30m` | 30 min | OpenAI's API, GPT-5.6 and later: `prompt_cache_options.ttl` is 30 minutes after the last write or reuse |
|
|
749
|
+
| `openai-24h` | 30 min | OpenAI's API, an earlier model with `prompt_cache_retention: '24h'` (`cache: { retention: '24h' }`): OpenAI keeps the prefix typically around 30 minutes and up to 24 hours, without promising more |
|
|
750
|
+
| `openai-in-memory` | 5 min | OpenAI's API, an earlier model with `in_memory` or no retention; or the ChatGPT backend of ChatGPT accounts, which takes no retention and documents none. In-memory entries last 5 to 10 minutes of inactivity |
|
|
751
|
+
| `configured` | `cache.warmDurationMs` | any |
|
|
752
|
+
|
|
753
|
+
With no basis (another provider or endpoint, or a custom transport that may change the request) the TTL is 5 minutes. No OpenAI retention promises a hit beyond about 30 minutes, so none makes warming unnecessary; extended retention only spaces warm turns further apart. Source: OpenAI's [prompt caching guide](https://developers.openai.com/api/docs/guides/prompt-caching), read 2026-10-11. A prompt, a steer or a running turn resets the wait, and no warm prompt is sent while a turn runs, a prompt is queued, an undo's redo history is open, a reversion is pending or a slash command runs. The warm prompt is the fixed text `FLEX_CACHE_WARM_PROMPT`, starting with `[cache-warm]`, which asks for the single word `ok`. Its turn uses the model and system of the session's last turn and offers the same tools, so it reads the cached prefix, but it refuses every tool call, takes at most two steps, caps each model call at `cacheWarm.maxOutputTokens` (default 1024) and takes no steer: `steerPrompt()` refuses one with reason `not-running`. Keeping a session warm costs a cache read of its prefix and a few new tokens per warm turn; the warm exchanges stay in the model context.
|
|
754
|
+
|
|
755
|
+
Warm turns are marked so hosts can leave them out of what they show and of what they count as work: both messages, the session `activity`, the prompt queue entry, `run.started`, `run.finished` and `model.usage` carry `cacheWarm: true`. A warm turn changes neither the session's `updatedAt` nor its `preview`, and adds no undo step. Three events report the keeper: `cache.warm.changed` (`source` `host` or `agent`, `warmUntil` absent once stopped), `cache.warm.completed` with the warm turn's `usage`, whose `cacheReadTokens` and `cacheWriteTokens` are the provider's own counts and show whether it hit the cache (OpenAI's `input_tokens_details.cached_tokens` and `cache_write_tokens`, Anthropic's `cache_read_input_tokens` and `cache_creation_input_tokens`), its `idleForMs` since the session's previous model call and its `basis`, and `cache.warm.stopped` with `reason` `deadline`, `cleared`, `failed` (the warm turn failed; `error` set) or `unavailable` (the session or its scope went away).
|
|
756
|
+
|
|
757
|
+
`builtInTools: { cacheWarm: true }` gives root sessions the `keep_cache_warm` tool, so the agent can keep its own cache warm: `{ duration: '1h' }` sets the deadline (amounts of `s`, `m` or `h`), `'off'` or `'0'` stops it. The keeper is process-local: it ends with `dispose()` and does not survive a restart. It warms only the loaded scope it was set in: retiring the scope or deleting the session stops it with `unavailable`, and it never loads a scope again. Tests pass `cacheWarm.clock` (`now`, `setTimeout`, `clearTimeout`) to drive it with a fake clock.
|
|
758
|
+
|
|
732
759
|
## Slash Commands
|
|
733
760
|
|
|
734
761
|
`parseSlashCommand()` is the public strict pure parser. It accepts at most 768 KiB, returns `not-command` for input not starting with `/`, `malformed` for invalid slash syntax, and `parsed` with the exact input, lowercase command name, separator-stripped raw argument text, and OpenCode-compatible quoted tokenization. Command names match `[a-z][a-z0-9_-]{0,63}`. The public `isValidSlashCommandName(name)` and `isReservedSlashCommandName(name)` helpers let applications validate registration names against the same rules. Single and double quotes group tokens and are stripped; escapes are not interpreted.
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import type {
|
|
13
13
|
IFlexAgentSessionPolicy,
|
|
14
14
|
IFlexAttachmentResolver,
|
|
15
|
+
IFlexCacheWarmClock,
|
|
16
|
+
IFlexCacheWarmOptions,
|
|
15
17
|
IFlexCallbackLimits,
|
|
16
18
|
IFlexHarnessOptions,
|
|
17
19
|
IFlexHarnessStores,
|
|
@@ -46,8 +48,21 @@ export interface INormalizedProjectManagementTools {
|
|
|
46
48
|
export interface INormalizedBuiltInTools {
|
|
47
49
|
readonly renameSession: boolean;
|
|
48
50
|
readonly projectManagement?: INormalizedProjectManagementTools;
|
|
51
|
+
readonly cacheWarm: boolean;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
export interface INormalizedCacheWarmOptions {
|
|
55
|
+
readonly maxDurationMs: number;
|
|
56
|
+
readonly maxOutputTokens: number;
|
|
57
|
+
readonly clock: IFlexCacheWarmClock;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const defaultCacheWarmMaxDurationMs = 4 * 60 * 60 * 1000;
|
|
61
|
+
// One call warms for at most a day; a host extends it by calling again.
|
|
62
|
+
const maximumCacheWarmMaxDurationMs = 24 * 60 * 60 * 1000;
|
|
63
|
+
const defaultCacheWarmMaxOutputTokens = 1024;
|
|
64
|
+
const maximumCacheWarmMaxOutputTokens = 32_768;
|
|
65
|
+
|
|
51
66
|
const DEFAULT_CALLBACK_LIMITS: Required<IFlexCallbackLimits> = {
|
|
52
67
|
maxEvents: 10_000,
|
|
53
68
|
maxOutputBytes: 1024 * 1024,
|
|
@@ -242,7 +257,7 @@ function normalizeCodeModeLimits(codeMode: unknown): Readonly<IFlexCodeModeLimit
|
|
|
242
257
|
export function normalizeBuiltInTools(
|
|
243
258
|
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
244
259
|
): INormalizedBuiltInTools {
|
|
245
|
-
if (options === undefined) return Object.freeze({ renameSession: false });
|
|
260
|
+
if (options === undefined) return Object.freeze({ renameSession: false, cacheWarm: false });
|
|
246
261
|
if (
|
|
247
262
|
!options
|
|
248
263
|
|| typeof options !== 'object'
|
|
@@ -252,14 +267,19 @@ export function normalizeBuiltInTools(
|
|
|
252
267
|
throw new FlexHarnessValidationError('builtInTools must be a plain object.');
|
|
253
268
|
}
|
|
254
269
|
const unsupported = Object.keys(options)
|
|
255
|
-
.find((key) => !['renameSession', 'projectManagement'].includes(key));
|
|
270
|
+
.find((key) => !['renameSession', 'projectManagement', 'cacheWarm'].includes(key));
|
|
256
271
|
if (unsupported) throw new FlexHarnessValidationError(`builtInTools does not support "${unsupported}".`);
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
for (const key of ['renameSession', 'cacheWarm'] as const) {
|
|
273
|
+
if (options[key] !== undefined && typeof options[key] !== 'boolean') {
|
|
274
|
+
throw new FlexHarnessValidationError(`builtInTools.${key} must be a boolean.`);
|
|
275
|
+
}
|
|
259
276
|
}
|
|
260
277
|
const projectManagement = options.projectManagement;
|
|
261
278
|
if (projectManagement === undefined) {
|
|
262
|
-
return Object.freeze({
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
renameSession: options.renameSession ?? false,
|
|
281
|
+
cacheWarm: options.cacheWarm ?? false,
|
|
282
|
+
});
|
|
263
283
|
}
|
|
264
284
|
if (
|
|
265
285
|
!projectManagement
|
|
@@ -283,6 +303,7 @@ export function normalizeBuiltInTools(
|
|
|
283
303
|
}
|
|
284
304
|
return Object.freeze({
|
|
285
305
|
renameSession: options.renameSession ?? false,
|
|
306
|
+
cacheWarm: options.cacheWarm ?? false,
|
|
286
307
|
projectManagement: Object.freeze({
|
|
287
308
|
task: projectManagement.task ?? true,
|
|
288
309
|
goal: projectManagement.goal ?? true,
|
|
@@ -374,6 +395,54 @@ export function normalizeSlashCommands<TScope>(
|
|
|
374
395
|
return Object.freeze(normalized);
|
|
375
396
|
}
|
|
376
397
|
|
|
398
|
+
const systemCacheWarmClock: IFlexCacheWarmClock = Object.freeze({
|
|
399
|
+
now: () => Date.now(),
|
|
400
|
+
setTimeout: (callback: () => void, delayMs: number) => {
|
|
401
|
+
const handle = setTimeout(callback, delayMs);
|
|
402
|
+
// A warm period never holds the process open.
|
|
403
|
+
handle.unref?.();
|
|
404
|
+
return handle;
|
|
405
|
+
},
|
|
406
|
+
clearTimeout: (handle: unknown) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
export function normalizeCacheWarmOptions(
|
|
410
|
+
input: IFlexHarnessOptions<unknown>['cacheWarm'],
|
|
411
|
+
): INormalizedCacheWarmOptions {
|
|
412
|
+
if (input !== undefined && !isPlainOptions(input as unknown)) {
|
|
413
|
+
throw new FlexHarnessValidationError('cacheWarm must be a plain object.');
|
|
414
|
+
}
|
|
415
|
+
const options: IFlexCacheWarmOptions = input ?? {};
|
|
416
|
+
const unsupported = Object.keys(options)
|
|
417
|
+
.find((key) => !['maxDurationMs', 'maxOutputTokens', 'clock'].includes(key));
|
|
418
|
+
if (unsupported) throw new FlexHarnessValidationError(`cacheWarm does not support "${unsupported}".`);
|
|
419
|
+
const clock: IFlexCacheWarmClock = options.clock ?? systemCacheWarmClock;
|
|
420
|
+
if (
|
|
421
|
+
!clock
|
|
422
|
+
|| typeof clock !== 'object'
|
|
423
|
+
|| typeof clock.now !== 'function'
|
|
424
|
+
|| typeof clock.setTimeout !== 'function'
|
|
425
|
+
|| typeof clock.clearTimeout !== 'function'
|
|
426
|
+
) {
|
|
427
|
+
throw new FlexHarnessValidationError('cacheWarm.clock must provide now, setTimeout and clearTimeout.');
|
|
428
|
+
}
|
|
429
|
+
return Object.freeze({
|
|
430
|
+
maxDurationMs: resolveBoundedPositiveInteger(
|
|
431
|
+
options.maxDurationMs,
|
|
432
|
+
'cacheWarm.maxDurationMs',
|
|
433
|
+
defaultCacheWarmMaxDurationMs,
|
|
434
|
+
maximumCacheWarmMaxDurationMs,
|
|
435
|
+
),
|
|
436
|
+
maxOutputTokens: resolveBoundedPositiveInteger(
|
|
437
|
+
options.maxOutputTokens,
|
|
438
|
+
'cacheWarm.maxOutputTokens',
|
|
439
|
+
defaultCacheWarmMaxOutputTokens,
|
|
440
|
+
maximumCacheWarmMaxOutputTokens,
|
|
441
|
+
),
|
|
442
|
+
clock,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
377
446
|
export function resolveBoundedPositiveInteger(
|
|
378
447
|
value: number | undefined,
|
|
379
448
|
name: string,
|
package/ts/utils.json.ts
CHANGED
|
@@ -566,7 +566,7 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
566
566
|
const activity = requireRecord(session.activity, `${path}.activity`);
|
|
567
567
|
requireOnlyKeys(
|
|
568
568
|
activity,
|
|
569
|
-
['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit', 'rejectedPermissionId'],
|
|
569
|
+
['runId', 'status', 'startedAt', 'completedAt', 'error', 'limit', 'rejectedPermissionId', 'cacheWarm'],
|
|
570
570
|
`${path}.activity`,
|
|
571
571
|
);
|
|
572
572
|
if (!activityStatuses.includes(activity.status as (typeof activityStatuses)[number])) {
|
|
@@ -578,6 +578,9 @@ function validateSession(value: unknown, path: string): IValidatedSessionIdentit
|
|
|
578
578
|
requireOptionalString(activity.error, `${path}.activity.error`);
|
|
579
579
|
requireOptionalModelLimit(activity.limit, `${path}.activity.limit`);
|
|
580
580
|
requireOptionalString(activity.rejectedPermissionId, `${path}.activity.rejectedPermissionId`);
|
|
581
|
+
if (activity.cacheWarm !== undefined && activity.cacheWarm !== true) {
|
|
582
|
+
throw new FlexHarnessStoreFormatError(`${path}.activity.cacheWarm must be true when present.`);
|
|
583
|
+
}
|
|
581
584
|
const relationship = [
|
|
582
585
|
session.parentSessionId,
|
|
583
586
|
session.parentRunId,
|
|
@@ -776,6 +779,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
776
779
|
'limit',
|
|
777
780
|
'rejectedPermissionId',
|
|
778
781
|
'steerId',
|
|
782
|
+
'cacheWarm',
|
|
779
783
|
],
|
|
780
784
|
path,
|
|
781
785
|
);
|
|
@@ -799,6 +803,9 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
|
|
|
799
803
|
throw new FlexHarnessStoreFormatError(`${path}.steerId is only valid on a user message.`);
|
|
800
804
|
}
|
|
801
805
|
}
|
|
806
|
+
if (message.cacheWarm !== undefined && message.cacheWarm !== true) {
|
|
807
|
+
throw new FlexHarnessStoreFormatError(`${path}.cacheWarm must be true when present.`);
|
|
808
|
+
}
|
|
802
809
|
if (message.model !== undefined) validateModel(message.model, `${path}.model`);
|
|
803
810
|
if (message.usage !== undefined) validateUsage(message.usage, `${path}.usage`);
|
|
804
811
|
if (!Array.isArray(message.parts)) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { FLEX_PROJECT_MANAGEMENT_LIMITS, type IFlexMessage } from './interfaces.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* What a session is about before anyone titles it: the text of its first prompt that has any,
|
|
5
|
-
* one line (every run of whitespace becomes one space), cut to the
|
|
6
|
-
* boundary. Undefined while no prompt carries text.
|
|
4
|
+
* What a session is about before anyone titles it: the text of its first prompt that has any,
|
|
5
|
+
* cache-warm prompts aside, as one line (every run of whitespace becomes one space), cut to the
|
|
6
|
+
* title bound at a character boundary. Undefined while no prompt carries text.
|
|
7
7
|
*/
|
|
8
8
|
export function sessionPreview(messagesArg: readonly IFlexMessage[]): string | undefined {
|
|
9
9
|
for (const message of messagesArg) {
|
|
10
|
-
if (message.role !== 'user') continue;
|
|
10
|
+
if (message.role !== 'user' || message.cacheWarm) continue;
|
|
11
11
|
const text = message.parts
|
|
12
12
|
.map((part) => (part.type === 'text' ? part.text : ''))
|
|
13
13
|
.join(' ')
|
|
@@ -180,7 +180,7 @@ export class FlexHarnessTranscript {
|
|
|
180
180
|
public view(): IFlexHarnessTranscriptView {
|
|
181
181
|
return structuredClone({
|
|
182
182
|
messages: this.flexMessages.flatMap((message, index) =>
|
|
183
|
-
flexMessageToHarnessRows(message, index, this.timings)),
|
|
183
|
+
message.cacheWarm ? [] : flexMessageToHarnessRows(message, index, this.timings)),
|
|
184
184
|
permissions: [...this.permissionRows.values()],
|
|
185
185
|
questions: [],
|
|
186
186
|
status: this.status,
|
|
@@ -217,6 +217,11 @@ export class FlexHarnessTranscript {
|
|
|
217
217
|
}
|
|
218
218
|
if (event.scopeId !== this.scopeId || event.sessionId !== this.sessionId) return change;
|
|
219
219
|
const timestamp = Date.parse(event.timestamp);
|
|
220
|
+
// A cache-warm turn's messages are held but not shown, so their events change no row.
|
|
221
|
+
const warmMessage = 'message' in event
|
|
222
|
+
? event.message.cacheWarm === true
|
|
223
|
+
: 'messageId' in event && this.flexMessages.some((message) =>
|
|
224
|
+
message.messageId === event.messageId && message.cacheWarm);
|
|
220
225
|
switch (event.type) {
|
|
221
226
|
case 'session.created':
|
|
222
227
|
case 'session.updated':
|
|
@@ -227,7 +232,7 @@ export class FlexHarnessTranscript {
|
|
|
227
232
|
change.messages = change.permissions = change.status = change.usage = true;
|
|
228
233
|
break;
|
|
229
234
|
case 'run.started':
|
|
230
|
-
this.setStatus(change, { type: 'busy' });
|
|
235
|
+
if (!event.cacheWarm) this.setStatus(change, { type: 'busy' });
|
|
231
236
|
break;
|
|
232
237
|
case 'message.created':
|
|
233
238
|
case 'message.updated':
|
|
@@ -280,13 +285,15 @@ export class FlexHarnessTranscript {
|
|
|
280
285
|
this.usage = sumUsage(this.usage, flexUsageToHarness(event.message.usage));
|
|
281
286
|
change.usage = true;
|
|
282
287
|
}
|
|
288
|
+
if (event.cacheWarm) break;
|
|
283
289
|
this.setStatus(change, event.status === 'failed'
|
|
284
290
|
? { type: 'error', ...(event.error ? { message: event.error.message } : {}) }
|
|
285
291
|
: { type: 'idle' });
|
|
286
292
|
break;
|
|
287
293
|
case 'prompt.started':
|
|
288
294
|
case 'prompt.running':
|
|
289
|
-
|
|
295
|
+
// A steer cannot join a cache-warm turn.
|
|
296
|
+
if (!event.entry.cacheWarm) this.runningQueueId = event.queueId;
|
|
290
297
|
break;
|
|
291
298
|
case 'prompt.finished':
|
|
292
299
|
if (this.runningQueueId === event.queueId) this.runningQueueId = undefined;
|
|
@@ -297,6 +304,10 @@ export class FlexHarnessTranscript {
|
|
|
297
304
|
default:
|
|
298
305
|
break;
|
|
299
306
|
}
|
|
307
|
+
if (warmMessage) {
|
|
308
|
+
change.messages = false;
|
|
309
|
+
change.deltas = [];
|
|
310
|
+
}
|
|
300
311
|
return change;
|
|
301
312
|
}
|
|
302
313
|
|
|
@@ -215,7 +215,9 @@ export function flexPermissionToHarness(
|
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/** A cache-warm turn is idle time to the user: its activity shows as idle. */
|
|
218
219
|
export function flexSessionToHarnessStatus(session: IFlexSession): plugins.IHarnessStatus {
|
|
220
|
+
if (session.activity.cacheWarm) return { type: 'idle' };
|
|
219
221
|
switch (session.activity.status) {
|
|
220
222
|
case 'running':
|
|
221
223
|
case 'scheduled':
|
|
@@ -247,7 +249,7 @@ export function flexSessionToHarnessMeta(session: IFlexSession): plugins.IHarnes
|
|
|
247
249
|
title: session.title ?? session.sessionId,
|
|
248
250
|
...(epoch(session.createdAt) === undefined ? {} : { createdAt: epoch(session.createdAt) }),
|
|
249
251
|
...(epoch(session.updatedAt) === undefined ? {} : { updatedAt: epoch(session.updatedAt) }),
|
|
250
|
-
state: sessionCardStates[session.activity.status],
|
|
252
|
+
state: session.activity.cacheWarm ? 'normal' : sessionCardStates[session.activity.status],
|
|
251
253
|
};
|
|
252
254
|
}
|
|
253
255
|
|