@modelprofile.com/flexharness 13.2.0 → 13.3.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.core.d.ts +5 -0
- package/dist_ts/classes.flexharness.core.js +51 -8
- package/dist_ts/classes.flexharness.d.ts +4 -4
- package/dist_ts/classes.flexharness.js +1 -1
- package/dist_ts/classes.flexharness.prompts.d.ts +8 -6
- package/dist_ts/classes.flexharness.prompts.js +64 -16
- package/dist_ts/classes.flexharness.runs.d.ts +6 -1
- package/dist_ts/classes.flexharness.runs.js +24 -10
- package/dist_ts/interfaces.d.ts +12 -2
- package/dist_ts/interfaces.flexharness.d.ts +12 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/utils.json.js +3 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +0 -1
- package/dist_ts_chat_harness/functions.mapping.js +2 -5
- package/package.json +3 -3
- package/readme.md +3 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.core.ts +45 -6
- package/ts/classes.flexharness.prompts.ts +73 -16
- package/ts/classes.flexharness.runs.ts +29 -7
- package/ts/classes.flexharness.ts +4 -4
- package/ts/interfaces.flexharness.ts +13 -1
- package/ts/interfaces.ts +13 -2
- package/ts/readme.md +3 -1
- package/ts/utils.json.ts +2 -0
- package/ts_chat_harness/functions.mapping.ts +1 -3
- package/ts_chat_harness/readme.md +1 -1
|
@@ -969,32 +969,42 @@ export class FlexHarnessCore<TScope> {
|
|
|
969
969
|
...stored.messages.map((message) => message.runId),
|
|
970
970
|
...stored.stagedTerminals.map((terminal) => terminal.runId),
|
|
971
971
|
]);
|
|
972
|
+
// The session's state is the latest real turn's: a cache-warm turn's terminal never sets it.
|
|
972
973
|
let latestTerminal: IFlexTerminalProjection | undefined;
|
|
973
974
|
for (const runId of runIds) {
|
|
974
975
|
const outcome = outcomes.get(runId);
|
|
975
976
|
const stage = stages.get(runId);
|
|
976
977
|
if (outcome === 'accepted' && stage?.status === 'completed') {
|
|
977
978
|
this.runs.replaceVisibleRun(stored, stage);
|
|
978
|
-
latestTerminal = stage;
|
|
979
|
+
if (!stage.userMessage.cacheWarm) latestTerminal = stage;
|
|
979
980
|
} else if (outcome !== 'accepted') {
|
|
980
981
|
const visible = stored.messages.filter((message) => message.runId === runId);
|
|
981
|
-
|
|
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) {
|
|
982
991
|
const status = outcome === 'rejected' ? 'failed' : 'cancelled';
|
|
983
992
|
const terminal = this.repairTerminal(stored.session.sessionId, runId, visible, stage, status);
|
|
984
993
|
this.runs.replaceVisibleRun(stored, terminal);
|
|
985
|
-
latestTerminal = terminal;
|
|
994
|
+
if (!terminal.userMessage.cacheWarm) latestTerminal = terminal;
|
|
986
995
|
}
|
|
987
996
|
} else {
|
|
988
997
|
const visibleSuccess = stored.messages.some((message) =>
|
|
989
998
|
message.runId === runId && message.status === 'completed');
|
|
990
999
|
if (visibleSuccess) {
|
|
991
|
-
|
|
1000
|
+
const terminal = this.repairTerminal(
|
|
992
1001
|
stored.session.sessionId,
|
|
993
1002
|
runId,
|
|
994
1003
|
stored.messages.filter((message) => message.runId === runId),
|
|
995
1004
|
undefined,
|
|
996
1005
|
'completed',
|
|
997
1006
|
);
|
|
1007
|
+
if (!terminal.userMessage.cacheWarm) latestTerminal = terminal;
|
|
998
1008
|
}
|
|
999
1009
|
}
|
|
1000
1010
|
}
|
|
@@ -1033,17 +1043,22 @@ export class FlexHarnessCore<TScope> {
|
|
|
1033
1043
|
}
|
|
1034
1044
|
await this.reversions.recoverPendingCapture(state, stored, scopeId, scope, outcomes);
|
|
1035
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;
|
|
1036
1048
|
if (latestTerminal) {
|
|
1037
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;
|
|
1038
1053
|
stored.session.status = latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status;
|
|
1039
1054
|
stored.session.activity = {
|
|
1040
1055
|
runId: latestTerminal.runId,
|
|
1041
1056
|
status: latestTerminal.status === 'completed' ? 'idle' : latestTerminal.status,
|
|
1057
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
1042
1058
|
completedAt,
|
|
1043
1059
|
...terminalActivityError(latestTerminal.assistantMessage),
|
|
1044
|
-
...(latestTerminal.userMessage.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
1045
1060
|
};
|
|
1046
|
-
|
|
1061
|
+
stored.session.updatedAt = completedAt;
|
|
1047
1062
|
} else if (['scheduled', 'running', 'waiting_permission'].includes(stored.session.status)) {
|
|
1048
1063
|
const timestamp = new Date().toISOString();
|
|
1049
1064
|
stored.session.status = 'cancelled';
|
|
@@ -1083,6 +1098,30 @@ export class FlexHarnessCore<TScope> {
|
|
|
1083
1098
|
return beforeSession !== JSON.stringify(stored.session);
|
|
1084
1099
|
}
|
|
1085
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
|
+
|
|
1086
1125
|
private repairTerminal(
|
|
1087
1126
|
sessionId: string,
|
|
1088
1127
|
runId: string,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './errors.js';
|
|
13
13
|
import type {
|
|
14
14
|
IFlexAgentContextInvocation,
|
|
15
|
+
IFlexEnqueuePromptOptions,
|
|
15
16
|
IFlexSteerAdmission,
|
|
16
17
|
IFlexSteerInput,
|
|
17
18
|
IFlexPromptAdmission,
|
|
@@ -35,6 +36,7 @@ import type {
|
|
|
35
36
|
IDelegatedRunAdmissionSeed,
|
|
36
37
|
IQueuedPrompt,
|
|
37
38
|
IPendingPromptAdmission,
|
|
39
|
+
IAdmittedSteer,
|
|
38
40
|
} from './interfaces.flexharness.js';
|
|
39
41
|
import { combineErrors, isAbortError } from './utils.flexharness.js';
|
|
40
42
|
import { sessionPreview } from './utils.sessionpreview.js';
|
|
@@ -91,7 +93,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
91
93
|
scopeId: string,
|
|
92
94
|
sessionId: string,
|
|
93
95
|
prompt: TFlexPrompt,
|
|
94
|
-
options:
|
|
96
|
+
options: IFlexEnqueuePromptOptions = {},
|
|
95
97
|
): Promise<IFlexPromptResult> {
|
|
96
98
|
const admission = await this.startPrompt(scopeId, sessionId, prompt, options);
|
|
97
99
|
return admission.completion;
|
|
@@ -101,10 +103,13 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
101
103
|
scopeId: string,
|
|
102
104
|
sessionId: string,
|
|
103
105
|
prompt: TFlexPrompt,
|
|
104
|
-
options:
|
|
106
|
+
options: IFlexEnqueuePromptOptions = {},
|
|
105
107
|
): Promise<IFlexPromptAdmission> {
|
|
106
|
-
|
|
107
|
-
const queued = await this.enqueuePromptInternal(
|
|
108
|
+
const admitted = this.splitEnqueueOptions(options);
|
|
109
|
+
const queued = await this.enqueuePromptInternal(
|
|
110
|
+
scopeId, sessionId, prompt, admitted.options,
|
|
111
|
+
undefined, undefined, undefined, undefined, false, false, admitted.steers,
|
|
112
|
+
);
|
|
108
113
|
return queued.started;
|
|
109
114
|
}
|
|
110
115
|
|
|
@@ -112,13 +117,47 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
112
117
|
scopeId: string,
|
|
113
118
|
sessionId: string,
|
|
114
119
|
prompt: TFlexPrompt,
|
|
115
|
-
options:
|
|
120
|
+
options: IFlexEnqueuePromptOptions = {},
|
|
116
121
|
): Promise<IFlexPromptQueueAdmission> {
|
|
117
|
-
|
|
118
|
-
const queued = await this.enqueuePromptInternal(
|
|
122
|
+
const admitted = this.splitEnqueueOptions(options);
|
|
123
|
+
const queued = await this.enqueuePromptInternal(
|
|
124
|
+
scopeId, sessionId, prompt, admitted.options,
|
|
125
|
+
undefined, undefined, undefined, undefined, false, false, admitted.steers,
|
|
126
|
+
);
|
|
119
127
|
return queued.admission;
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
/** Validates the options of a prompt and the steers that join its run, in their given order. */
|
|
131
|
+
private splitEnqueueOptions(
|
|
132
|
+
options: IFlexEnqueuePromptOptions,
|
|
133
|
+
): { options: IFlexPromptOptions; steers: IAdmittedSteer[] } {
|
|
134
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
135
|
+
throw new FlexHarnessValidationError('Prompt options must be a plain object.');
|
|
136
|
+
}
|
|
137
|
+
const { steers, ...promptOptions } = options;
|
|
138
|
+
validatePromptOptions(promptOptions, false);
|
|
139
|
+
if (steers === undefined) return { options: promptOptions, steers: [] };
|
|
140
|
+
if (!Array.isArray(steers)) throw new FlexHarnessValidationError('steers must be an array.');
|
|
141
|
+
const steerIds = new Set<string>();
|
|
142
|
+
const admitted = steers.map((steer): IAdmittedSteer => {
|
|
143
|
+
if (!steer || typeof steer !== 'object' || Array.isArray(steer)) {
|
|
144
|
+
throw new FlexHarnessValidationError('Each steer must be a plain object.');
|
|
145
|
+
}
|
|
146
|
+
validateIdentifier(steer.steerId, 'steerId');
|
|
147
|
+
requireTransferIdentifier(steer.steerId, 'steerId');
|
|
148
|
+
if (steerIds.has(steer.steerId)) {
|
|
149
|
+
throw new FlexHarnessValidationError(`Steer "${steer.steerId}" is given more than once.`);
|
|
150
|
+
}
|
|
151
|
+
steerIds.add(steer.steerId);
|
|
152
|
+
return {
|
|
153
|
+
steerId: steer.steerId,
|
|
154
|
+
prompt: normalizeFlexPrompt(steer.prompt, this.core.attachmentReferenceProtocols),
|
|
155
|
+
byteSize: this.promptAdmissionByteSize(steer.prompt, {}),
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
return { options: promptOptions, steers: admitted };
|
|
159
|
+
}
|
|
160
|
+
|
|
122
161
|
public async schedulePrompt(
|
|
123
162
|
scopeId: string,
|
|
124
163
|
sessionId: string,
|
|
@@ -326,6 +365,7 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
326
365
|
admissionSignal?: AbortSignal,
|
|
327
366
|
slashCommandAdmission = false,
|
|
328
367
|
cacheWarm = false,
|
|
368
|
+
steers: readonly IAdmittedSteer[] = [],
|
|
329
369
|
): Promise<{
|
|
330
370
|
admission: IFlexPromptQueueAdmission;
|
|
331
371
|
started: Promise<IFlexPromptAdmission>;
|
|
@@ -335,7 +375,8 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
335
375
|
const normalizedPrompt = normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols);
|
|
336
376
|
const normalizedOptions = cloneSerializable(options);
|
|
337
377
|
const byteSize = this.promptAdmissionByteSize(prompt, options, scheduleKey, debounceMs);
|
|
338
|
-
const
|
|
378
|
+
const steerBytes = steers.reduce((sum, steer) => sum + steer.byteSize, 0);
|
|
379
|
+
const releasePendingAdmission = this.reservePendingPromptAdmission(byteSize + steerBytes);
|
|
339
380
|
let resolveSettled!: () => void;
|
|
340
381
|
const pendingOwner: IPendingPromptAdmission = {
|
|
341
382
|
controller: new AbortController(),
|
|
@@ -400,8 +441,11 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
400
441
|
`Session "${sessionId}" has reached its outstanding prompt limit.`,
|
|
401
442
|
);
|
|
402
443
|
}
|
|
444
|
+
if (steers.length > this.core.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
445
|
+
throw new FlexHarnessQueueFullError(`Session "${sessionId}" has reached its pending steer limit.`);
|
|
446
|
+
}
|
|
403
447
|
if (
|
|
404
|
-
stored.outstandingPromptBytes + byteSize
|
|
448
|
+
stored.outstandingPromptBytes + byteSize + steerBytes
|
|
405
449
|
> this.core.promptQueueLimits.maxOutstandingBytesPerSession
|
|
406
450
|
) {
|
|
407
451
|
throw new FlexHarnessQueueFullError(
|
|
@@ -452,9 +496,14 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
452
496
|
resolveStarted,
|
|
453
497
|
rejectStarted,
|
|
454
498
|
};
|
|
499
|
+
if (steers.length > 0) {
|
|
500
|
+
// The steers are the run's from admission on: a prompt that never runs reports them unapplied.
|
|
501
|
+
queued.steering = this.core.runs.createRunSteering(stored, steers);
|
|
502
|
+
queued.admittedSteerIds = steers.map((steer) => steer.steerId);
|
|
503
|
+
}
|
|
455
504
|
stored.promptQueue.push(queued);
|
|
456
505
|
stored.outstandingPromptsById.set(queued.queueId, queued);
|
|
457
|
-
stored.outstandingPromptBytes += byteSize;
|
|
506
|
+
stored.outstandingPromptBytes += byteSize + steerBytes;
|
|
458
507
|
if (!cacheWarm) this.core.cacheWarm.onActivity(state.storageKey, sessionId);
|
|
459
508
|
this.emitPromptQueueEvent(queued, 'prompt.queued');
|
|
460
509
|
this.schedulePromptQueueDrain(state, stored);
|
|
@@ -642,18 +691,18 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
642
691
|
if (run.state.tombstones.has(run.sessionId) || run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
643
692
|
throw new FlexHarnessAbortError('The session was deleted during prompt admission.');
|
|
644
693
|
}
|
|
694
|
+
// A cache-warm turn leaves the session's status, activity, updatedAt and preview as the
|
|
695
|
+
// last real turn left them; the active run alone keeps the session busy.
|
|
696
|
+
if (run.cacheWarm) return;
|
|
645
697
|
run.stored.session.status = run.scheduleKey ? 'scheduled' : 'running';
|
|
646
698
|
run.stored.session.activity = {
|
|
647
699
|
runId: run.runId,
|
|
648
700
|
status: run.scheduleKey ? 'scheduled' : 'running',
|
|
649
701
|
startedAt: reservation.userMessage.createdAt,
|
|
650
|
-
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
651
702
|
};
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (preview !== undefined) run.stored.session.preview = preview;
|
|
656
|
-
}
|
|
703
|
+
run.stored.session.updatedAt = reservation.userMessage.createdAt;
|
|
704
|
+
const preview = run.stored.session.preview ?? sessionPreview(run.stored.messages);
|
|
705
|
+
if (preview !== undefined) run.stored.session.preview = preview;
|
|
657
706
|
});
|
|
658
707
|
} catch (error) {
|
|
659
708
|
if (containsStoreCommitUncertainty(error)) run.state.lifecycle = 'fenced';
|
|
@@ -712,6 +761,14 @@ export class FlexHarnessPromptQueue<TScope> {
|
|
|
712
761
|
queued.status = run.scheduleKey ? 'scheduled' : 'starting';
|
|
713
762
|
this.core.cacheWarm.onRunStarted(run, queued);
|
|
714
763
|
this.emitPromptQueueEvent(queued, 'prompt.started');
|
|
764
|
+
for (const steerId of queued.admittedSteerIds ?? []) {
|
|
765
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
766
|
+
type: 'prompt.steer.accepted',
|
|
767
|
+
queueId: run.queueId,
|
|
768
|
+
runId: run.runId,
|
|
769
|
+
steerId,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
715
772
|
queued.resolveStarted(Object.freeze({
|
|
716
773
|
queueId: queued.queueId,
|
|
717
774
|
runId: run.runId,
|
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
IStoredSessionState,
|
|
47
47
|
IStorageState,
|
|
48
48
|
IActiveRun,
|
|
49
|
+
IAdmittedSteer,
|
|
49
50
|
IRunSteering,
|
|
50
51
|
IRunSteeringOutcome,
|
|
51
52
|
IQueuedPrompt,
|
|
@@ -76,13 +77,34 @@ export class FlexHarnessRuns<TScope> {
|
|
|
76
77
|
return true;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The steering of a run, holding `steers` as accepted and waiting for its first step boundary.
|
|
82
|
+
* The caller already counted their bytes against the session.
|
|
83
|
+
*/
|
|
84
|
+
public createRunSteering(
|
|
85
|
+
stored: IStoredSessionState,
|
|
86
|
+
steers: readonly IAdmittedSteer[] = [],
|
|
87
|
+
): IRunSteering {
|
|
80
88
|
const steering: IRunSteering = {
|
|
81
89
|
input: new plugins.AgentPendingInputQueue(),
|
|
82
|
-
stored
|
|
90
|
+
stored,
|
|
83
91
|
accepted: new Map(),
|
|
84
92
|
applied: new Set(),
|
|
85
93
|
};
|
|
94
|
+
for (const steer of steers) {
|
|
95
|
+
steering.input.add({
|
|
96
|
+
inputId: steer.steerId,
|
|
97
|
+
content: cloneSerializable(steer.prompt.modelMessage.content) as Parameters<
|
|
98
|
+
plugins.IAgentSession['beginGeneration']
|
|
99
|
+
>[0],
|
|
100
|
+
});
|
|
101
|
+
steering.accepted.set(steer.steerId, { prompt: steer.prompt, byteSize: steer.byteSize });
|
|
102
|
+
}
|
|
103
|
+
return steering;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public createActiveRun(queued: IQueuedPrompt): IActiveRun {
|
|
107
|
+
const steering = queued.steering ?? this.createRunSteering(queued.stored);
|
|
86
108
|
queued.steering = steering;
|
|
87
109
|
return {
|
|
88
110
|
steering,
|
|
@@ -965,7 +987,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
965
987
|
this.replaceVisibleRun(run.stored, terminal);
|
|
966
988
|
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
967
989
|
}, true);
|
|
968
|
-
if (!this.core.isTombstoned(run)) {
|
|
990
|
+
if (!this.core.isTombstoned(run) && !run.cacheWarm) {
|
|
969
991
|
await this.core.mutateScope(run.state, () => {
|
|
970
992
|
if (run.state.sessions.get(run.sessionId) !== run.stored) return;
|
|
971
993
|
const timestamp = terminal.assistantMessage.completedAt!;
|
|
@@ -975,9 +997,8 @@ export class FlexHarnessRuns<TScope> {
|
|
|
975
997
|
status: 'idle',
|
|
976
998
|
startedAt: run.stored.session.activity.startedAt,
|
|
977
999
|
completedAt: timestamp,
|
|
978
|
-
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
979
1000
|
};
|
|
980
|
-
|
|
1001
|
+
run.stored.session.updatedAt = timestamp;
|
|
981
1002
|
}, true);
|
|
982
1003
|
}
|
|
983
1004
|
}
|
|
@@ -990,6 +1011,8 @@ export class FlexHarnessRuns<TScope> {
|
|
|
990
1011
|
this.replaceVisibleRun(run.stored, terminal);
|
|
991
1012
|
run.stored.stagedTerminals = run.stored.stagedTerminals.filter((entry) => entry.runId !== run.runId);
|
|
992
1013
|
}, true);
|
|
1014
|
+
// A failed cache-warm turn reports through `cache.warm.stopped`; the session keeps its state.
|
|
1015
|
+
if (run.cacheWarm) return;
|
|
993
1016
|
await this.core.mutateScope(run.state, () => {
|
|
994
1017
|
if (run.state.sessions.get(run.sessionId) !== run.stored) return;
|
|
995
1018
|
const timestamp = terminal.assistantMessage.completedAt!;
|
|
@@ -1001,9 +1024,8 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1001
1024
|
startedAt: run.stored.session.activity.startedAt,
|
|
1002
1025
|
completedAt: timestamp,
|
|
1003
1026
|
...terminalActivityError(terminal.assistantMessage),
|
|
1004
|
-
...(run.cacheWarm ? { cacheWarm: true as const } : {}),
|
|
1005
1027
|
};
|
|
1006
|
-
|
|
1028
|
+
run.stored.session.updatedAt = timestamp;
|
|
1007
1029
|
}, true);
|
|
1008
1030
|
}
|
|
1009
1031
|
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
IFlexCreateSessionOptions,
|
|
9
9
|
IFlexDeleteSessionGenerationCohortInput,
|
|
10
10
|
IFlexDeleteSessionGenerationCohortResult,
|
|
11
|
+
IFlexEnqueuePromptOptions,
|
|
11
12
|
IFlexEventArchiveMetadata,
|
|
12
13
|
IFlexHarnessOptions,
|
|
13
14
|
IFlexImportedRun,
|
|
@@ -25,7 +26,6 @@ import type {
|
|
|
25
26
|
IFlexPromptAdmission,
|
|
26
27
|
IFlexPromptQueueAdmission,
|
|
27
28
|
IFlexPromptQueueEntry,
|
|
28
|
-
IFlexPromptOptions,
|
|
29
29
|
IFlexPromptResult,
|
|
30
30
|
IFlexRedoSessionResult,
|
|
31
31
|
IFlexScheduledPromptAdmission,
|
|
@@ -305,7 +305,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
305
305
|
scopeId: string,
|
|
306
306
|
sessionId: string,
|
|
307
307
|
prompt: TFlexPrompt,
|
|
308
|
-
options?:
|
|
308
|
+
options?: IFlexEnqueuePromptOptions,
|
|
309
309
|
): Promise<IFlexPromptResult> {
|
|
310
310
|
return this.core.prompts.prompt(scopeId, sessionId, prompt, options);
|
|
311
311
|
}
|
|
@@ -314,7 +314,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
314
314
|
scopeId: string,
|
|
315
315
|
sessionId: string,
|
|
316
316
|
prompt: TFlexPrompt,
|
|
317
|
-
options?:
|
|
317
|
+
options?: IFlexEnqueuePromptOptions,
|
|
318
318
|
): Promise<IFlexPromptAdmission> {
|
|
319
319
|
return this.core.prompts.startPrompt(scopeId, sessionId, prompt, options);
|
|
320
320
|
}
|
|
@@ -323,7 +323,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
323
323
|
scopeId: string,
|
|
324
324
|
sessionId: string,
|
|
325
325
|
prompt: TFlexPrompt,
|
|
326
|
-
options?:
|
|
326
|
+
options?: IFlexEnqueuePromptOptions,
|
|
327
327
|
): Promise<IFlexPromptQueueAdmission> {
|
|
328
328
|
return this.core.prompts.enqueuePrompt(scopeId, sessionId, prompt, options);
|
|
329
329
|
}
|
|
@@ -337,6 +337,13 @@ export interface IRunSteering {
|
|
|
337
337
|
outcome?: IRunSteeringOutcome;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
/** A steer admitted together with the prompt whose run takes it in before its first model call. */
|
|
341
|
+
export interface IAdmittedSteer {
|
|
342
|
+
readonly steerId: string;
|
|
343
|
+
readonly prompt: INormalizedFlexPrompt;
|
|
344
|
+
readonly byteSize: number;
|
|
345
|
+
}
|
|
346
|
+
|
|
340
347
|
export interface IRunSteeringOutcome {
|
|
341
348
|
/** Whether the run's prompt, the steers it took in and what its model produced stay in context. */
|
|
342
349
|
readonly keptInContext: boolean;
|
|
@@ -410,8 +417,13 @@ export interface IQueuedPrompt {
|
|
|
410
417
|
options?: IFlexPromptOptions;
|
|
411
418
|
delegationSeed?: IDelegatedRunAdmissionSeed;
|
|
412
419
|
byteSize: number;
|
|
413
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* The steering of the run the prompt started, once it started one. A prompt admitted with steers
|
|
422
|
+
* has it from admission on, holding those steers and their bytes.
|
|
423
|
+
*/
|
|
414
424
|
steering?: IRunSteering;
|
|
425
|
+
/** The steers admitted with the prompt, in their given order. */
|
|
426
|
+
admittedSteerIds?: readonly string[];
|
|
415
427
|
completion: Promise<IFlexPromptResult>;
|
|
416
428
|
resolveCompletion: (result: IFlexPromptResult) => void;
|
|
417
429
|
rejectCompletion: (error: unknown) => void;
|
package/ts/interfaces.ts
CHANGED
|
@@ -239,8 +239,6 @@ export interface IFlexSessionActivity {
|
|
|
239
239
|
limit?: TFlexModelLimit;
|
|
240
240
|
/** The permission whose rejection stopped the last run, next to its error. */
|
|
241
241
|
rejectedPermissionId?: string;
|
|
242
|
-
/** The run is or was a cache-warm turn; see {@link IFlexMessage.cacheWarm}. */
|
|
243
|
-
cacheWarm?: true;
|
|
244
242
|
}
|
|
245
243
|
|
|
246
244
|
export interface IFlexSession {
|
|
@@ -590,6 +588,19 @@ export interface IFlexSchedulePromptOptions extends IFlexPromptOptions {
|
|
|
590
588
|
debounceMs?: number;
|
|
591
589
|
}
|
|
592
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Options of a prompt that starts a run now or queues it. `steers` are further user messages that
|
|
593
|
+
* join the prompt's run in the order given: the run accepts them when it starts and its generation
|
|
594
|
+
* takes them in before its first model call, each as a user message of its own carrying its
|
|
595
|
+
* `steerId`. They count against the session's outstanding prompt limits from admission on, and
|
|
596
|
+
* report like every other steer of the run: `prompt.steer.applied` names each message, and
|
|
597
|
+
* `unappliedSteerIds` names the ones the model context of later turns does not hold, also when the
|
|
598
|
+
* prompt is cancelled before its run started.
|
|
599
|
+
*/
|
|
600
|
+
export interface IFlexEnqueuePromptOptions extends IFlexPromptOptions {
|
|
601
|
+
steers?: readonly IFlexSteerInput[];
|
|
602
|
+
}
|
|
603
|
+
|
|
593
604
|
export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
|
|
594
605
|
scheduleKey: string;
|
|
595
606
|
}
|
package/ts/readme.md
CHANGED
|
@@ -730,6 +730,8 @@ The run takes a steer in at its next step boundary: after the tool results of it
|
|
|
730
730
|
|
|
731
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.
|
|
732
732
|
|
|
733
|
+
A prompt can bring further user messages into its run: `startPrompt()`, `enqueuePrompt()` and `prompt()` take `steers`, a list of `{ steerId, prompt }`, in their options. The run accepts them when it starts, emitting `prompt.steer.accepted` for each in the given order right after `prompt.started`, and takes them in before its first model call, so the model sees the prompt and its steers as consecutive user messages of their own in one call, each with its own `steerId`, `messageId` and attachments. From then on they report like every other steer of the run. They count against the session's outstanding prompt limits from admission on. The whole prompt is refused with `FlexHarnessValidationError` when a `steerId` repeats, and with `FlexHarnessQueueFullError` when it brings more steers than `maxOutstandingPromptsPerSession` or its steers' bytes exceed the outstanding byte limit. A prompt cancelled before its run started lists them in `unappliedSteerIds` on its finished queue entry and `prompt.finished`, with `keptInContext: false`. A caller holding several waiting messages for a session delivers them in one turn this way, without merging them into one message.
|
|
734
|
+
|
|
733
735
|
### Keeping the Prompt Cache Warm
|
|
734
736
|
|
|
735
737
|
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:
|
|
@@ -1157,7 +1159,7 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
1157
1159
|
unsubscribe();
|
|
1158
1160
|
```
|
|
1159
1161
|
|
|
1160
|
-
Events are discriminated, deeply immutable values with one global sequence within each `FlexHarness` instance. Every `IFlexEventBase` carries the exact source `sessionGenerationId` and `sessionGenerationSequence`; `session.deleted` retains the deleted generation after live metadata is removed. Listener exceptions are isolated from runs and other listeners. Every accepted queue entry emits `prompt.queued` and exactly one `prompt.finished`. Every accepted steer emits `prompt.steer.accepted
|
|
1162
|
+
Events are discriminated, deeply immutable values with one global sequence within each `FlexHarness` instance. Every `IFlexEventBase` carries the exact source `sessionGenerationId` and `sessionGenerationSequence`; `session.deleted` retains the deleted generation after live metadata is removed. Listener exceptions are isolated from runs and other listeners. Every accepted queue entry emits `prompt.queued` and exactly one `prompt.finished`. Every accepted steer emits `prompt.steer.accepted` (a steer admitted with its prompt when the run starts, and not at all when the prompt is cancelled or fails before), and `prompt.steer.applied`, after the `message.*` events of its user message, once the run took it in; `run.finished` lists it in `unappliedSteerIds` unless it is in the model context of later turns. Durable promotion additionally emits `prompt.started`, and actual model preparation emits `prompt.running`; cancellation or failure can omit either intermediate event. Existing durable run/message terminal events precede `prompt.finished`. Every callback-backed streamed text part that stays in its message emits exactly one `part.completed` event before the corresponding `run.finished` event: once the model step that streamed it is committed, so its text is final and saved, or with the run's terminal projection when that step did not commit. Text a later model step streams starts a new part. When a model call fails and is retried, the text and reasoning parts it streamed are not in the session: `part.removed` takes each of them out of its message, last part first, and the retry streams its own parts. Its `partIndex` is the part's position before it left; the parts after it move up by one. A removed part has no further events, and `part.removed` does not count against `callbackLimits`. The tool parts of calls the failed model call executed stay. Every `part.started`, `part.delta`, `part.updated`, `part.completed`, and `part.removed` event carries zero-based `messageIndex` and `partIndex` coordinates from the session's authoritative message and part sequences.
|
|
1161
1163
|
|
|
1162
1164
|
`getSessionView(scopeId, sessionId)` reads a session's metadata, visible messages and pending permission requests in one step, together with the instance's event `sequence` they include. The messages include what a running turn has streamed so far, so a consumer that applies every event of the instance with a greater `sequence` follows the session exactly from the view on; events up to `sequence` are already in it. The read waits while a queued write or a change that emits events for the session is in progress; a session that keeps changing through 64 consecutive waits is refused with `FlexHarnessSessionBusyError`, and the read can be repeated. Session metadata is current: status changes such as `waiting_permission` emit no event. A permission request its run leaves unanswered leaves `pendingPermissions` without an event of its own; the run's `run.finished` follows.
|
|
1163
1165
|
|
package/ts/utils.json.ts
CHANGED
|
@@ -578,6 +578,8 @@ 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
|
+
// FlexHarness 13.2.0 stored a warm turn's activity with `cacheWarm`; session repair on load
|
|
582
|
+
// removes it and restores the latest real turn's activity.
|
|
581
583
|
if (activity.cacheWarm !== undefined && activity.cacheWarm !== true) {
|
|
582
584
|
throw new FlexHarnessStoreFormatError(`${path}.activity.cacheWarm must be true when present.`);
|
|
583
585
|
}
|
|
@@ -215,9 +215,7 @@ 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. */
|
|
219
218
|
export function flexSessionToHarnessStatus(session: IFlexSession): plugins.IHarnessStatus {
|
|
220
|
-
if (session.activity.cacheWarm) return { type: 'idle' };
|
|
221
219
|
switch (session.activity.status) {
|
|
222
220
|
case 'running':
|
|
223
221
|
case 'scheduled':
|
|
@@ -249,7 +247,7 @@ export function flexSessionToHarnessMeta(session: IFlexSession): plugins.IHarnes
|
|
|
249
247
|
title: session.title ?? session.sessionId,
|
|
250
248
|
...(epoch(session.createdAt) === undefined ? {} : { createdAt: epoch(session.createdAt) }),
|
|
251
249
|
...(epoch(session.updatedAt) === undefined ? {} : { updatedAt: epoch(session.updatedAt) }),
|
|
252
|
-
state:
|
|
250
|
+
state: sessionCardStates[session.activity.status],
|
|
253
251
|
};
|
|
254
252
|
}
|
|
255
253
|
|
|
@@ -78,7 +78,7 @@ The host forwards every event of the harness instance, of every session, in orde
|
|
|
78
78
|
- Permission rows stay in the transcript once answered, showing the decision. A request its run leaves unanswered, because the run was stopped or failed, is removed when the run finishes.
|
|
79
79
|
- FlexHarness emits no event for project state. After a run, or after a project tool's row completes, read `getProjectState()` and pass it to `applyProjectState()` for the task list and the scratchpad.
|
|
80
80
|
- `questions` is always empty: FlexHarness has no question requests.
|
|
81
|
-
- A cache-warm turn (see the FlexHarness readme, Keeping the Prompt Cache Warm) has no rows and leaves the status
|
|
81
|
+
- A cache-warm turn (see the FlexHarness readme, Keeping the Prompt Cache Warm) has no rows and leaves the status and the session card as the last real turn left them and `activeQueueId` unset, so a steer never joins it. Its usage still counts in `usage`: it is spent.
|
|
82
82
|
- Messages the session has not taken in yet (a queued prompt, a steer) carry no text in FlexHarness events. The host that sent them shows them as pending rows.
|
|
83
83
|
- An attachment row needs a name, a media type and a size. The public projection keeps no payload and states these only when the prompt did, so an attachment without all three, such as a remote URL of unknown size, has no row.
|
|
84
84
|
|