@modelprofile.com/flexharness 9.9.1 → 9.10.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 +2 -2
- package/dist_ts/classes.flexharness.d.ts +32 -1
- package/dist_ts/classes.flexharness.js +269 -20
- package/dist_ts/errors.d.ts +11 -0
- package/dist_ts/errors.js +18 -1
- package/dist_ts/interfaces.d.ts +58 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.json.js +8 -1
- package/dist_ts/utils.transfer.js +3 -1
- package/package.json +3 -3
- package/readme.md +29 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +338 -23
- package/ts/errors.ts +24 -0
- package/ts/interfaces.ts +63 -0
- package/ts/plugins.ts +7 -0
- package/ts/readme.md +29 -2
- package/ts/utils.json.ts +7 -0
- package/ts/utils.transfer.ts +1 -0
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
FlexHarnessSessionBusyError,
|
|
14
14
|
FlexHarnessSlashCommandReentryError,
|
|
15
15
|
FlexHarnessSlashCommandUnavailableError,
|
|
16
|
+
FlexHarnessSteerRejectedError,
|
|
16
17
|
FlexHarnessStoreConflictError,
|
|
17
18
|
FlexHarnessStoreCommitUncertainError,
|
|
18
19
|
FlexHarnessValidationError,
|
|
@@ -58,6 +59,8 @@ import type {
|
|
|
58
59
|
IFlexPermissionRequestInput,
|
|
59
60
|
IFlexPermissionSnapshot,
|
|
60
61
|
IFlexProjectGoalResult,
|
|
62
|
+
IFlexSteerAdmission,
|
|
63
|
+
IFlexSteerInput,
|
|
61
64
|
IFlexProjectManagementSessionContext,
|
|
62
65
|
IFlexProjectManagementSnapshot,
|
|
63
66
|
IFlexProjectManagementTombstone,
|
|
@@ -407,6 +410,31 @@ interface IActiveRun {
|
|
|
407
410
|
delegatedRunAdmissionOwner?: IDelegatedRunAdmissionOwner;
|
|
408
411
|
delegatedRunAdmissionClose?: Promise<void>;
|
|
409
412
|
completion: Promise<IFlexPromptResult>;
|
|
413
|
+
steering: IRunSteering;
|
|
414
|
+
/** Stops the run's generation for its owner, so what the model produced stays in context. */
|
|
415
|
+
stop: AbortController;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The steers of a run: the input its generation takes in, and the prompts it accepted for it. */
|
|
419
|
+
interface IRunSteering {
|
|
420
|
+
readonly input: plugins.AgentPendingInputQueue;
|
|
421
|
+
/** The session whose outstanding prompt bytes hold the bytes of the steers not taken in yet. */
|
|
422
|
+
readonly stored: IStoredSessionState;
|
|
423
|
+
/** Every steer the run accepted, in arrival order, by steer id. */
|
|
424
|
+
readonly accepted: Map<string, { prompt: INormalizedFlexPrompt; byteSize: number }>;
|
|
425
|
+
/** Accepted steers the generation recorded durably as user messages of the run. */
|
|
426
|
+
readonly applied: Set<string>;
|
|
427
|
+
/** Whether the run stays in the model context of later turns, once that is established. */
|
|
428
|
+
keptInContext?: boolean;
|
|
429
|
+
/** What became of the run's steers, fixed when the run ends. */
|
|
430
|
+
outcome?: IRunSteeringOutcome;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
interface IRunSteeringOutcome {
|
|
434
|
+
/** Whether the run's prompt, the steers it took in and what its model produced stay in context. */
|
|
435
|
+
readonly keptInContext: boolean;
|
|
436
|
+
/** Accepted steers the model context of later turns does not hold, in arrival order. */
|
|
437
|
+
readonly unappliedSteerIds: readonly string[];
|
|
410
438
|
}
|
|
411
439
|
|
|
412
440
|
interface IDelegatedRunAdmissionSeed {
|
|
@@ -472,6 +500,8 @@ interface IQueuedPrompt {
|
|
|
472
500
|
options?: IFlexPromptOptions;
|
|
473
501
|
delegationSeed?: IDelegatedRunAdmissionSeed;
|
|
474
502
|
byteSize: number;
|
|
503
|
+
/** The steering of the run the prompt started, once it started one. */
|
|
504
|
+
steering?: IRunSteering;
|
|
475
505
|
completion: Promise<IFlexPromptResult>;
|
|
476
506
|
resolveCompletion: (result: IFlexPromptResult) => void;
|
|
477
507
|
rejectCompletion: (error: unknown) => void;
|
|
@@ -2860,6 +2890,89 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2860
2890
|
);
|
|
2861
2891
|
}
|
|
2862
2892
|
|
|
2893
|
+
/**
|
|
2894
|
+
* Steers the running run of the prompt `queueId`: the run takes `steer.prompt` in as a user
|
|
2895
|
+
* message at its next step boundary, after the tool results of its current model step are
|
|
2896
|
+
* recorded and before its next model call. A running tool call or permission wait is not
|
|
2897
|
+
* interrupted. A steer the run has not taken in when it ends is reported in its
|
|
2898
|
+
* `unappliedSteerIds`; a steer for a prompt that has no running run, or whose run is ending,
|
|
2899
|
+
* is refused with `FlexHarnessSteerRejectedError` reason `not-running` and belongs to the
|
|
2900
|
+
* next turn.
|
|
2901
|
+
*/
|
|
2902
|
+
public async steerPrompt(
|
|
2903
|
+
scopeId: string,
|
|
2904
|
+
sessionId: string,
|
|
2905
|
+
queueId: string,
|
|
2906
|
+
steer: IFlexSteerInput,
|
|
2907
|
+
): Promise<IFlexSteerAdmission> {
|
|
2908
|
+
validateIdentifier(queueId, 'queueId');
|
|
2909
|
+
requireTransferIdentifier(queueId, 'queueId');
|
|
2910
|
+
validateIdentifier(steer.steerId, 'steerId');
|
|
2911
|
+
requireTransferIdentifier(steer.steerId, 'steerId');
|
|
2912
|
+
this.assertOpen();
|
|
2913
|
+
const prompt = normalizeFlexPrompt(steer.prompt);
|
|
2914
|
+
const byteSize = this.promptAdmissionByteSize(steer.prompt, {});
|
|
2915
|
+
const { state } = await this.resolveState(scopeId);
|
|
2916
|
+
this.assertStateAcceptingWork(state);
|
|
2917
|
+
const stored = this.requireSession(state, sessionId);
|
|
2918
|
+
this.assertSessionWritable(stored);
|
|
2919
|
+
if (stored.session.agent !== undefined) {
|
|
2920
|
+
throw new FlexHarnessValidationError(
|
|
2921
|
+
'Subagent sessions can only be prompted through the foreground delegate tool.',
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
const run = state.activeRuns.get(sessionId);
|
|
2925
|
+
if (!run || run.queueId !== queueId) {
|
|
2926
|
+
if (stored.outstandingPromptsById.has(queueId) || stored.terminalPromptQueueEntries.has(queueId)) {
|
|
2927
|
+
throw new FlexHarnessSteerRejectedError('not-running', steer.steerId);
|
|
2928
|
+
}
|
|
2929
|
+
throw new FlexHarnessNotFoundError('Prompt queue entry', queueId);
|
|
2930
|
+
}
|
|
2931
|
+
const steering = run.steering;
|
|
2932
|
+
if (!steering.input.closed && !steering.accepted.has(steer.steerId)) {
|
|
2933
|
+
if (
|
|
2934
|
+
steering.accepted.size - steering.applied.size
|
|
2935
|
+
>= this.promptQueueLimits.maxOutstandingPromptsPerSession
|
|
2936
|
+
) {
|
|
2937
|
+
throw new FlexHarnessQueueFullError(`Session "${sessionId}" has reached its pending steer limit.`);
|
|
2938
|
+
}
|
|
2939
|
+
// Pending steers hold outstanding prompt bytes of the session until they are taken in.
|
|
2940
|
+
if (
|
|
2941
|
+
steering.stored.outstandingPromptBytes + byteSize
|
|
2942
|
+
> this.promptQueueLimits.maxOutstandingBytesPerSession
|
|
2943
|
+
) {
|
|
2944
|
+
throw new FlexHarnessQueueFullError(
|
|
2945
|
+
`Session "${sessionId}" has reached its outstanding prompt byte limit.`,
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
try {
|
|
2950
|
+
steering.input.add({
|
|
2951
|
+
inputId: steer.steerId,
|
|
2952
|
+
content: cloneSerializable(prompt.modelMessage.content) as Parameters<
|
|
2953
|
+
plugins.IAgentSession['beginGeneration']
|
|
2954
|
+
>[0],
|
|
2955
|
+
});
|
|
2956
|
+
} catch (error) {
|
|
2957
|
+
if (error instanceof plugins.AgentPendingInputRejectedError) {
|
|
2958
|
+
throw new FlexHarnessSteerRejectedError(
|
|
2959
|
+
error.reason === 'duplicate' ? 'duplicate' : 'not-running',
|
|
2960
|
+
steer.steerId,
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
throw error;
|
|
2964
|
+
}
|
|
2965
|
+
steering.accepted.set(steer.steerId, { prompt, byteSize });
|
|
2966
|
+
steering.stored.outstandingPromptBytes += byteSize;
|
|
2967
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
2968
|
+
type: 'prompt.steer.accepted',
|
|
2969
|
+
queueId,
|
|
2970
|
+
runId: run.runId,
|
|
2971
|
+
steerId: steer.steerId,
|
|
2972
|
+
});
|
|
2973
|
+
return { steerId: steer.steerId, queueId, runId: run.runId };
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2863
2976
|
public async abort(scopeId: string, sessionId: string): Promise<boolean> {
|
|
2864
2977
|
const { scope, state } = await this.resolveState(scopeId);
|
|
2865
2978
|
const run = state.activeRuns.get(sessionId);
|
|
@@ -3966,7 +4079,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3966
4079
|
}
|
|
3967
4080
|
|
|
3968
4081
|
private createActiveRun(queued: IQueuedPrompt): IActiveRun {
|
|
4082
|
+
const steering: IRunSteering = {
|
|
4083
|
+
input: new plugins.AgentPendingInputQueue(),
|
|
4084
|
+
stored: queued.stored,
|
|
4085
|
+
accepted: new Map(),
|
|
4086
|
+
applied: new Set(),
|
|
4087
|
+
};
|
|
4088
|
+
queued.steering = steering;
|
|
3969
4089
|
return {
|
|
4090
|
+
steering,
|
|
4091
|
+
stop: new AbortController(),
|
|
3970
4092
|
subagents: this.subagents,
|
|
3971
4093
|
state: queued.state,
|
|
3972
4094
|
stored: queued.stored,
|
|
@@ -4410,8 +4532,11 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4410
4532
|
const generateOptions = {
|
|
4411
4533
|
transaction: run.transaction,
|
|
4412
4534
|
abort: run.controller.signal,
|
|
4535
|
+
stop: run.stop.signal,
|
|
4413
4536
|
prepare: (context: { generationId: string; abortSignal: AbortSignal }) =>
|
|
4414
4537
|
this.prepareGeneration(run, options, context.abortSignal),
|
|
4538
|
+
pendingInput: run.steering.input,
|
|
4539
|
+
onInputApplied: (input: plugins.IAgentAppliedInput) => this.applyRunSteer(run, input),
|
|
4415
4540
|
};
|
|
4416
4541
|
const generation = this.withRunCompactorContext(run, () => run.scheduleKey
|
|
4417
4542
|
? run.stored.agentSession.scheduleGenerate({
|
|
@@ -4482,11 +4607,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4482
4607
|
delete queued.prompt;
|
|
4483
4608
|
delete queued.options;
|
|
4484
4609
|
delete queued.delegationSeed;
|
|
4610
|
+
const steering = queued.steering === undefined ? undefined : this.closeRunSteering(queued.steering);
|
|
4485
4611
|
const terminal: IFlexPromptQueueEntry = {
|
|
4486
4612
|
...this.projectPromptQueueEntry(queued),
|
|
4487
4613
|
status,
|
|
4488
4614
|
finishedAt: new Date().toISOString(),
|
|
4489
4615
|
...(error === undefined ? {} : { error: errorToInfo(error) }),
|
|
4616
|
+
keptInContext: steering?.keptInContext ?? false,
|
|
4617
|
+
...(steering === undefined || steering.unappliedSteerIds.length === 0
|
|
4618
|
+
? {}
|
|
4619
|
+
: { unappliedSteerIds: [...steering.unappliedSteerIds] }),
|
|
4490
4620
|
};
|
|
4491
4621
|
stored.terminalPromptQueueEntries.set(queued.queueId, terminal);
|
|
4492
4622
|
while (
|
|
@@ -4590,6 +4720,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4590
4720
|
await this.fenceNamespace(run.state, run, projected);
|
|
4591
4721
|
throw projected;
|
|
4592
4722
|
}
|
|
4723
|
+
run.steering.keptInContext = true;
|
|
4593
4724
|
run.phase = 'promoting';
|
|
4594
4725
|
try {
|
|
4595
4726
|
await this.promoteCompletedTerminal(run, terminal);
|
|
@@ -4616,6 +4747,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4616
4747
|
usage: publicSnapshot(terminal.usage!),
|
|
4617
4748
|
finishReason: terminal.finishReason!,
|
|
4618
4749
|
steps: terminal.steps!,
|
|
4750
|
+
unappliedSteerIds: [...this.closeRunSteering(run.steering).unappliedSteerIds],
|
|
4619
4751
|
};
|
|
4620
4752
|
} catch (error) {
|
|
4621
4753
|
if (run.state.lifecycle === 'fenced') {
|
|
@@ -4680,14 +4812,21 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4680
4812
|
try {
|
|
4681
4813
|
await this.withRunCompactorContext(
|
|
4682
4814
|
run,
|
|
4683
|
-
() => run.stored.agentSession.finalizeGeneration(
|
|
4815
|
+
() => run.stored.agentSession.finalizeGeneration(
|
|
4816
|
+
run.transaction!,
|
|
4817
|
+
desiredOutcome,
|
|
4818
|
+
{ stopped: desiredOutcome === 'interrupted' && cancelled },
|
|
4819
|
+
),
|
|
4684
4820
|
);
|
|
4685
4821
|
} catch (finalizationError) {
|
|
4686
4822
|
errors.push(this.projectExternalError(run, finalizationError, 'agentSession'));
|
|
4687
4823
|
}
|
|
4688
4824
|
let outcome: TCanonicalOutcome | undefined;
|
|
4689
4825
|
try {
|
|
4690
|
-
|
|
4826
|
+
const snapshot = await run.stored.eventStore.load(run.stored.session.sessionId);
|
|
4827
|
+
const events = snapshot?.events ?? [];
|
|
4828
|
+
outcome = this.canonicalOutcomes(events).get(run.runId);
|
|
4829
|
+
run.steering.keptInContext = plugins.isGenerationInModelContext(events, run.runId);
|
|
4691
4830
|
} catch (outcomeError) {
|
|
4692
4831
|
errors.push(this.projectExternalError(run, outcomeError, 'persistence'));
|
|
4693
4832
|
}
|
|
@@ -6238,17 +6377,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6238
6377
|
role: 'user',
|
|
6239
6378
|
status: 'streaming',
|
|
6240
6379
|
createdAt: timestamp,
|
|
6241
|
-
parts:
|
|
6242
|
-
if (part.type === 'text') {
|
|
6243
|
-
return { partId: plugins.crypto.randomUUID(), type: 'text' as const, text: part.text };
|
|
6244
|
-
}
|
|
6245
|
-
return {
|
|
6246
|
-
partId: plugins.crypto.randomUUID(),
|
|
6247
|
-
type: 'attachment' as const,
|
|
6248
|
-
attachmentType: part.type,
|
|
6249
|
-
...describeAttachment(part),
|
|
6250
|
-
};
|
|
6251
|
-
}),
|
|
6380
|
+
parts: this.userMessageParts(prompt),
|
|
6252
6381
|
};
|
|
6253
6382
|
const assistantMessage: IFlexMessage = {
|
|
6254
6383
|
messageId: run.assistantMessageId,
|
|
@@ -6262,6 +6391,151 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6262
6391
|
return { userMessage, assistantMessage };
|
|
6263
6392
|
}
|
|
6264
6393
|
|
|
6394
|
+
private userMessageParts(prompt: INormalizedFlexPrompt): TFlexMessagePart[] {
|
|
6395
|
+
return prompt.parts.map((part) => {
|
|
6396
|
+
if (part.type === 'text') {
|
|
6397
|
+
return { partId: plugins.crypto.randomUUID(), type: 'text' as const, text: part.text };
|
|
6398
|
+
}
|
|
6399
|
+
return {
|
|
6400
|
+
partId: plugins.crypto.randomUUID(),
|
|
6401
|
+
type: 'attachment' as const,
|
|
6402
|
+
attachmentType: part.type,
|
|
6403
|
+
...describeAttachment(part),
|
|
6404
|
+
};
|
|
6405
|
+
});
|
|
6406
|
+
}
|
|
6407
|
+
|
|
6408
|
+
/**
|
|
6409
|
+
* Ends steering intake once the run's generation has settled and fixes what became of its
|
|
6410
|
+
* steers. A run that stays in the model context reports the steers its generation did not
|
|
6411
|
+
* record; a run that does not, because it failed, was stopped before any model output or its
|
|
6412
|
+
* outcome could not be established, reports every steer it accepted, since the model context
|
|
6413
|
+
* of later turns holds none of them. The bytes of the steers not taken in are released.
|
|
6414
|
+
*/
|
|
6415
|
+
private closeRunSteering(steering: IRunSteering): IRunSteeringOutcome {
|
|
6416
|
+
if (steering.outcome) return steering.outcome;
|
|
6417
|
+
steering.input.close();
|
|
6418
|
+
const keptInContext = steering.keptInContext === true;
|
|
6419
|
+
const unappliedSteerIds: string[] = [];
|
|
6420
|
+
let heldBytes = 0;
|
|
6421
|
+
for (const [steerId, entry] of steering.accepted) {
|
|
6422
|
+
const applied = steering.applied.has(steerId);
|
|
6423
|
+
if (!applied) heldBytes += entry.byteSize;
|
|
6424
|
+
if (!keptInContext || !applied) unappliedSteerIds.push(steerId);
|
|
6425
|
+
}
|
|
6426
|
+
steering.stored.outstandingPromptBytes = Math.max(0, steering.stored.outstandingPromptBytes - heldBytes);
|
|
6427
|
+
steering.outcome = Object.freeze({
|
|
6428
|
+
keptInContext,
|
|
6429
|
+
unappliedSteerIds: Object.freeze(unappliedSteerIds),
|
|
6430
|
+
});
|
|
6431
|
+
return steering.outcome;
|
|
6432
|
+
}
|
|
6433
|
+
|
|
6434
|
+
/**
|
|
6435
|
+
* Projects a steer the run's generation took in as a user message of the run. The assistant
|
|
6436
|
+
* message that answered before it is completed and the run answers on in a new one; a steer
|
|
6437
|
+
* taken in before the run answered anything is placed before its still empty assistant message.
|
|
6438
|
+
*/
|
|
6439
|
+
private async applyRunSteer(run: IActiveRun, applied: plugins.IAgentAppliedInput): Promise<void> {
|
|
6440
|
+
const steering = run.steering;
|
|
6441
|
+
const accepted = steering.accepted.get(applied.inputId);
|
|
6442
|
+
if (
|
|
6443
|
+
!accepted
|
|
6444
|
+
|| applied.generationId !== run.runId
|
|
6445
|
+
|| steering.applied.has(applied.inputId)
|
|
6446
|
+
|| steering.outcome !== undefined
|
|
6447
|
+
) {
|
|
6448
|
+
throw new Error(`Steer "${applied.inputId}" was taken in without being accepted by its run.`);
|
|
6449
|
+
}
|
|
6450
|
+
// The generation recorded the steer durably: it is in the model context whenever the run is.
|
|
6451
|
+
steering.applied.add(applied.inputId);
|
|
6452
|
+
steering.stored.outstandingPromptBytes = Math.max(
|
|
6453
|
+
0,
|
|
6454
|
+
steering.stored.outstandingPromptBytes - accepted.byteSize,
|
|
6455
|
+
);
|
|
6456
|
+
const timestamp = new Date().toISOString();
|
|
6457
|
+
const steerMessage: IFlexMessage = {
|
|
6458
|
+
messageId: plugins.crypto.randomUUID(),
|
|
6459
|
+
sessionId: run.sessionId,
|
|
6460
|
+
runId: run.runId,
|
|
6461
|
+
role: 'user',
|
|
6462
|
+
status: 'completed',
|
|
6463
|
+
createdAt: timestamp,
|
|
6464
|
+
completedAt: timestamp,
|
|
6465
|
+
steerId: applied.inputId,
|
|
6466
|
+
parts: this.userMessageParts(accepted.prompt),
|
|
6467
|
+
};
|
|
6468
|
+
const answered = run.callbackParts.length > 0;
|
|
6469
|
+
const answeredAssistantId = run.assistantMessageId;
|
|
6470
|
+
let completedAssistant: IFlexMessage | undefined;
|
|
6471
|
+
const nextAssistant: IFlexMessage | undefined = answered
|
|
6472
|
+
? {
|
|
6473
|
+
messageId: plugins.crypto.randomUUID(),
|
|
6474
|
+
sessionId: run.sessionId,
|
|
6475
|
+
runId: run.runId,
|
|
6476
|
+
role: 'assistant',
|
|
6477
|
+
status: 'streaming',
|
|
6478
|
+
createdAt: timestamp,
|
|
6479
|
+
parts: [],
|
|
6480
|
+
}
|
|
6481
|
+
: undefined;
|
|
6482
|
+
await this.mutateProjection(run.state, run.stored, () => {
|
|
6483
|
+
const index = run.stored.messages.findIndex((message) => message.messageId === answeredAssistantId);
|
|
6484
|
+
if (index < 0) throw new Error('The steered run has no projected assistant message.');
|
|
6485
|
+
if (!nextAssistant) {
|
|
6486
|
+
run.stored.messages.splice(index, 0, cloneSerializable(steerMessage));
|
|
6487
|
+
return;
|
|
6488
|
+
}
|
|
6489
|
+
completedAssistant = {
|
|
6490
|
+
...cloneSerializable(run.stored.messages[index]),
|
|
6491
|
+
status: 'completed',
|
|
6492
|
+
completedAt: timestamp,
|
|
6493
|
+
parts: cloneSerializable(run.callbackParts),
|
|
6494
|
+
...(run.modelResolution ? { model: cloneSerializable(run.modelResolution.identity) } : {}),
|
|
6495
|
+
};
|
|
6496
|
+
run.stored.messages.splice(
|
|
6497
|
+
index,
|
|
6498
|
+
1,
|
|
6499
|
+
cloneSerializable(completedAssistant),
|
|
6500
|
+
cloneSerializable(steerMessage),
|
|
6501
|
+
cloneSerializable(nextAssistant),
|
|
6502
|
+
);
|
|
6503
|
+
});
|
|
6504
|
+
if (completedAssistant && nextAssistant) {
|
|
6505
|
+
run.assistantMessageId = nextAssistant.messageId;
|
|
6506
|
+
run.reservedAssistantMessage = publicSnapshot(nextAssistant);
|
|
6507
|
+
run.callbackParts = [];
|
|
6508
|
+
run.reasoningPartIds.clear();
|
|
6509
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6510
|
+
type: 'message.updated',
|
|
6511
|
+
runId: run.runId,
|
|
6512
|
+
messageId: completedAssistant.messageId,
|
|
6513
|
+
message: publicSnapshot(completedAssistant),
|
|
6514
|
+
});
|
|
6515
|
+
}
|
|
6516
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6517
|
+
type: 'message.created',
|
|
6518
|
+
runId: run.runId,
|
|
6519
|
+
messageId: steerMessage.messageId,
|
|
6520
|
+
message: publicSnapshot(steerMessage),
|
|
6521
|
+
});
|
|
6522
|
+
if (nextAssistant) {
|
|
6523
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6524
|
+
type: 'message.created',
|
|
6525
|
+
runId: run.runId,
|
|
6526
|
+
messageId: nextAssistant.messageId,
|
|
6527
|
+
message: publicSnapshot(nextAssistant),
|
|
6528
|
+
});
|
|
6529
|
+
}
|
|
6530
|
+
this.emitEvent(run.scopeId, run.stored.session, {
|
|
6531
|
+
type: 'prompt.steer.applied',
|
|
6532
|
+
queueId: run.queueId,
|
|
6533
|
+
runId: run.runId,
|
|
6534
|
+
steerId: applied.inputId,
|
|
6535
|
+
messageId: steerMessage.messageId,
|
|
6536
|
+
});
|
|
6537
|
+
}
|
|
6538
|
+
|
|
6265
6539
|
private buildTerminal(
|
|
6266
6540
|
run: IActiveRun,
|
|
6267
6541
|
status: 'completed' | 'failed' | 'cancelled',
|
|
@@ -7428,16 +7702,45 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7428
7702
|
stored.stagedTerminals.push(cloneSerializable(terminal));
|
|
7429
7703
|
}
|
|
7430
7704
|
|
|
7705
|
+
/**
|
|
7706
|
+
* Puts a run's terminal messages in place of their projected versions: its prompt, and its last
|
|
7707
|
+
* assistant message. The steers a run took in, and the assistant messages it completed before
|
|
7708
|
+
* them, stay where they are. A terminal message that is not projected yet is added: the prompt
|
|
7709
|
+
* before the run's other messages, the assistant message after them.
|
|
7710
|
+
*/
|
|
7431
7711
|
private replaceVisibleRun(stored: IStoredSessionState, terminal: IFlexTerminalProjection): void {
|
|
7712
|
+
const replacements = new Map([
|
|
7713
|
+
[terminal.userMessage.messageId, terminal.userMessage],
|
|
7714
|
+
[terminal.assistantMessage.messageId, terminal.assistantMessage],
|
|
7715
|
+
]);
|
|
7716
|
+
const placed = new Set<string>();
|
|
7717
|
+
stored.messages = stored.messages.map((message) => {
|
|
7718
|
+
const replacement = message.runId === terminal.runId
|
|
7719
|
+
? replacements.get(message.messageId)
|
|
7720
|
+
: undefined;
|
|
7721
|
+
if (!replacement) return message;
|
|
7722
|
+
placed.add(replacement.messageId);
|
|
7723
|
+
return cloneSerializable(replacement);
|
|
7724
|
+
});
|
|
7432
7725
|
const firstIndex = stored.messages.findIndex((message) => message.runId === terminal.runId);
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
)
|
|
7726
|
+
if (!placed.has(terminal.userMessage.messageId)) {
|
|
7727
|
+
stored.messages.splice(
|
|
7728
|
+
firstIndex < 0 ? stored.messages.length : firstIndex,
|
|
7729
|
+
0,
|
|
7730
|
+
cloneSerializable(terminal.userMessage),
|
|
7731
|
+
);
|
|
7732
|
+
}
|
|
7733
|
+
if (!placed.has(terminal.assistantMessage.messageId)) {
|
|
7734
|
+
const lastIndex = stored.messages.reduce(
|
|
7735
|
+
(last, message, index) => message.runId === terminal.runId ? index : last,
|
|
7736
|
+
-1,
|
|
7737
|
+
);
|
|
7738
|
+
stored.messages.splice(
|
|
7739
|
+
lastIndex < 0 ? stored.messages.length : lastIndex + 1,
|
|
7740
|
+
0,
|
|
7741
|
+
cloneSerializable(terminal.assistantMessage),
|
|
7742
|
+
);
|
|
7743
|
+
}
|
|
7441
7744
|
}
|
|
7442
7745
|
|
|
7443
7746
|
private emitTerminalProjection(run: IActiveRun, terminal: IFlexTerminalProjection): void {
|
|
@@ -7473,6 +7776,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7473
7776
|
const status: IFlexRunFinishedEvent['status'] = error
|
|
7474
7777
|
? cancelled ? 'cancelled' : 'failed'
|
|
7475
7778
|
: 'completed';
|
|
7779
|
+
const steering = this.closeRunSteering(run.steering);
|
|
7476
7780
|
this.emitEvent(run.scopeId, run.stored.session, {
|
|
7477
7781
|
type: 'run.finished',
|
|
7478
7782
|
runId: run.runId,
|
|
@@ -7480,6 +7784,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7480
7784
|
status,
|
|
7481
7785
|
message: publicSnapshot(message),
|
|
7482
7786
|
...(error === undefined ? {} : { error: errorToInfo(error) }),
|
|
7787
|
+
keptInContext: steering.keptInContext,
|
|
7788
|
+
unappliedSteerIds: [...steering.unappliedSteerIds],
|
|
7483
7789
|
});
|
|
7484
7790
|
}
|
|
7485
7791
|
|
|
@@ -8156,6 +8462,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8156
8462
|
context?: IFlexAgentContextInvocation<TScope>,
|
|
8157
8463
|
): void {
|
|
8158
8464
|
const invocationContext = context ?? this.createRunCompactorContext(run);
|
|
8465
|
+
// A stopped run takes no further steers; the ones it has not taken in are reported unapplied.
|
|
8466
|
+
run.steering.input.close();
|
|
8159
8467
|
if (run.internalFailure === undefined && run.ownerCancellation === undefined) {
|
|
8160
8468
|
run.ownerCancellation = reason;
|
|
8161
8469
|
run.deferredCompactorContext = invocationContext;
|
|
@@ -8165,6 +8473,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8165
8473
|
(run.deferredCompactorContext ?? invocationContext) as IFlexAgentContextInvocation<TScope>,
|
|
8166
8474
|
() => {
|
|
8167
8475
|
this.rejectRunPermissions(run.state, run, reason);
|
|
8476
|
+
// The owner stops the generation: what its model produced stays in the model context.
|
|
8477
|
+
if (run.ownerCancellation === reason && !run.stop.signal.aborted) run.stop.abort(reason);
|
|
8168
8478
|
if (!run.controller.signal.aborted) run.controller.abort(reason);
|
|
8169
8479
|
if (run.phase === 'admitting') return;
|
|
8170
8480
|
if (run.scheduleKey) {
|
|
@@ -8182,6 +8492,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8182
8492
|
context?: IFlexAgentContextInvocation<TScope>,
|
|
8183
8493
|
): void {
|
|
8184
8494
|
const invocationContext = context ?? this.createRunCompactorContext(run);
|
|
8495
|
+
run.steering.input.close();
|
|
8185
8496
|
if (run.internalFailure === undefined) {
|
|
8186
8497
|
run.internalFailure = error ?? new Error('Internal FlexHarness run failure.');
|
|
8187
8498
|
run.deferredCompactorContext = invocationContext;
|
|
@@ -9265,8 +9576,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
9265
9576
|
status: 'completed' | 'failed' | 'cancelled',
|
|
9266
9577
|
): IFlexTerminalProjection {
|
|
9267
9578
|
const timestamp = new Date().toISOString();
|
|
9268
|
-
|
|
9269
|
-
|
|
9579
|
+
// A steered run projects its prompt first and its steers after it; its last assistant
|
|
9580
|
+
// message is the one that was still answering.
|
|
9581
|
+
const sourceUser = stage?.userMessage
|
|
9582
|
+
?? visible.find((message) => message.role === 'user' && message.steerId === undefined);
|
|
9583
|
+
const sourceAssistant = stage?.assistantMessage
|
|
9584
|
+
?? [...visible].reverse().find((message) => message.role === 'assistant');
|
|
9270
9585
|
const userMessage: IFlexMessage = cloneSerializable(sourceUser ?? {
|
|
9271
9586
|
messageId: plugins.crypto.randomUUID(),
|
|
9272
9587
|
sessionId,
|
package/ts/errors.ts
CHANGED
|
@@ -44,6 +44,30 @@ export class FlexHarnessSessionBusyError extends FlexHarnessError {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export type TFlexSteerRejection = 'not-running' | 'duplicate';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The run refused a steer. `not-running`: the prompt has no run that takes input, because it
|
|
51
|
+
* has not started or its run is ending; the steer belongs to the next turn. `duplicate`: the run
|
|
52
|
+
* already accepted a steer with this id.
|
|
53
|
+
*/
|
|
54
|
+
export class FlexHarnessSteerRejectedError extends FlexHarnessError {
|
|
55
|
+
public readonly reason: TFlexSteerRejection;
|
|
56
|
+
public readonly steerId: string;
|
|
57
|
+
|
|
58
|
+
constructor(reason: TFlexSteerRejection, steerId: string) {
|
|
59
|
+
super(
|
|
60
|
+
reason === 'duplicate' ? 'FLEX_STEER_DUPLICATE' : 'FLEX_STEER_NOT_RUNNING',
|
|
61
|
+
reason === 'duplicate'
|
|
62
|
+
? `Steer "${steerId}" was already accepted.`
|
|
63
|
+
: `Steer "${steerId}" has no running turn to join.`,
|
|
64
|
+
);
|
|
65
|
+
this.name = 'FlexHarnessSteerRejectedError';
|
|
66
|
+
this.reason = reason;
|
|
67
|
+
this.steerId = steerId;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
/** An operation would change the history or model context of an imported, read-only session. */
|
|
48
72
|
export class FlexHarnessImportedSessionError extends FlexHarnessError {
|
|
49
73
|
public readonly sessionId: string;
|
package/ts/interfaces.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface IFlexMessage {
|
|
|
141
141
|
error?: string;
|
|
142
142
|
/** The model limit that failed this run, next to its error. */
|
|
143
143
|
limit?: TFlexModelLimit;
|
|
144
|
+
/** On a user message that steered its running run: the caller's steer identity. */
|
|
145
|
+
steerId?: string;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
export interface IFlexSessionActivity {
|
|
@@ -436,17 +438,46 @@ export interface IFlexPromptQueueEntry {
|
|
|
436
438
|
startedAt?: string;
|
|
437
439
|
finishedAt?: string;
|
|
438
440
|
error?: IFlexErrorInfo;
|
|
441
|
+
/**
|
|
442
|
+
* On a finished entry: whether its prompt, the steers its run took in and what its model
|
|
443
|
+
* produced stay in the model context of later turns. True for a completed run and a run
|
|
444
|
+
* stopped after its model produced output; false for a failed run, a run stopped before any
|
|
445
|
+
* model output and a prompt that never ran. Send the prompt again to have it answered.
|
|
446
|
+
*/
|
|
447
|
+
keptInContext?: boolean;
|
|
448
|
+
/**
|
|
449
|
+
* On a finished entry whose run left steers unapplied: the steers it accepted that the model
|
|
450
|
+
* context of later turns does not hold, in the order they arrived. The caller decides what
|
|
451
|
+
* becomes of them.
|
|
452
|
+
*/
|
|
453
|
+
unappliedSteerIds?: string[];
|
|
439
454
|
}
|
|
440
455
|
|
|
441
456
|
export interface IFlexPromptResult {
|
|
442
457
|
runId: string;
|
|
443
458
|
sessionId: string;
|
|
444
459
|
userMessage: IFlexMessage;
|
|
460
|
+
/** The run's last assistant message; a steered run completed earlier ones as it took steers in. */
|
|
445
461
|
assistantMessage: IFlexMessage;
|
|
446
462
|
model: IFlexModelIdentity;
|
|
447
463
|
usage: IFlexUsage;
|
|
448
464
|
finishReason: string;
|
|
449
465
|
steps: number;
|
|
466
|
+
/** Steers the run accepted but never took in, in the order they arrived. */
|
|
467
|
+
unappliedSteerIds: string[];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** A message for the running run of a session, taken in at the run's next step boundary. */
|
|
471
|
+
export interface IFlexSteerInput {
|
|
472
|
+
/** Caller-chosen identity, unique among the steers of the run. */
|
|
473
|
+
steerId: string;
|
|
474
|
+
prompt: TFlexPrompt;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export interface IFlexSteerAdmission {
|
|
478
|
+
steerId: string;
|
|
479
|
+
queueId: string;
|
|
480
|
+
runId: string;
|
|
450
481
|
}
|
|
451
482
|
|
|
452
483
|
export interface IFlexPromptQueueAdmission {
|
|
@@ -1455,6 +1486,36 @@ export interface IFlexRunFinishedEvent extends IFlexEventBase {
|
|
|
1455
1486
|
readonly status: 'completed' | 'failed' | 'cancelled';
|
|
1456
1487
|
readonly message: IFlexMessage;
|
|
1457
1488
|
readonly error?: IFlexErrorInfo;
|
|
1489
|
+
/**
|
|
1490
|
+
* Whether the run's prompt, the steers it took in and what its model produced stay in the
|
|
1491
|
+
* model context of later turns. True for a completed run and a run stopped after its model
|
|
1492
|
+
* produced output; false for a failed run and a run stopped before any model output, whose
|
|
1493
|
+
* prompt the caller sends again to have it answered.
|
|
1494
|
+
*/
|
|
1495
|
+
readonly keptInContext: boolean;
|
|
1496
|
+
/**
|
|
1497
|
+
* Steers the run accepted that the model context of later turns does not hold, in the order
|
|
1498
|
+
* they arrived: the ones it never took in, and, when the run is not kept in context, every
|
|
1499
|
+
* steer it accepted.
|
|
1500
|
+
*/
|
|
1501
|
+
readonly unappliedSteerIds: readonly string[];
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** The running run accepted a steer; it is taken in at the run's next step boundary. */
|
|
1505
|
+
export interface IFlexSteerAcceptedEvent extends IFlexEventBase {
|
|
1506
|
+
readonly type: 'prompt.steer.accepted';
|
|
1507
|
+
readonly queueId: string;
|
|
1508
|
+
readonly runId: string;
|
|
1509
|
+
readonly steerId: string;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** The run took a steer in as the user message `messageId`, before its next model call. */
|
|
1513
|
+
export interface IFlexSteerAppliedEvent extends IFlexEventBase {
|
|
1514
|
+
readonly type: 'prompt.steer.applied';
|
|
1515
|
+
readonly queueId: string;
|
|
1516
|
+
readonly runId: string;
|
|
1517
|
+
readonly steerId: string;
|
|
1518
|
+
readonly messageId: string;
|
|
1458
1519
|
}
|
|
1459
1520
|
|
|
1460
1521
|
/** The run waits `delayMs` before retrying a rate-limited or unavailable model call. */
|
|
@@ -1513,6 +1574,8 @@ export type TFlexHarnessEvent =
|
|
|
1513
1574
|
| IFlexRunRetryingEvent
|
|
1514
1575
|
| TFlexModelUsageEvent
|
|
1515
1576
|
| IFlexPromptQueueEvent
|
|
1577
|
+
| IFlexSteerAcceptedEvent
|
|
1578
|
+
| IFlexSteerAppliedEvent
|
|
1516
1579
|
| IFlexSessionHistoryChangedEvent;
|
|
1517
1580
|
|
|
1518
1581
|
export type TFlexHarnessEventListener = (event: TFlexHarnessEvent) => void;
|
package/ts/plugins.ts
CHANGED
|
@@ -9,12 +9,15 @@ export { AsyncLocalStorage, crypto, fs, path };
|
|
|
9
9
|
// @push.rocks scope
|
|
10
10
|
import {
|
|
11
11
|
AgentEventStoreConflictError,
|
|
12
|
+
AgentPendingInputQueue,
|
|
13
|
+
AgentPendingInputRejectedError,
|
|
12
14
|
AgentSession,
|
|
13
15
|
buildModelMessages,
|
|
14
16
|
ToolJobStoreConflictError,
|
|
15
17
|
createAgentEvent,
|
|
16
18
|
filterModelVisibleAgentEvents,
|
|
17
19
|
getAgentGenerationTransactions,
|
|
20
|
+
isGenerationInModelContext,
|
|
18
21
|
modelMessagesToAgentEvents,
|
|
19
22
|
tool,
|
|
20
23
|
validateAgentEventArchiveV2,
|
|
@@ -24,12 +27,15 @@ import {
|
|
|
24
27
|
|
|
25
28
|
export {
|
|
26
29
|
AgentEventStoreConflictError,
|
|
30
|
+
AgentPendingInputQueue,
|
|
31
|
+
AgentPendingInputRejectedError,
|
|
27
32
|
AgentSession,
|
|
28
33
|
buildModelMessages,
|
|
29
34
|
ToolJobStoreConflictError,
|
|
30
35
|
createAgentEvent,
|
|
31
36
|
filterModelVisibleAgentEvents,
|
|
32
37
|
getAgentGenerationTransactions,
|
|
38
|
+
isGenerationInModelContext,
|
|
33
39
|
modelMessagesToAgentEvents,
|
|
34
40
|
tool,
|
|
35
41
|
validateAgentEventArchiveV2,
|
|
@@ -37,6 +43,7 @@ export {
|
|
|
37
43
|
z,
|
|
38
44
|
};
|
|
39
45
|
export type {
|
|
46
|
+
IAgentAppliedInput,
|
|
40
47
|
IAgentEventArchiveV2,
|
|
41
48
|
IAgentContextCompactionOptions,
|
|
42
49
|
IAgentEventSnapshotV2,
|