@sema-agent/core 5.60.1 → 5.62.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/CHANGELOG.md +125 -0
- package/dist/agents/subagent.d.ts +4 -2
- package/dist/agents/subagent.js +9 -9
- package/dist/brain/open-responses.js +8 -3
- package/dist/brain/openai.js +4 -4
- package/dist/brain/stream-engine.d.ts +13 -2
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/auto-mode-prompt-assets.js +1 -1
- package/dist/core/checkpoint-store.d.ts +36 -4
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -2
- package/dist/core/hooks.d.ts +83 -4
- package/dist/core/hooks.js +3 -3
- package/dist/core/memory-engine/consolidation-driver.d.ts +19 -1
- package/dist/core/memory-engine/consolidation-driver.js +75 -3
- package/dist/core/memory-engine/consolidation.d.ts +52 -5
- package/dist/core/memory-engine/consolidation.js +3 -1
- package/dist/core/memory-engine/distiller.d.ts +89 -1
- package/dist/core/memory-engine/distiller.js +94 -5
- package/dist/core/memory-engine/engine.d.ts +8 -0
- package/dist/core/memory-engine/engine.js +51 -8
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/pricing.d.ts +24 -0
- package/dist/core/pricing.js +18 -0
- package/dist/core/runner/prepare-config-doors.d.ts +34 -0
- package/dist/core/runner/prepare-config-doors.js +55 -0
- package/dist/core/runner/prepare-task.d.ts +52 -10
- package/dist/core/runner/prepare-task.js +77 -42
- package/dist/core/runner/runtask.d.ts +7 -0
- package/dist/core/runner/runtask.js +254 -38
- package/dist/core/runner/turn-attachments.d.ts +137 -5
- package/dist/core/runner/turn-attachments.js +25 -2
- package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
- package/dist/core/task-notification.d.ts +50 -23
- package/dist/core/task-notification.js +20 -4
- package/dist/core/tool-errors.d.ts +2 -1
- package/dist/core/tool-policy.d.ts +27 -0
- package/dist/core/types.d.ts +214 -31
- package/dist/core/untrusted-text.d.ts +5 -4
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/usage-window-store.d.ts +109 -8
- package/dist/core/usage-window-store.js +79 -12
- package/dist/engine/harness/agent-harness.d.ts +58 -2
- package/dist/engine/harness/agent-harness.js +115 -5
- package/dist/engine/loop/agent-loop.js +153 -15
- package/dist/engine/loop/types.d.ts +32 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +9 -4
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow.d.ts +2 -2
- package/dist/prompt-assembly/event-registry.js +2 -0
- package/dist/server/http.d.ts +1 -1
- package/dist/stores/file/usage-window-store.d.ts +1 -1
- package/dist/stores/file/usage-window-store.js +27 -6
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/monitor.d.ts +3 -3
- package/dist/tools/monitor.js +1 -1
- package/dist/tools/scheduler-tools.js +9 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
|
@@ -24,6 +24,9 @@ export function resolveUsageWindows(windows) {
|
|
|
24
24
|
if (typeof w.maxTokens !== "number" || !Number.isFinite(w.maxTokens) || w.maxTokens < 0) {
|
|
25
25
|
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].maxTokens must be a finite, non-negative number (got ${String(w.maxTokens)})`);
|
|
26
26
|
}
|
|
27
|
+
if (w.maxCostUsd !== undefined && (typeof w.maxCostUsd !== "number" || !Number.isFinite(w.maxCostUsd) || w.maxCostUsd < 0)) {
|
|
28
|
+
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].maxCostUsd must be a finite, non-negative number of USD when present (got ${String(w.maxCostUsd)})`);
|
|
29
|
+
}
|
|
27
30
|
if (w.anchor !== "first-use" && w.anchor !== "rolling") {
|
|
28
31
|
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].anchor must be "first-use" or "rolling" (got ${String(w.anchor)})`);
|
|
29
32
|
}
|
|
@@ -40,11 +43,34 @@ function rollingHorizonMs(windows) {
|
|
|
40
43
|
}
|
|
41
44
|
return widest;
|
|
42
45
|
}
|
|
43
|
-
export function
|
|
46
|
+
export function windowsGovernCost(windows) {
|
|
47
|
+
return windows.some((w) => w.maxCostUsd !== undefined);
|
|
48
|
+
}
|
|
49
|
+
function maxCostMicroUsdOf(w) {
|
|
50
|
+
return w.maxCostUsd === undefined ? undefined : Math.ceil(Number((w.maxCostUsd * 1e6).toPrecision(12)));
|
|
51
|
+
}
|
|
52
|
+
export function chargeUsageRecord(record, tokens, at, windows, costMicroUsd) {
|
|
44
53
|
const horizon = rollingHorizonMs(windows);
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
54
|
+
const governsCost = windowsGovernCost(windows);
|
|
55
|
+
if (governsCost && costMicroUsd === undefined) {
|
|
56
|
+
throw usageConfigError("usage_window.store_cost_unanswered", "a usage-window CHARGE for a deployment that governs maxCostUsd arrived with no cost figure at all. Core supplies one (a number, or an explicit null for spend nothing could price) on every such charge, so an absent one means a store in the chain does not carry the cost argument — folding it to 0 would leave the money ceiling open forever with nothing able to notice. Forward `costMicroUsd` from `UsageWindowStore.charge` into `chargeUsageRecord`.");
|
|
57
|
+
}
|
|
58
|
+
const cost = governsCost && costMicroUsd !== null ? costMicroUsd : undefined;
|
|
59
|
+
const unknown = governsCost && costMicroUsd === null;
|
|
60
|
+
const moves = tokens > 0 || (cost !== undefined && cost > 0) || unknown;
|
|
61
|
+
const keptSlots = horizon === undefined
|
|
62
|
+
? []
|
|
63
|
+
: record.slots
|
|
64
|
+
.filter((s) => s.at > at - horizon)
|
|
65
|
+
.map((s) => ({
|
|
66
|
+
at: s.at,
|
|
67
|
+
tokens: s.tokens,
|
|
68
|
+
...(s.costMicroUsd === undefined ? {} : { costMicroUsd: s.costMicroUsd }),
|
|
69
|
+
...(s.costUnknown === true ? { costUnknown: true } : {}),
|
|
70
|
+
}));
|
|
71
|
+
if (horizon !== undefined && moves) {
|
|
72
|
+
keptSlots.push({ at, tokens, ...(cost === undefined ? {} : { costMicroUsd: cost }), ...(unknown ? { costUnknown: true } : {}) });
|
|
73
|
+
}
|
|
48
74
|
const buckets = [];
|
|
49
75
|
for (const w of windows) {
|
|
50
76
|
if (w.anchor !== "first-use")
|
|
@@ -54,11 +80,21 @@ export function chargeUsageRecord(record, tokens, at, windows) {
|
|
|
54
80
|
const prior = record.buckets.find((b) => b.windowMs === w.windowMs);
|
|
55
81
|
const lapsed = prior === undefined || at >= prior.openedAt + w.windowMs;
|
|
56
82
|
if (lapsed) {
|
|
57
|
-
if (
|
|
58
|
-
buckets.push({ windowMs: w.windowMs, openedAt: at, tokens });
|
|
83
|
+
if (moves) {
|
|
84
|
+
buckets.push({ windowMs: w.windowMs, openedAt: at, tokens, ...(cost === undefined ? {} : { costMicroUsd: cost }), ...(unknown ? { costUnknown: true } : {}) });
|
|
85
|
+
}
|
|
59
86
|
continue;
|
|
60
87
|
}
|
|
61
|
-
|
|
88
|
+
const priorCost = prior.costMicroUsd;
|
|
89
|
+
const nextCost = cost === undefined ? priorCost : (priorCost ?? 0) + cost;
|
|
90
|
+
const nextUnknown = unknown || prior.costUnknown === true;
|
|
91
|
+
buckets.push({
|
|
92
|
+
windowMs: w.windowMs,
|
|
93
|
+
openedAt: prior.openedAt,
|
|
94
|
+
tokens: prior.tokens + tokens,
|
|
95
|
+
...(nextCost === undefined ? {} : { costMicroUsd: nextCost }),
|
|
96
|
+
...(nextUnknown ? { costUnknown: true } : {}),
|
|
97
|
+
});
|
|
62
98
|
}
|
|
63
99
|
return { slots: keptSlots, buckets };
|
|
64
100
|
}
|
|
@@ -66,6 +102,8 @@ export function readUsageRecord(record, windows, now) {
|
|
|
66
102
|
const readings = [];
|
|
67
103
|
for (const w of windows) {
|
|
68
104
|
let tokens = 0;
|
|
105
|
+
let costMicroUsd = 0;
|
|
106
|
+
let costUnknown = false;
|
|
69
107
|
let freesAt;
|
|
70
108
|
if (w.anchor === "rolling") {
|
|
71
109
|
const floor = now - w.windowMs;
|
|
@@ -74,6 +112,9 @@ export function readUsageRecord(record, windows, now) {
|
|
|
74
112
|
if (s.at <= floor)
|
|
75
113
|
continue;
|
|
76
114
|
tokens += s.tokens;
|
|
115
|
+
costMicroUsd += s.costMicroUsd ?? 0;
|
|
116
|
+
if (s.costUnknown === true)
|
|
117
|
+
costUnknown = true;
|
|
77
118
|
if (oldest === undefined || s.at < oldest)
|
|
78
119
|
oldest = s.at;
|
|
79
120
|
}
|
|
@@ -84,18 +125,44 @@ export function readUsageRecord(record, windows, now) {
|
|
|
84
125
|
const row = record.buckets.find((b) => b.windowMs === w.windowMs);
|
|
85
126
|
if (row !== undefined && now < row.openedAt + w.windowMs) {
|
|
86
127
|
tokens = row.tokens;
|
|
128
|
+
costMicroUsd = row.costMicroUsd ?? 0;
|
|
129
|
+
if (row.costUnknown === true)
|
|
130
|
+
costUnknown = true;
|
|
87
131
|
freesAt = row.openedAt + w.windowMs;
|
|
88
132
|
}
|
|
89
133
|
}
|
|
90
|
-
const
|
|
134
|
+
const maxCostMicroUsd = maxCostMicroUsdOf(w);
|
|
135
|
+
const exhausted = tokens >= w.maxTokens || (maxCostMicroUsd !== undefined && costMicroUsd >= maxCostMicroUsd);
|
|
91
136
|
const retryAfterMs = exhausted ? Math.max(1, freesAt === undefined ? w.windowMs : freesAt - now) : 0;
|
|
92
|
-
readings.push({
|
|
137
|
+
readings.push({
|
|
138
|
+
window: w,
|
|
139
|
+
tokens,
|
|
140
|
+
...(maxCostMicroUsd !== undefined ? { costMicroUsd } : {}),
|
|
141
|
+
...(maxCostMicroUsd !== undefined && costUnknown ? { costUnknown: true } : {}),
|
|
142
|
+
exhausted,
|
|
143
|
+
retryAfterMs,
|
|
144
|
+
});
|
|
93
145
|
}
|
|
94
146
|
return readings;
|
|
95
147
|
}
|
|
96
|
-
export function usageRetryAfterMs(readings) {
|
|
148
|
+
export function usageRetryAfterMs(readings, windows) {
|
|
149
|
+
if (windows !== undefined) {
|
|
150
|
+
if (readings.length !== windows.length) {
|
|
151
|
+
throw usageConfigError("usage_window.store_cost_unanswered", `the usage-window ledger answered ${String(readings.length)} reading(s) for ${String(windows.length)} declared window(s) — the read contract is one reading per window, in the order given. A window with no reading was never evaluated: refused rather than treated as open.`);
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < windows.length; i++) {
|
|
154
|
+
const w = windows[i];
|
|
155
|
+
const echoed = readings[i].window;
|
|
156
|
+
if (echoed === undefined || echoed.windowMs !== w.windowMs || echoed.anchor !== w.anchor || echoed.maxTokens !== w.maxTokens || echoed.maxCostUsd !== w.maxCostUsd) {
|
|
157
|
+
throw usageConfigError("usage_window.store_cost_unanswered", `the usage-window ledger's reading #${String(i)} does not echo declared window #${String(i)} verbatim (declared: windowMs ${String(w.windowMs)}, ${String(w.anchor)}, maxTokens ${String(w.maxTokens)}, maxCostUsd ${String(w.maxCostUsd)}) — the read contract requires the window back verbatim, in order, so its ceilings are provably the ones evaluated. Refused rather than re-attributed.`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
97
161
|
let worst;
|
|
98
162
|
for (const r of readings) {
|
|
163
|
+
if (r.window?.maxCostUsd !== undefined && r.costMicroUsd === undefined) {
|
|
164
|
+
throw usageConfigError("usage_window.store_cost_unanswered", `the usage-window ledger answered a window that declares maxCostUsd ${String(r.window.maxCostUsd)} (windowMs ${String(r.window.windowMs)}, ${String(r.window.anchor)}) with NO costMicroUsd — the money ceiling was never evaluated. Refused rather than treated as open: the store must persist the cost charged to it and report what the window holds.`);
|
|
165
|
+
}
|
|
99
166
|
if (!r.exhausted)
|
|
100
167
|
continue;
|
|
101
168
|
if (worst === undefined || r.retryAfterMs > worst)
|
|
@@ -105,9 +172,9 @@ export function usageRetryAfterMs(readings) {
|
|
|
105
172
|
}
|
|
106
173
|
export class InMemoryUsageWindowStore {
|
|
107
174
|
records = new Map();
|
|
108
|
-
async charge(key, tokens, at, windows) {
|
|
175
|
+
async charge(key, tokens, at, windows, costMicroUsd) {
|
|
109
176
|
const prior = this.records.get(key) ?? EMPTY_USAGE_WINDOW_RECORD;
|
|
110
|
-
this.records.set(key, chargeUsageRecord(prior, tokens, at, windows));
|
|
177
|
+
this.records.set(key, chargeUsageRecord(prior, tokens, at, windows, costMicroUsd));
|
|
111
178
|
}
|
|
112
179
|
async read(key, windows, now) {
|
|
113
180
|
return readUsageRecord(this.records.get(key) ?? EMPTY_USAGE_WINDOW_RECORD, windows, now);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActorAssertion, AssistantMessage, ImageContent, Model } from "../llm/index.js";
|
|
1
|
+
import type { ActorAssertion, AssistantMessage, ImageContent, Model, UserMessage } from "../llm/index.js";
|
|
2
2
|
import type { AgentMessage, AgentTool, LoopMalformedToolUseRecovery, LoopThinkingOnlyRecovery, LoopTruncatedOutputRecovery, QueueMode, ThinkingLevel } from "../loop/types.js";
|
|
3
3
|
import { type EngineSegment } from "../../core/untrusted-text.js";
|
|
4
4
|
import type { AbortResult, AgentHarnessEvent, AgentHarnessEventResultMap, AgentHarnessOptions, AgentHarnessOwnEvent, AgentHarnessResources, AgentHarnessStreamOptions, ExecutionEnv, PromptTemplate, Skill } from "./types.js";
|
|
@@ -47,6 +47,22 @@ export interface UserMessageProvenance {
|
|
|
47
47
|
* the parked-steer queue needs (pre-framing text + trust + inputId), which only the minting seam
|
|
48
48
|
* knows — hence opaque. */
|
|
49
49
|
parkRecord?: unknown;
|
|
50
|
+
/** design/373 — an IMMEDIATE-class ("now") frame: the steer-lane enqueue inserts it ahead of the
|
|
51
|
+
* first non-immediate frame (FIFO among immediates — the dequeue-priority form), the loop's
|
|
52
|
+
* pre-request re-check sweeps it into the imminent turn, and {@link AgentHarness.interruptTurn}
|
|
53
|
+
* may force the boundary for it. Class metadata only (a sidecar, judged at the mint) — it never
|
|
54
|
+
* rides the message or the wire, and it grants NOTHING by itself: the interrupt is a separate,
|
|
55
|
+
* caller-provenance verb. */
|
|
56
|
+
immediate?: true;
|
|
57
|
+
/** design/373 (#445 saturation prerequisite) — terminal-preference at the engine-note backlog
|
|
58
|
+
* cap: when the target queue's engine-note count is AT the cap, a frame carrying this mark
|
|
59
|
+
* displaces the OLDEST non-preferred engine-note frame instead of being refused — the displaced
|
|
60
|
+
* payload is handed to the undrained sink (the runner's lossless per-session park lane), so a
|
|
61
|
+
* watcher's event storm can delay its own batches but never crowd a terminal completion out of
|
|
62
|
+
* the run that is waiting on it (the pend store's terminal-preferred eviction, mirrored at the
|
|
63
|
+
* queue mouth). Only meaningful WITH `enginePayload`; a preferred frame among only preferred
|
|
64
|
+
* frames still gets the cap refusal (nothing displaceable). */
|
|
65
|
+
capPreferred?: true;
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
52
68
|
* Harness-level recovery wiring (design/118 ④b/⑤). The loop's prompt-too-long seam wants a
|
|
@@ -190,6 +206,9 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
190
206
|
private loopRecovery?;
|
|
191
207
|
/** roadmap #5 (CC Stop hook): the runner-wired stop gate — see {@link setStopGate}. */
|
|
192
208
|
private stopGate?;
|
|
209
|
+
/** design/373 — the loop-published turn-scoped AbortController (live exactly while a main turn's
|
|
210
|
+
* work phase is in flight; `undefined` between turns). Read by {@link interruptTurn} only. */
|
|
211
|
+
private turnInterruptSeat?;
|
|
193
212
|
private handlers;
|
|
194
213
|
constructor(options: AgentHarnessOptions<TSkill, TPromptTemplate, TTool>);
|
|
195
214
|
private getHandlers;
|
|
@@ -264,12 +283,49 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
264
283
|
* engine lanes each check the halt fact before injecting, so this arm exists for the lane that
|
|
265
284
|
* does not — including the one not written yet. */
|
|
266
285
|
private refuseHeldEngineInjection;
|
|
286
|
+
/** design/373 — returns the minted queue frame (or `undefined` on the empty-injection no-op), so
|
|
287
|
+
* a caller-provenance immediate steer can hand the SAME frame to {@link interruptTurn} (the
|
|
288
|
+
* consumption guard is identity-keyed). Additive: every existing caller awaited `void`. */
|
|
267
289
|
steer(text: string, options?: {
|
|
268
290
|
images?: ImageContent[];
|
|
269
|
-
} & UserMessageProvenance): Promise<
|
|
291
|
+
} & UserMessageProvenance): Promise<UserMessage | undefined>;
|
|
270
292
|
followUp(text: string, options?: {
|
|
271
293
|
images?: ImageContent[];
|
|
272
294
|
} & UserMessageProvenance): Promise<void>;
|
|
295
|
+
/**
|
|
296
|
+
* design/373 §3.4 — force the running turn's boundary FOR a queued immediate-class frame:
|
|
297
|
+
* abort the CURRENT main turn (provider stream + tool batch — never the run, never boundary
|
|
298
|
+
* housekeeping), whose loop-side settlement reconciles the cut and continues the run with the
|
|
299
|
+
* frame at the queue head. Best-effort accelerator by contract: `false` means no interruption
|
|
300
|
+
* happened AND none was needed — the frame is already consumed or already rides the imminent
|
|
301
|
+
* boundary — so delivery semantics are exactly the non-interrupting ones either way.
|
|
302
|
+
*
|
|
303
|
+
* Guards, in order:
|
|
304
|
+
* - an unwinding run (`abort()` latched) owns its ending — never contest it;
|
|
305
|
+
* - CONSUMPTION guard (r4/R3-1): the frame must still sit in the steer queue. Drained/consumed ⇒
|
|
306
|
+
* it is already part of a turn's input — aborting THAT turn would cut the very delivery this
|
|
307
|
+
* verb exists to speed up. Same-tick check-then-abort (single-threaded: no window);
|
|
308
|
+
* - seat liveness: no published turn (idle tail / between turns / boundary housekeeping) ⇒ no-op,
|
|
309
|
+
* the frame rides the next boundary; an already-fired seat ⇒ idempotent no-op (one boundary per
|
|
310
|
+
* turn — later immediates join the same boundary batch).
|
|
311
|
+
*/
|
|
312
|
+
interruptTurn(frame: AgentMessage): boolean;
|
|
313
|
+
/**
|
|
314
|
+
* design/373 §3.3 (the final-commit-point double check) — how many queued injection frames a
|
|
315
|
+
* boundary drain could deliver RIGHT NOW: both lanes, minus frames the current state refuses to
|
|
316
|
+
* drain (an unwinding run drains nothing — those frames belong to the terminal account; a live
|
|
317
|
+
* human-halt hold skips engine-authored frames). Synchronous and side-effect-free by contract:
|
|
318
|
+
* the loop reads it between its last drain and the terminal commit, with zero awaits.
|
|
319
|
+
*/
|
|
320
|
+
pendingInjectionCount(): number;
|
|
321
|
+
/** design/373 §3.4-3 — the pre-request re-check's drain half (see the loop-config seam): sweep
|
|
322
|
+
* queued immediate-class frames into the imminent turn's batch, merged at the class position
|
|
323
|
+
* (after immediates already pending, ahead of everything else — FIFO within the class holds
|
|
324
|
+
* across the drain/re-check split). Same booking discipline as {@link drainQueuedMessages}
|
|
325
|
+
* (in-flight account, queue_update broadcast, consumption sink, throw-rollback); the hold skips
|
|
326
|
+
* engine-authored immediates exactly as the boundary drain would (a theoretical lane today —
|
|
327
|
+
* no engine mint issues immediates — held by construction rather than by remembering). */
|
|
328
|
+
private absorbImmediateSteering;
|
|
273
329
|
nextTurn(text: string, options?: {
|
|
274
330
|
images?: ImageContent[];
|
|
275
331
|
} & UserMessageProvenance): Promise<void>;
|
|
@@ -38,6 +38,8 @@ const engineNotePayloads = new WeakMap();
|
|
|
38
38
|
const engineAuthoredInjections = new WeakSet();
|
|
39
39
|
const userInputParkRecords = new WeakMap();
|
|
40
40
|
const drainedFromLane = new WeakMap();
|
|
41
|
+
const immediateClassInjections = new WeakSet();
|
|
42
|
+
const capPreferredInjections = new WeakSet();
|
|
41
43
|
function isEngineAuthoredInjection(options) {
|
|
42
44
|
if (options === undefined)
|
|
43
45
|
return false;
|
|
@@ -257,6 +259,7 @@ export class AgentHarness {
|
|
|
257
259
|
nextTurnQueue = [];
|
|
258
260
|
loopRecovery;
|
|
259
261
|
stopGate;
|
|
262
|
+
turnInterruptSeat;
|
|
260
263
|
handlers = new Map();
|
|
261
264
|
constructor(options) {
|
|
262
265
|
this.env = options.env;
|
|
@@ -598,6 +601,11 @@ export class AgentHarness {
|
|
|
598
601
|
};
|
|
599
602
|
},
|
|
600
603
|
getSteeringMessages: async () => this.drainQueuedMessages(this.steerQueue, this.steeringQueueMode),
|
|
604
|
+
publishTurnInterruptSeat: (seat) => {
|
|
605
|
+
this.turnInterruptSeat = seat;
|
|
606
|
+
},
|
|
607
|
+
recheckImmediateInjections: async (pending) => this.absorbImmediateSteering(pending),
|
|
608
|
+
pendingInjectionCount: () => this.pendingInjectionCount(),
|
|
601
609
|
getFollowUpMessages: async () => {
|
|
602
610
|
const queued = await this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode);
|
|
603
611
|
if (queued.length > 0)
|
|
@@ -841,9 +849,16 @@ export class AgentHarness {
|
|
|
841
849
|
}
|
|
842
850
|
async enqueueInjection(queue, text, options) {
|
|
843
851
|
if (AgentHarness.emptyInjection(text, options))
|
|
844
|
-
return;
|
|
852
|
+
return undefined;
|
|
853
|
+
let displaced;
|
|
845
854
|
if (options?.enginePayload !== undefined && queue.filter((q) => engineNotePayloads.has(q)).length >= ENGINE_NOTE_STEER_BACKLOG_CAP) {
|
|
846
|
-
|
|
855
|
+
const victimIdx = options.capPreferred === true
|
|
856
|
+
? queue.findIndex((q) => engineNotePayloads.has(q) && !capPreferredInjections.has(q))
|
|
857
|
+
: -1;
|
|
858
|
+
if (victimIdx === -1) {
|
|
859
|
+
throw new AgentHarnessError("invalid_state", `engine-note backlog at cap (${ENGINE_NOTE_STEER_BACKLOG_CAP}) — park the payload for the session's next run`);
|
|
860
|
+
}
|
|
861
|
+
displaced = { frame: queue.splice(victimIdx, 1)[0], at: victimIdx };
|
|
847
862
|
}
|
|
848
863
|
const m = createUserMessage(text, options?.images, options);
|
|
849
864
|
if (options?.enginePayload !== undefined)
|
|
@@ -852,8 +867,43 @@ export class AgentHarness {
|
|
|
852
867
|
engineAuthoredInjections.add(m);
|
|
853
868
|
if (options?.parkRecord !== undefined)
|
|
854
869
|
userInputParkRecords.set(m, options.parkRecord);
|
|
855
|
-
|
|
856
|
-
|
|
870
|
+
if (options?.immediate === true)
|
|
871
|
+
immediateClassInjections.add(m);
|
|
872
|
+
if (options?.capPreferred === true && options?.enginePayload !== undefined)
|
|
873
|
+
capPreferredInjections.add(m);
|
|
874
|
+
if (options?.immediate === true && queue === this.steerQueue) {
|
|
875
|
+
let at = 0;
|
|
876
|
+
while (at < queue.length && immediateClassInjections.has(queue[at]))
|
|
877
|
+
at++;
|
|
878
|
+
queue.splice(at, 0, m);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
queue.push(m);
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
await this.emitQueueUpdate();
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
const at = queue.indexOf(m);
|
|
888
|
+
if (at !== -1) {
|
|
889
|
+
queue.splice(at, 1);
|
|
890
|
+
if (displaced !== undefined)
|
|
891
|
+
queue.splice(Math.min(displaced.at, queue.length), 0, displaced.frame);
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (displaced !== undefined) {
|
|
896
|
+
const victimPayload = engineNotePayloads.get(displaced.frame);
|
|
897
|
+
engineNotePayloads.delete(displaced.frame);
|
|
898
|
+
if (victimPayload !== undefined && this.onUndrainedEngineNotes) {
|
|
899
|
+
try {
|
|
900
|
+
this.onUndrainedEngineNotes([victimPayload]);
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return m;
|
|
857
907
|
}
|
|
858
908
|
refuseHeldEngineInjection(options) {
|
|
859
909
|
if (isEngineAuthoredInjection(options) && this.engineInjectionsHeld?.() === true) {
|
|
@@ -865,7 +915,7 @@ export class AgentHarness {
|
|
|
865
915
|
throw new AgentHarnessError("invalid_state", "Cannot steer while idle");
|
|
866
916
|
}
|
|
867
917
|
this.refuseHeldEngineInjection(options);
|
|
868
|
-
await this.enqueueInjection(this.steerQueue, text, options);
|
|
918
|
+
return await this.enqueueInjection(this.steerQueue, text, options);
|
|
869
919
|
}
|
|
870
920
|
async followUp(text, options) {
|
|
871
921
|
if (this.phase === "idle") {
|
|
@@ -874,6 +924,66 @@ export class AgentHarness {
|
|
|
874
924
|
this.refuseHeldEngineInjection(options);
|
|
875
925
|
await this.enqueueInjection(this.followUpQueue, text, options);
|
|
876
926
|
}
|
|
927
|
+
interruptTurn(frame) {
|
|
928
|
+
if (this.aborting)
|
|
929
|
+
return false;
|
|
930
|
+
if (!this.steerQueue.includes(frame))
|
|
931
|
+
return false;
|
|
932
|
+
const seat = this.turnInterruptSeat;
|
|
933
|
+
if (seat === undefined || seat.signal.aborted)
|
|
934
|
+
return false;
|
|
935
|
+
seat.abort();
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
pendingInjectionCount() {
|
|
939
|
+
if (this.aborting)
|
|
940
|
+
return 0;
|
|
941
|
+
const held = this.engineInjectionsHeld?.() === true;
|
|
942
|
+
const drainable = (q) => held ? q.filter((m) => !engineAuthoredInjections.has(m)).length : q.length;
|
|
943
|
+
return drainable(this.steerQueue) + drainable(this.followUpQueue);
|
|
944
|
+
}
|
|
945
|
+
async absorbImmediateSteering(pending) {
|
|
946
|
+
if (this.aborting)
|
|
947
|
+
return pending;
|
|
948
|
+
const held = this.engineInjectionsHeld?.() === true;
|
|
949
|
+
const taken = [];
|
|
950
|
+
for (let i = 0; i < this.steerQueue.length; i++) {
|
|
951
|
+
const m = this.steerQueue[i];
|
|
952
|
+
if (!immediateClassInjections.has(m))
|
|
953
|
+
break;
|
|
954
|
+
if (held && engineAuthoredInjections.has(m))
|
|
955
|
+
continue;
|
|
956
|
+
taken.push(...this.steerQueue.splice(i, 1));
|
|
957
|
+
i--;
|
|
958
|
+
}
|
|
959
|
+
if (taken.length === 0)
|
|
960
|
+
return pending;
|
|
961
|
+
for (const m of taken)
|
|
962
|
+
drainedFromLane.set(m, "steer");
|
|
963
|
+
this.drainedPendingInjection.push(...taken);
|
|
964
|
+
try {
|
|
965
|
+
await this.emitQueueUpdate();
|
|
966
|
+
for (const m of taken) {
|
|
967
|
+
const payload = engineNotePayloads.get(m);
|
|
968
|
+
if (payload !== undefined) {
|
|
969
|
+
try {
|
|
970
|
+
this.onEngineNoteConsumed?.(payload);
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
this.steerQueue.unshift(...taken);
|
|
979
|
+
this.unbookInFlight(taken);
|
|
980
|
+
throw normalizeHookError(error);
|
|
981
|
+
}
|
|
982
|
+
let at = 0;
|
|
983
|
+
while (at < pending.length && immediateClassInjections.has(pending[at]))
|
|
984
|
+
at++;
|
|
985
|
+
return [...pending.slice(0, at), ...taken, ...pending.slice(at)];
|
|
986
|
+
}
|
|
877
987
|
async nextTurn(text, options) {
|
|
878
988
|
await this.enqueueInjection(this.nextTurnQueue, text, options);
|
|
879
989
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { findToolByName, validateToolArguments } from "../llm/index.js";
|
|
2
2
|
import { truncateError } from "../../core/tool-errors.js";
|
|
3
|
+
import { INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER, INTERRUPTED_BY_USER_MARKER, } from "../../core/session-reconcile.js";
|
|
3
4
|
import { resolveAgentCoreStreamFn } from "./runtime-deps.js";
|
|
4
5
|
function appendTextDeltaToAssistantMessage(message, contentIndex, delta) {
|
|
5
6
|
const content = [...message.content];
|
|
@@ -158,6 +159,13 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
158
159
|
if (!state.hasMoreToolCalls && state.pendingMessages.length === 0) {
|
|
159
160
|
const followUpMessages = (await state.config.getFollowUpMessages?.()) || [];
|
|
160
161
|
if (followUpMessages.length === 0) {
|
|
162
|
+
if ((state.config.pendingInjectionCount?.() ?? 0) > 0) {
|
|
163
|
+
state.pendingMessages = (await state.config.getSteeringMessages?.()) || [];
|
|
164
|
+
if (state.pendingMessages.length > 0) {
|
|
165
|
+
trace?.({ kind: "continue", reason: "steer_injected" });
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
161
169
|
trace?.({ kind: "terminal", reason: "completed" });
|
|
162
170
|
break;
|
|
163
171
|
}
|
|
@@ -193,6 +201,38 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
193
201
|
else {
|
|
194
202
|
state.firstTurn = false;
|
|
195
203
|
}
|
|
204
|
+
const turnController = new AbortController();
|
|
205
|
+
const onRunAbort = () => turnController.abort();
|
|
206
|
+
if (signal !== undefined) {
|
|
207
|
+
if (signal.aborted)
|
|
208
|
+
turnController.abort();
|
|
209
|
+
else
|
|
210
|
+
signal.addEventListener("abort", onRunAbort, { once: true });
|
|
211
|
+
}
|
|
212
|
+
let seatLive = false;
|
|
213
|
+
const publishSeat = () => {
|
|
214
|
+
seatLive = true;
|
|
215
|
+
state.config.publishTurnInterruptSeat?.(turnController);
|
|
216
|
+
};
|
|
217
|
+
const retractSeat = () => {
|
|
218
|
+
if (!seatLive)
|
|
219
|
+
return;
|
|
220
|
+
seatLive = false;
|
|
221
|
+
state.config.publishTurnInterruptSeat?.(undefined);
|
|
222
|
+
};
|
|
223
|
+
try {
|
|
224
|
+
return await runTurnPhases(state, signal, turnController, publishSeat, retractSeat, emit, streamFn, runtime, trace);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
retractSeat();
|
|
228
|
+
signal?.removeEventListener("abort", onRunAbort);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function runTurnPhases(state, signal, turnController, publishSeat, retractSeat, emit, streamFn, runtime, trace) {
|
|
232
|
+
publishSeat();
|
|
233
|
+
if (state.config.recheckImmediateInjections) {
|
|
234
|
+
state.pendingMessages = await state.config.recheckImmediateInjections(state.pendingMessages);
|
|
235
|
+
}
|
|
196
236
|
if (state.pendingMessages.length > 0) {
|
|
197
237
|
for (const message of state.pendingMessages) {
|
|
198
238
|
await emit({ type: "message_start", message });
|
|
@@ -201,7 +241,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
201
241
|
state.newMessages.push(message);
|
|
202
242
|
}
|
|
203
243
|
}
|
|
204
|
-
let executor = new StreamToolExecutor(state.context, state.config, signal, emit);
|
|
244
|
+
let executor = new StreamToolExecutor(state.context, state.config, turnController.signal, emit);
|
|
205
245
|
const staticReasoningCutDowngrade = state.config.recovery?.truncatedOutput !== undefined &&
|
|
206
246
|
state.staticReasoningCutRecoveries < MAX_STATIC_REASONING_CUT_RECOVERIES;
|
|
207
247
|
const ptl = state.config.recovery?.promptTooLong;
|
|
@@ -210,7 +250,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
210
250
|
let withhold = withholdEnabled ? createPtlWithholdBuffer(emit, ptlDetect) : undefined;
|
|
211
251
|
let message;
|
|
212
252
|
try {
|
|
213
|
-
message = await streamAssistantResponse(state.context, state.config, signal, withhold?.sink ?? emit, streamFn, runtime, executor, staticReasoningCutDowngrade);
|
|
253
|
+
message = await streamAssistantResponse(state.context, state.config, turnController.signal, withhold?.sink ?? emit, streamFn, runtime, executor, staticReasoningCutDowngrade);
|
|
214
254
|
}
|
|
215
255
|
finally {
|
|
216
256
|
await executor.settle();
|
|
@@ -246,9 +286,9 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
246
286
|
withhold = withholdEnabled ? createPtlWithholdBuffer(emit, ptlDetect) : undefined;
|
|
247
287
|
state.context.messages = replaced;
|
|
248
288
|
trace?.({ kind: "continue", reason: "reactive_compact_retry" });
|
|
249
|
-
executor = new StreamToolExecutor(state.context, state.config, signal, emit);
|
|
289
|
+
executor = new StreamToolExecutor(state.context, state.config, turnController.signal, emit);
|
|
250
290
|
try {
|
|
251
|
-
message = await streamAssistantResponse(state.context, state.config, signal, withhold?.sink ?? emit, streamFn, runtime, executor, staticReasoningCutDowngrade);
|
|
291
|
+
message = await streamAssistantResponse(state.context, state.config, turnController.signal, withhold?.sink ?? emit, streamFn, runtime, executor, staticReasoningCutDowngrade);
|
|
252
292
|
}
|
|
253
293
|
finally {
|
|
254
294
|
await executor.settle();
|
|
@@ -261,6 +301,10 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
261
301
|
state.newMessages.push(message);
|
|
262
302
|
if (message.staticReasoningCut === true)
|
|
263
303
|
state.staticReasoningCutRecoveries++;
|
|
304
|
+
if (message.stopReason === "aborted" && turnController.signal.aborted && !signal?.aborted) {
|
|
305
|
+
retractSeat();
|
|
306
|
+
return await settleInterruptedTurn(state, message, executor, [], signal, emit);
|
|
307
|
+
}
|
|
264
308
|
{
|
|
265
309
|
const degen = state.config.recovery?.degenerateOutput;
|
|
266
310
|
if (degen &&
|
|
@@ -270,6 +314,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
270
314
|
executor.admittedCount === 0 &&
|
|
271
315
|
message.content.every((b) => b.type !== "toolCall") &&
|
|
272
316
|
state.degenerateContinues < (degen.maxContinues ?? 2)) {
|
|
317
|
+
retractSeat();
|
|
273
318
|
state.degenerateContinues++;
|
|
274
319
|
await emit({ type: "turn_end", message, toolResults: [] });
|
|
275
320
|
const drained = (await state.config.getSteeringMessages?.()) || [];
|
|
@@ -280,7 +325,8 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
280
325
|
state.degenerateContinues = 0;
|
|
281
326
|
}
|
|
282
327
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
283
|
-
|
|
328
|
+
retractSeat();
|
|
329
|
+
const errorFinalResults = await harvestExecutorOnErrorFinal(executor, state, message, turnController.signal, emit);
|
|
284
330
|
await emit({ type: "turn_end", message, toolResults: errorFinalResults });
|
|
285
331
|
await emit({ type: "agent_end", messages: state.newMessages });
|
|
286
332
|
return { kind: "terminal", reason: "assistant_error" };
|
|
@@ -289,7 +335,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
289
335
|
const toolResults = [];
|
|
290
336
|
state.hasMoreToolCalls = false;
|
|
291
337
|
if (toolCalls.length > 0) {
|
|
292
|
-
const executedToolBatch = await executeToolCalls(state.context, message, state.config, signal, emit, executor);
|
|
338
|
+
const executedToolBatch = await executeToolCalls(state.context, message, state.config, turnController.signal, emit, executor);
|
|
293
339
|
toolResults.push(...executedToolBatch.messages);
|
|
294
340
|
state.hasMoreToolCalls = !executedToolBatch.terminate;
|
|
295
341
|
for (const result of toolResults) {
|
|
@@ -303,6 +349,10 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
303
349
|
await closeOrphanedStreamEntry(orphan, emit);
|
|
304
350
|
}
|
|
305
351
|
}
|
|
352
|
+
retractSeat();
|
|
353
|
+
if (toolCalls.length > 0 && turnController.signal.aborted && !signal?.aborted) {
|
|
354
|
+
return await settleInterruptedTurn(state, message, executor, toolResults, signal, emit);
|
|
355
|
+
}
|
|
306
356
|
await emit({ type: "turn_end", message, toolResults });
|
|
307
357
|
const nextTurnContext = {
|
|
308
358
|
message,
|
|
@@ -311,15 +361,7 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
311
361
|
newMessages: state.newMessages,
|
|
312
362
|
};
|
|
313
363
|
const nextTurnSnapshot = await state.config.prepareNextTurn?.(nextTurnContext);
|
|
314
|
-
|
|
315
|
-
state.context = nextTurnSnapshot.context ?? state.context;
|
|
316
|
-
state.config = Object.assign({}, state.config, {
|
|
317
|
-
model: nextTurnSnapshot.model ?? state.config.model,
|
|
318
|
-
reasoning: nextTurnSnapshot.thinkingLevel === undefined
|
|
319
|
-
? state.config.reasoning
|
|
320
|
-
: nextTurnSnapshot.thinkingLevel,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
364
|
+
adoptNextTurnSnapshot(state, nextTurnSnapshot);
|
|
323
365
|
if (await state.config.shouldStopAfterTurn?.({
|
|
324
366
|
message,
|
|
325
367
|
toolResults,
|
|
@@ -383,6 +425,102 @@ async function runSingleTurn(state, signal, emit, streamFn, runtime, trace) {
|
|
|
383
425
|
}
|
|
384
426
|
return { kind: "ran" };
|
|
385
427
|
}
|
|
428
|
+
function adoptNextTurnSnapshot(state, nextTurnSnapshot) {
|
|
429
|
+
if (!nextTurnSnapshot)
|
|
430
|
+
return;
|
|
431
|
+
state.context = nextTurnSnapshot.context ?? state.context;
|
|
432
|
+
state.config = Object.assign({}, state.config, {
|
|
433
|
+
model: nextTurnSnapshot.model ?? state.config.model,
|
|
434
|
+
reasoning: nextTurnSnapshot.thinkingLevel === undefined
|
|
435
|
+
? state.config.reasoning
|
|
436
|
+
: nextTurnSnapshot.thinkingLevel,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const TURN_INTERRUPTED_TOOL_TEXT = "[INTERRUPTED] The user interrupted this turn before this tool call started. It was never executed and " +
|
|
440
|
+
"had no side effects. Address the user's interjection first; re-issue the call afterwards if it is still needed.";
|
|
441
|
+
async function settleInterruptedTurn(state, message, executor, committedResults, signal, emit) {
|
|
442
|
+
const toolResults = [...committedResults];
|
|
443
|
+
toolResults.push(...(await harvestExecutorOnErrorFinal(executor, state, message, signal, emit)));
|
|
444
|
+
const turnCalls = message.content.filter((c) => c.type === "toolCall");
|
|
445
|
+
const answered = new Set(toolResults.map((r) => r.toolCallId));
|
|
446
|
+
for (const call of turnCalls) {
|
|
447
|
+
if (answered.has(call.id))
|
|
448
|
+
continue;
|
|
449
|
+
await emit({
|
|
450
|
+
type: "tool_execution_start",
|
|
451
|
+
toolCallId: call.id,
|
|
452
|
+
toolName: call.name,
|
|
453
|
+
args: call.arguments,
|
|
454
|
+
});
|
|
455
|
+
const finalized = {
|
|
456
|
+
toolCall: call,
|
|
457
|
+
result: {
|
|
458
|
+
content: [{ type: "text", text: TURN_INTERRUPTED_TOOL_TEXT }],
|
|
459
|
+
details: { errorKind: "interrupted_never_started" },
|
|
460
|
+
},
|
|
461
|
+
isError: true,
|
|
462
|
+
};
|
|
463
|
+
await emit({
|
|
464
|
+
type: "tool_execution_end",
|
|
465
|
+
toolCallId: call.id,
|
|
466
|
+
toolName: call.name,
|
|
467
|
+
result: finalized.result,
|
|
468
|
+
isError: true,
|
|
469
|
+
notExecuted: true,
|
|
470
|
+
});
|
|
471
|
+
const toolResultMessage = createToolResultMessage(finalized);
|
|
472
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
473
|
+
state.context.messages.push(toolResultMessage);
|
|
474
|
+
state.newMessages.push(toolResultMessage);
|
|
475
|
+
toolResults.push(toolResultMessage);
|
|
476
|
+
}
|
|
477
|
+
if (turnCalls.length === 0 &&
|
|
478
|
+
isBlankFailureContent(message) &&
|
|
479
|
+
state.context.messages[state.context.messages.length - 1] === message) {
|
|
480
|
+
state.context.messages.pop();
|
|
481
|
+
if (state.newMessages[state.newMessages.length - 1] === message)
|
|
482
|
+
state.newMessages.pop();
|
|
483
|
+
}
|
|
484
|
+
const marker = {
|
|
485
|
+
role: "user",
|
|
486
|
+
content: [
|
|
487
|
+
{
|
|
488
|
+
type: "text",
|
|
489
|
+
text: turnCalls.length > 0 ? INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER : INTERRUPTED_BY_USER_MARKER,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
provenance: "engine-note",
|
|
493
|
+
timestamp: Date.now(),
|
|
494
|
+
};
|
|
495
|
+
await emit({ type: "message_start", message: marker });
|
|
496
|
+
await emit({ type: "message_end", message: marker });
|
|
497
|
+
state.context.messages.push(marker);
|
|
498
|
+
state.newMessages.push(marker);
|
|
499
|
+
state.hasMoreToolCalls = false;
|
|
500
|
+
await emit({ type: "turn_end", message, toolResults });
|
|
501
|
+
if (signal?.aborted) {
|
|
502
|
+
await emit({ type: "agent_end", messages: state.newMessages });
|
|
503
|
+
return { kind: "terminal", reason: "aborted_before_stream" };
|
|
504
|
+
}
|
|
505
|
+
const nextTurnSnapshot = await state.config.prepareNextTurn?.({
|
|
506
|
+
message,
|
|
507
|
+
toolResults,
|
|
508
|
+
context: state.context,
|
|
509
|
+
newMessages: state.newMessages,
|
|
510
|
+
});
|
|
511
|
+
adoptNextTurnSnapshot(state, nextTurnSnapshot);
|
|
512
|
+
if (await state.config.shouldStopAfterTurn?.({
|
|
513
|
+
message,
|
|
514
|
+
toolResults,
|
|
515
|
+
context: state.context,
|
|
516
|
+
newMessages: state.newMessages,
|
|
517
|
+
})) {
|
|
518
|
+
await emit({ type: "agent_end", messages: state.newMessages });
|
|
519
|
+
return { kind: "terminal", reason: "stop_requested" };
|
|
520
|
+
}
|
|
521
|
+
state.pendingMessages = (await state.config.getSteeringMessages?.()) || [];
|
|
522
|
+
return { kind: "ran" };
|
|
523
|
+
}
|
|
386
524
|
async function streamAssistantResponse(context, config, signal, emit, streamFn, runtime, executor, staticReasoningCutDowngrade) {
|
|
387
525
|
let messages = context.messages;
|
|
388
526
|
if (config.transformContext) {
|