@modelprofile.com/flexharness 13.2.1 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.callbacks.d.ts +3 -4
- package/dist_ts/classes.flexharness.callbacks.js +5 -14
- package/dist_ts/classes.flexharness.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 +56 -9
- package/dist_ts/classes.flexharness.reversions.js +9 -3
- package/dist_ts/classes.flexharness.runs.d.ts +6 -1
- package/dist_ts/classes.flexharness.runs.js +18 -3
- package/dist_ts/interfaces.d.ts +17 -3
- package/dist_ts/interfaces.flexharness.d.ts +12 -1
- package/dist_ts/interfaces.js +1 -1
- package/package.json +3 -3
- package/readme.md +14 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.callbacks.ts +4 -12
- package/ts/classes.flexharness.prompts.ts +67 -10
- package/ts/classes.flexharness.reversions.ts +10 -2
- package/ts/classes.flexharness.runs.ts +24 -2
- package/ts/classes.flexharness.ts +4 -4
- package/ts/interfaces.flexharness.ts +13 -1
- package/ts/interfaces.ts +18 -3
- package/ts/readme.md +14 -10
|
@@ -384,22 +384,14 @@ export class FlexHarnessRunCallbacks<TScope> {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
/**
|
|
387
|
-
* Announces a retry
|
|
388
|
-
* parts
|
|
389
|
-
*
|
|
390
|
-
* by the run's parts. What the removed parts took of the limits stays taken.
|
|
387
|
+
* Announces a retry after the failed inference's observed output was saved. Keep those
|
|
388
|
+
* immutable parts in the transcript; a retry does not erase the work it follows.
|
|
389
|
+
* Completing a text part ends that part, not the run or its unsuccessful inference.
|
|
391
390
|
*/
|
|
392
391
|
public onRetry(state: IStorageState, sessionId: string, retry: plugins.IAgentRetryEvent): void {
|
|
393
392
|
const run = this.callbackRun(state, sessionId);
|
|
394
393
|
if (!run || !this.reserveCallbackCapacity(run, 1, 0, 0)) return;
|
|
395
|
-
|
|
396
|
-
const part = run.callbackParts[partIndex]!;
|
|
397
|
-
if (!run.stepPartIds.has(part.partId)) continue;
|
|
398
|
-
this.core.emitPartRemovedEvent(run, part);
|
|
399
|
-
run.callbackParts.splice(partIndex, 1);
|
|
400
|
-
run.partTextUtf8Bytes.delete(part.partId);
|
|
401
|
-
}
|
|
402
|
-
run.stepPartIds.clear();
|
|
394
|
+
this.onStepCommitted(state, sessionId);
|
|
403
395
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
404
396
|
type: 'run.retrying',
|
|
405
397
|
runId: run.runId,
|
|
@@ -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);
|
|
@@ -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,
|
|
@@ -1224,8 +1224,16 @@ export class FlexHarnessReversions<TScope> {
|
|
|
1224
1224
|
const nextCandidate = candidates[1];
|
|
1225
1225
|
let end = 0;
|
|
1226
1226
|
if (!firstCandidate) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1227
|
+
// Unsuccessful turns with output still contribute to model context. Keep their
|
|
1228
|
+
// segments so the next completed turn can group (and undo) that leading history.
|
|
1229
|
+
const firstVisibleSegment = dropNonUndoablePrefix
|
|
1230
|
+
? stored.reversionSegments.findIndex((segment) => plugins.isGenerationInModelContext(
|
|
1231
|
+
stored.agentSession.getEvents(), segment.runId,
|
|
1232
|
+
))
|
|
1233
|
+
: -1;
|
|
1234
|
+
if (dropNonUndoablePrefix && firstVisibleSegment !== 0) {
|
|
1235
|
+
end = firstVisibleSegment < 0 ? stored.reversionSegments.length : firstVisibleSegment;
|
|
1236
|
+
} else if (overLimit) {
|
|
1229
1237
|
end = Math.max(
|
|
1230
1238
|
1,
|
|
1231
1239
|
stored.reversionSegments.length + (reserveSegment ? 1 : 0)
|
|
@@ -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,
|
|
@@ -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
|
@@ -404,6 +404,7 @@ export interface IFlexUpdateSessionOptions {
|
|
|
404
404
|
export interface IFlexPromptOptions {
|
|
405
405
|
modelHint?: string;
|
|
406
406
|
system?: string;
|
|
407
|
+
/** Optional positive safe integer step cap; no limit unless configured. */
|
|
407
408
|
maxSteps?: number;
|
|
408
409
|
}
|
|
409
410
|
|
|
@@ -588,6 +589,19 @@ export interface IFlexSchedulePromptOptions extends IFlexPromptOptions {
|
|
|
588
589
|
debounceMs?: number;
|
|
589
590
|
}
|
|
590
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Options of a prompt that starts a run now or queues it. `steers` are further user messages that
|
|
594
|
+
* join the prompt's run in the order given: the run accepts them when it starts and its generation
|
|
595
|
+
* takes them in before its first model call, each as a user message of its own carrying its
|
|
596
|
+
* `steerId`. They count against the session's outstanding prompt limits from admission on, and
|
|
597
|
+
* report like every other steer of the run: `prompt.steer.applied` names each message, and
|
|
598
|
+
* `unappliedSteerIds` names the ones the model context of later turns does not hold, also when the
|
|
599
|
+
* prompt is cancelled before its run started.
|
|
600
|
+
*/
|
|
601
|
+
export interface IFlexEnqueuePromptOptions extends IFlexPromptOptions {
|
|
602
|
+
steers?: readonly IFlexSteerInput[];
|
|
603
|
+
}
|
|
604
|
+
|
|
591
605
|
export interface IFlexScheduledPromptAdmission extends IFlexPromptAdmission {
|
|
592
606
|
scheduleKey: string;
|
|
593
607
|
}
|
|
@@ -653,6 +667,7 @@ export interface IFlexResolvedModel {
|
|
|
653
667
|
system?: string;
|
|
654
668
|
providerOptions?: TFlexAgentProviderOptions;
|
|
655
669
|
cache?: TFlexAgentCacheSetting;
|
|
670
|
+
/** Optional positive safe integer step cap; no limit unless configured. */
|
|
656
671
|
maxSteps?: number;
|
|
657
672
|
}
|
|
658
673
|
|
|
@@ -1890,6 +1905,7 @@ export interface IFlexSubagentDefinition {
|
|
|
1890
1905
|
description: string;
|
|
1891
1906
|
modelHint?: string;
|
|
1892
1907
|
system?: string;
|
|
1908
|
+
/** Optional positive safe integer step cap; no limit unless configured. */
|
|
1893
1909
|
maxSteps?: number;
|
|
1894
1910
|
/** Exact model-facing names permitted for this agent; omitted means unrestricted. */
|
|
1895
1911
|
allowedTools?: readonly string[];
|
|
@@ -2107,9 +2123,8 @@ export interface IFlexPartDeltaEvent extends IFlexPartEventBase {
|
|
|
2107
2123
|
}
|
|
2108
2124
|
|
|
2109
2125
|
/**
|
|
2110
|
-
* A
|
|
2111
|
-
*
|
|
2112
|
-
* after it move up by one.
|
|
2126
|
+
* A part left its message. `partIndex` is its position before it left; the parts after it
|
|
2127
|
+
* move up by one. Failed-inference observations are retained, not removed for a retry.
|
|
2113
2128
|
*/
|
|
2114
2129
|
export interface IFlexPartRemovedEvent extends IFlexPartEventBase {
|
|
2115
2130
|
readonly type: 'part.removed';
|
package/ts/readme.md
CHANGED
|
@@ -130,7 +130,9 @@ Sessions whose metrics were written before input and cache tokens were counted c
|
|
|
130
130
|
them from their next settled call on and never report the breakdown complete; their
|
|
131
131
|
lifetime total keeps its completeness.
|
|
132
132
|
|
|
133
|
-
Conversational runs
|
|
133
|
+
Conversational runs have **no `maxSteps` limit by default**. A cap is optional and must be a positive finite safe integer; `0`, negative or fractional values, `NaN`, infinity and unsafe integers are invalid, not ways to request unlimited execution. In AgentSession, a prepared generation lease's `maxSteps` takes precedence over the per-generation option, then the session setting; omitting all three leaves execution unbounded. FlexHarness prompt options override the resolved model's cap, and a subagent definition may explicitly cap its child run. Cache-warm turns retain their separate two-step cap. An unbounded run still honors owner stops, permission rejection, real errors, usage accounting and resource cleanup.
|
|
134
|
+
|
|
135
|
+
Conversational runs that exhaust an explicitly configured `maxSteps` with tool work or unseen input remaining
|
|
134
136
|
fail with `FlexHarnessStepLimitError` (`FLEX_STEP_LIMIT`), rather than being marked
|
|
135
137
|
completed with an empty answer. Standalone Agent callers retain tool-only results
|
|
136
138
|
and can inspect `terminationReason: 'model' | 'step-limit'`.
|
|
@@ -620,7 +622,7 @@ Attachment payloads are never copied into public audit messages or events. Publi
|
|
|
620
622
|
}
|
|
621
623
|
```
|
|
622
624
|
|
|
623
|
-
The original string remains only in canonical private Agent events, so a later model turn can receive the attachment again.
|
|
625
|
+
The original string remains only in canonical private Agent events, so a later model turn can receive the attachment again. Successful turns stay in future context. Failed, stopped, rejected and restart-interrupted generations also retain their prompt, applied steers and recorded partial model/tool output once execution started and produced output. Their model-visible outcome marker truthfully says rejected, interrupted or stopped by owner, never successful. An unsuccessful generation with no model/tool output stays out of future context; its `run.finished` event and finished queue entry carry `keptInContext: false`. An unresolved tool call is closed in model context with an error result saying its effect is unknown: this does not assert that the action failed, succeeded or was rolled back. Restoring history never automatically reruns the generation or replays tools; inspect `listUncertainToolExecutions()` and reconcile external effects before treating them as known. Resolver, cleanup and persistence failures still reject completion; retention depends on durable canonical output and outcome, not the failure label.
|
|
624
626
|
|
|
625
627
|
The main session methods are:
|
|
626
628
|
|
|
@@ -728,7 +730,9 @@ console.log(result.unappliedSteerIds); // [] once the run took the steer in
|
|
|
728
730
|
|
|
729
731
|
The run takes a steer in at its next step boundary: after the tool results of its current model step are recorded and before its next model call. A running tool call or a pending permission is never interrupted; the steer waits for the boundary that follows it. A steer taken in is a user message of the run (`steerId` set) that the model sees in its next call and every later turn. If the run had already answered, the assistant message that answered is completed and the run answers on in a new assistant message after the steer; a steer taken in before the run answered anything is placed before its still empty assistant message. `IFlexPromptResult.userMessage` stays the prompt and `assistantMessage` is the run's last assistant message. Undo and redo treat the run's messages as one turn.
|
|
730
732
|
|
|
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
|
|
733
|
+
`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 reports actual canonical retention, not success: a failed or stopped run with retained model/tool output can report `true`. Applied steers in that retained history are not listed as unapplied; pending steers that never became durable are. When the run is not retained, every accepted steer is listed, including ones reported with `prompt.steer.applied`. Decide explicitly whether to send absent input as a later turn; do not automatically replay a retained prompt or its tool effects. 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.
|
|
734
|
+
|
|
735
|
+
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.
|
|
732
736
|
|
|
733
737
|
### Keeping the Prompt Cache Warm
|
|
734
738
|
|
|
@@ -808,7 +812,7 @@ At most 128 custom commands may be registered. Every registration must be a plai
|
|
|
808
812
|
|
|
809
813
|
`/undo` and `/redo`, plus `undoSession()` and `redoSession()`, move a durable history cursor. The direct methods return `{ revertedRunId }` and `{ restoredRunId }`; the slash forms return `{ type: 'operation', name: 'undo' | 'redo' }`. Each committed cursor move emits one `session.history.changed` event with `direction`, `runId`, and the selected session identity. A committed branch emits the same event with `direction: 'branch'` and no `runId`, so controllers should refresh the complete selected session. Capture finalization, cleanup, and metadata-only changes do not emit this event.
|
|
810
814
|
|
|
811
|
-
A completed root-session turn defines an operation-group boundary. Failed and cancelled turns after it belong to that group; leading failed or cancelled turns belong to the first completed group. No completed boundary means there is nothing to undo. Undo applies selected segments in reverse order and redo applies them in forward order. Without `turnReversionProvider`, only transcript and future model context move. Hidden messages disappear from `getMessages()`, message pages, exact message lookup, and future model context. Starting a new prompt, template, handler, or compaction from an undone position commits a branch: hidden messages and segments are removed durably and cannot be redone. Successful event archival also commits hidden redo history; a missing compaction or failed archive leaves it intact. Automatic budget compaction archives covered events after generation finalization and advances the same undo horizon; set `contextCompactionBytes: false` to disable that automatic policy.
|
|
815
|
+
A completed root-session turn defines an operation-group boundary. Failed and cancelled turns after it belong to that group; leading failed or cancelled turns belong to the first completed group. No completed boundary means there is nothing to undo; retained leading unsuccessful history is grouped with the next completed turn rather than silently discarded. Undo hides that group's canonical partial history and outcome markers as well as its visible messages, redo restores them, and branching keeps the hidden history excluded. Transcript-only undo never claims to reverse external tool effects. Undo applies selected segments in reverse order and redo applies them in forward order. Without `turnReversionProvider`, only transcript and future model context move. Hidden messages disappear from `getMessages()`, message pages, exact message lookup, and future model context. Starting a new prompt, template, handler, or compaction from an undone position commits a branch: hidden messages and segments are removed durably and cannot be redone. Successful event archival also commits hidden redo history; a missing compaction or failed archive leaves it intact. Automatic budget compaction archives covered events after generation finalization and advances the same undo horizon; set `contextCompactionBytes: false` to disable that automatic policy.
|
|
812
816
|
|
|
813
817
|
Two horizons bound undo. Retention pruning removes the oldest complete visible units when `reversionLimits` is exceeded. Explicit event archival marks covered turns context-unavailable and prunes complete prefixes that can no longer be rebuilt; FlexHarness never crosses that archive horizon. Manual compaction without archival retains the original events and remains undoable. Schema-1 projection history and sessions migrated from `2.x` have no reversion segments, so historical turns are not retroactively undoable; newly written turns are tracked normally.
|
|
814
818
|
|
|
@@ -942,11 +946,11 @@ Applications removing a scope must stop and serialize new admission across every
|
|
|
942
946
|
|
|
943
947
|
The model context is built by the session's canonical AgentSession event history as:
|
|
944
948
|
|
|
945
|
-
1. Previous canonically accepted generations.
|
|
949
|
+
1. Previous canonically accepted generations and unsuccessful generations with recorded model/tool output, including their truthful outcome markers, subject to the reversion cursor and compaction horizon.
|
|
946
950
|
2. The normalized current user message.
|
|
947
951
|
3. AgentSession's result messages.
|
|
948
952
|
|
|
949
|
-
A failed or cancelled prompt remains visible through `getMessages()
|
|
953
|
+
A failed or cancelled prompt remains visible through `getMessages()` with its truthful `failed` or `cancelled` status. Its canonical partial history also remains in future model context when execution produced output; an unsuccessful generation without output is omitted. Retention is not acceptance and does not roll back or replay external effects. Canonical Agent events remain private and are not exposed by the session or message APIs.
|
|
950
954
|
|
|
951
955
|
Resolved model identity contains provider and model IDs plus optional display name and effective `variant`. The identity, including its variant, is attached to a failed assistant message when resolution completed before a later failure, matching the provider/model behavior. Prompt results contain it only on success.
|
|
952
956
|
|
|
@@ -1133,8 +1137,8 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
1133
1137
|
break;
|
|
1134
1138
|
case 'run.finished':
|
|
1135
1139
|
markRunFinished(event.runId, event.status);
|
|
1136
|
-
//
|
|
1137
|
-
|
|
1140
|
+
// Retention is not success. Offer absent input for explicit resubmission; never autoplay effects.
|
|
1141
|
+
offerResubmission(event.keptInContext ? [] : [event.runId], event.unappliedSteerIds);
|
|
1138
1142
|
break;
|
|
1139
1143
|
case 'prompt.steer.accepted':
|
|
1140
1144
|
markSteerPending(event.steerId);
|
|
@@ -1157,7 +1161,7 @@ const unsubscribe = harness.subscribe((event) => {
|
|
|
1157
1161
|
unsubscribe();
|
|
1158
1162
|
```
|
|
1159
1163
|
|
|
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
|
|
1164
|
+
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
1165
|
|
|
1162
1166
|
`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
1167
|
|
|
@@ -1205,7 +1209,7 @@ The JSON stores are explicitly not cross-process safe. When several processes ca
|
|
|
1205
1209
|
|
|
1206
1210
|
Direct store operations and non-run session mutations surface conflicts as `FlexHarnessStoreConflictError` or the corresponding AgentSession store conflict. Malformed, wrong-schema, or non-JSON snapshots are surfaced as `FlexHarnessStoreFormatError`. A write or deletion that changed its target but cannot confirm parent-directory durability surfaces `FlexHarnessStoreCommitUncertainError` with the affected path, operation, and cause. Run persistence failures cross the external error boundary and therefore become `FlexHarnessExternalError`. FlexHarness does not merge conflicts.
|
|
1207
1211
|
|
|
1208
|
-
Each domain serializes its own mutations. A successful run first persists a hidden completed projection, then finalizes the canonical Agent generation as `accepted`, then promotes the hidden projection publicly. Recovery uses the canonical generation outcome to promote an accepted stage or publish a failed/cancelled projection. Failed and cancelled generations remain auditable
|
|
1212
|
+
Each domain serializes its own mutations. A successful run first persists a hidden completed projection, then finalizes the canonical Agent generation as `accepted`, then promotes the hidden projection publicly. Recovery uses the canonical generation outcome to promote an accepted stage or publish a failed/cancelled projection. Failed and cancelled generations remain auditable and, once execution produced model/tool output, also remain in future model context with their actual outcome; no-output unsuccessful generations remain excluded.
|
|
1209
1213
|
|
|
1210
1214
|
Projection stores accept schema version 1, 2, or 3 from `load()`, but every `save()` receives a change that makes the current schema-3 shape. A change to a loaded schema-1 or schema-2 projection carries both lists whole. Schema 3 records explicit reversion protocol, transcript/workspace provenance, and V2 disposition. A terminal V2 segment must be conclusively `revertible`, `no-change`, or `nonrevertible`; a pending V2 capture remains owned by its capture WAL and is never treated as transcript history.
|
|
1211
1215
|
|