@sema-agent/core 5.62.0 → 5.64.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 +96 -0
- package/dist/agents/cascade.d.ts +5 -1
- package/dist/agents/cascade.js +6 -1
- package/dist/agents/subagent.d.ts +12 -2
- package/dist/agents/subagent.js +4 -2
- package/dist/agents/verify.d.ts +5 -1
- package/dist/agents/verify.js +5 -2
- package/dist/core/auto-compaction.d.ts +6 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/checkpoint-store.d.ts +5 -1
- package/dist/core/context-edit.d.ts +36 -29
- package/dist/core/context-edit.js +3 -3
- package/dist/core/fs-write-gate-policy.d.ts +21 -0
- package/dist/core/fs-write-gate-policy.js +14 -3
- package/dist/core/hooks.d.ts +8 -5
- package/dist/core/memory-engine/engine.d.ts +11 -0
- package/dist/core/memory-engine/engine.js +29 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/origin-clearance.d.ts +28 -0
- package/dist/core/remote-env.d.ts +34 -2
- package/dist/core/runner/prepare-config-doors.d.ts +2 -2
- package/dist/core/runner/prepare-config-doors.js +11 -8
- package/dist/core/runner/prepare-task.d.ts +87 -5
- package/dist/core/runner/prepare-task.js +174 -92
- package/dist/core/runner/prepare-workspace-restore.js +13 -0
- package/dist/core/runner/runtask.js +203 -152
- package/dist/core/trace.d.ts +5 -4
- package/dist/core/types.d.ts +38 -20
- package/dist/core/usage-window-store.d.ts +44 -12
- package/dist/core/usage-window-store.js +11 -3
- package/dist/core/workflow-run-store-contract.js +17 -0
- package/dist/core/workflow-run-store.d.ts +22 -1
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/harness/agent-harness.d.ts +8 -3
- package/dist/engine/harness/agent-harness.js +29 -9
- package/dist/engine/harness/types.d.ts +127 -3
- package/dist/engine/loop/agent-loop.js +47 -4
- package/dist/engine/loop/types.d.ts +67 -6
- package/dist/index.d.ts +2 -2
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +6 -0
- package/dist/orchestration/run-workflow-tool.js +1 -0
- package/dist/orchestration/workflow-types.d.ts +48 -1
- package/dist/orchestration/workflow-types.js +12 -4
- package/dist/orchestration/workflow.d.ts +18 -1
- package/dist/orchestration/workflow.js +45 -19
- package/dist/prompts/default.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +44 -1
- package/dist/tools/fs/bash-readonly-classifier.js +132 -5
- package/dist/tools/fs/fs-bash.js +9 -2
- package/dist/tools/fs/fs-write.js +19 -8
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +1 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
16
16
|
* One governed window. Its fields are allowances, not hints: `windowMs` is the window's WIDTH and
|
|
17
|
-
* `maxTokens` / `maxCostUsd` the two CEILINGS it admits work under
|
|
17
|
+
* `maxTokens` / `maxCostUsd` the two CEILINGS it admits work under — each optional on its own, at least
|
|
18
|
+
* one required (a window with neither is refused at the door). `anchor` picks between the two window
|
|
18
19
|
* shapes that actually exist in the wild:
|
|
19
20
|
* - `"first-use"` — the window OPENS at the key's first charge and lasts `windowMs`; when it lapses with
|
|
20
21
|
* no further use, the next charge opens a fresh one. This is the "5 hours from when you started"
|
|
@@ -26,19 +27,33 @@ export interface UsageWindow {
|
|
|
26
27
|
/** Window width in ms. Must be finite and > 0 (a zero-width window admits nothing and expires
|
|
27
28
|
* instantly — refused at the door rather than silently governing every task to death). */
|
|
28
29
|
windowMs: number;
|
|
29
|
-
/** Tokens the window admits before it is exhausted. Must be finite and >= 0; `0` is a real, always-full
|
|
30
|
-
* window (an operator lock-out), not "unset". */
|
|
31
|
-
maxTokens: number;
|
|
32
30
|
/**
|
|
33
|
-
*
|
|
31
|
+
* Tokens the window admits before it is exhausted. Must be finite and >= 0 WHEN PRESENT; `0` is a real,
|
|
32
|
+
* always-full window (an operator lock-out), not "unset" — absence is the only way to say "this window
|
|
33
|
+
* does not govern tokens".
|
|
34
|
+
*
|
|
35
|
+
* OPTIONAL since the $ ceiling below became a peer rather than an addition: a deployment governed by
|
|
36
|
+
* SPEND alone would otherwise have had to declare a token ceiling no run could reach, i.e. a number
|
|
37
|
+
* nobody chose sitting in the field this door exists to keep honest. Either ceiling alone is a complete
|
|
38
|
+
* declaration; a window that declares NEITHER is refused (`config.usage_window_invalid`) rather than
|
|
39
|
+
* admitting everything silently.
|
|
40
|
+
*/
|
|
41
|
+
maxTokens?: number;
|
|
42
|
+
/**
|
|
43
|
+
* The OTHER ceiling: the MONEY the window admits before it is exhausted, in absolute USD — the
|
|
34
44
|
* same quantity and unit `TaskLimits.maxCostUsd` names, one governance level up (a task's own allowance
|
|
35
|
-
* vs the allowance an operator grants a principal across tasks). Must be finite
|
|
45
|
+
* vs the allowance an operator grants a principal across tasks). Must be finite, >= 0, and must still be
|
|
46
|
+
* finite once converted to the ledger's micro-USD unit (`× 1e6`, i.e. below ~1.79e302 USD — an Infinity
|
|
47
|
+
* threshold is not the declared ceiling in the unit the ledger counts, so such a window would read as
|
|
48
|
+
* governed while enforcing nothing it was given, and is refused at the door). `0` is a real,
|
|
36
49
|
* always-full window, exactly as it is for `maxTokens`.
|
|
37
50
|
*
|
|
38
|
-
* The two ceilings are INDEPENDENT and
|
|
39
|
-
* `maxTokens` or its cost reaches `maxCostUsd` — whichever fills first
|
|
40
|
-
*
|
|
41
|
-
*
|
|
51
|
+
* The two ceilings are INDEPENDENT and every DECLARED one binds: a window is exhausted when either its
|
|
52
|
+
* tokens reach `maxTokens` or its cost reaches `maxCostUsd` — whichever fills first — and a ceiling that
|
|
53
|
+
* is absent is not a ceiling at all (never a zero, never an infinity: it simply does not participate).
|
|
54
|
+
* Absent (the default, and every pre-existing deployment) means THIS window governs tokens only — its
|
|
55
|
+
* own reading never asks for or enforces a cost. Whether cost is tracked/recorded at all is a
|
|
56
|
+
* DEPLOYMENT-level fact, not a
|
|
42
57
|
* per-window one (the `UsageSlot.costMicroUsd` / `UsageBucketRow` in-presence condition: "while the
|
|
43
58
|
* deployment governs at least one `maxCostUsd` window"): in a MIXED declaration the shared ledger
|
|
44
59
|
* rows carry cost under every window's bucket, and only a deployment with NO $ window anywhere keeps
|
|
@@ -111,8 +126,10 @@ export interface UsageWindowReading {
|
|
|
111
126
|
* whole window as exhausted until the unpriced charge ages out, would lock a key out for as long as
|
|
112
127
|
* `windowMs` over one degraded turn, which is a larger outage than the gap it answers. */
|
|
113
128
|
costUnknown?: true;
|
|
114
|
-
/** The window admits no further work: `tokens >= window.maxTokens`, OR
|
|
115
|
-
* `costMicroUsd >= window.maxCostUsd` — the two ceilings are independent
|
|
129
|
+
/** The window admits no further work: (when the window declares one) `tokens >= window.maxTokens`, OR
|
|
130
|
+
* (when the window declares one) `costMicroUsd >= window.maxCostUsd` — the two ceilings are independent
|
|
131
|
+
* and every DECLARED one binds. An undeclared axis never binds, however much the window holds on it:
|
|
132
|
+
* `tokens` is still reported for a money-only window (what it holds), it is just not a ceiling there. */
|
|
116
133
|
exhausted: boolean;
|
|
117
134
|
/** Ms until this window next frees capacity: for `first-use`, when the open window lapses; for
|
|
118
135
|
* `rolling`, when its OLDEST in-window slot ages out (which frees that slot's tokens, not
|
|
@@ -146,6 +163,21 @@ export interface UsageWindowStore {
|
|
|
146
163
|
* the pre-cost-arm signature drops the argument silently — which is why the arithmetic refuses an absent
|
|
147
164
|
* cost outright (`usage_window.store_cost_unanswered`) instead of folding it to a 0 that would leave the
|
|
148
165
|
* money ceiling open forever with nothing able to notice.
|
|
166
|
+
*
|
|
167
|
+
* IMPLEMENTER SELF-CHECK — three questions, in order. The in-presence condition above is a
|
|
168
|
+
* DEPLOYMENT-wide property of the `windows` argument, not a per-window one, and a store that reasons one
|
|
169
|
+
* window at a time is exactly where it gets missed:
|
|
170
|
+
* 1. Does this call's window set carry ANY window declaring `maxCostUsd`? Then this is a COST charge
|
|
171
|
+
* for every row it writes — the rolling slot is ONE row every rolling window reads, and a first-use
|
|
172
|
+
* bucket is ONE row per width shared by every window of that width — so there is no "cost only on
|
|
173
|
+
* the $ windows" split to implement. A row written cost-less because a token-only window happened
|
|
174
|
+
* to be the one in view is a row the $ window later reads as unpriced.
|
|
175
|
+
* 2. Was the spend real but unpriceable (no rate for the model, a provider that answered no usage)?
|
|
176
|
+
* Then carry the `null` through AS `null`, never as `0`: `0` asserts "this charge cost nothing" and
|
|
177
|
+
* silently raises the remaining allowance, while `null` is what lets the row say so and surfaces as
|
|
178
|
+
* {@link UsageWindowReading.costUnknown}, keeping the reported total honest as a LOWER BOUND.
|
|
179
|
+
* 3. Cannot answer either — the argument never reached this layer? Then leave it `undefined` and let
|
|
180
|
+
* the charge be refused loudly. A guessed 0 is the one outcome the money ceiling cannot survive.
|
|
149
181
|
*/
|
|
150
182
|
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[], costMicroUsd?: number | null): Promise<void>;
|
|
151
183
|
/** Read `key`'s state for each window as of `now`, in the order the windows were given. */
|
|
@@ -21,12 +21,19 @@ export function resolveUsageWindows(windows) {
|
|
|
21
21
|
if (typeof w.windowMs !== "number" || !Number.isFinite(w.windowMs) || w.windowMs <= 0) {
|
|
22
22
|
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].windowMs must be a finite number of milliseconds > 0 (got ${String(w.windowMs)})`);
|
|
23
23
|
}
|
|
24
|
-
if (typeof w.maxTokens !== "number" || !Number.isFinite(w.maxTokens) || w.maxTokens < 0) {
|
|
25
|
-
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].maxTokens must be a finite, non-negative number (got ${String(w.maxTokens)})`);
|
|
24
|
+
if (w.maxTokens !== undefined && (typeof w.maxTokens !== "number" || !Number.isFinite(w.maxTokens) || w.maxTokens < 0)) {
|
|
25
|
+
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].maxTokens must be a finite, non-negative number when present (got ${String(w.maxTokens)})`);
|
|
26
26
|
}
|
|
27
27
|
if (w.maxCostUsd !== undefined && (typeof w.maxCostUsd !== "number" || !Number.isFinite(w.maxCostUsd) || w.maxCostUsd < 0)) {
|
|
28
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
29
|
}
|
|
30
|
+
const maxCostMicroUsd = maxCostMicroUsdOf(w);
|
|
31
|
+
if (maxCostMicroUsd !== undefined && !Number.isFinite(maxCostMicroUsd)) {
|
|
32
|
+
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].maxCostUsd ${String(w.maxCostUsd)} overflows the ledger's micro-USD unit (${String(w.maxCostUsd)} × 1e6 = ${String(maxCostMicroUsd)}) — Infinity is not the declared ceiling in the unit this ledger counts, so the window would read as governed while enforcing nothing it was given. Declare a ceiling below 1.79e302 USD.`);
|
|
33
|
+
}
|
|
34
|
+
if (w.maxTokens === undefined && w.maxCostUsd === undefined) {
|
|
35
|
+
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[] must declare at least one ceiling — maxTokens, maxCostUsd, or both (got neither, over windowMs ${String(w.windowMs)}, ${String(w.anchor)}). A window with no ceiling admits everything, which is not what declaring a window means.`);
|
|
36
|
+
}
|
|
30
37
|
if (w.anchor !== "first-use" && w.anchor !== "rolling") {
|
|
31
38
|
throw usageConfigError("config.usage_window_invalid", `RunnerDeps.usageWindows[].anchor must be "first-use" or "rolling" (got ${String(w.anchor)})`);
|
|
32
39
|
}
|
|
@@ -132,7 +139,8 @@ export function readUsageRecord(record, windows, now) {
|
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
const maxCostMicroUsd = maxCostMicroUsdOf(w);
|
|
135
|
-
const exhausted =
|
|
142
|
+
const exhausted = (w.maxTokens !== undefined && tokens >= w.maxTokens) ||
|
|
143
|
+
(maxCostMicroUsd !== undefined && costMicroUsd >= maxCostMicroUsd);
|
|
136
144
|
const retryAfterMs = exhausted ? Math.max(1, freesAt === undefined ? w.windowMs : freesAt - now) : 0;
|
|
137
145
|
readings.push({
|
|
138
146
|
window: w,
|
|
@@ -101,6 +101,23 @@ export async function workflowRunStoreContract(make, runAssertion = defaultSeque
|
|
|
101
101
|
assert.equal(namedSummary.description, "find flaky tests");
|
|
102
102
|
assert.equal(namedSummary.currentPhase, "fix");
|
|
103
103
|
});
|
|
104
|
+
run("the ADDITIVE terminal disclosures reach the LIST row: agentFailures + budgetOvershoot project, and a clean run keeps both keys ABSENT", async () => {
|
|
105
|
+
const store = make();
|
|
106
|
+
const over = createWorkflowRun({ id: "d-over", createdAt: 5_000, agentFailures: 2, budgetOvershoot: { budgetTokens: 150, spentTokens: 1_200 } });
|
|
107
|
+
const unsettled = createWorkflowRun({ id: "d-unsettled", createdAt: 6_000, budgetOvershoot: { budgetTokens: 100, spentTokens: 0, unsettledTokens: 500 } });
|
|
108
|
+
const clean = createWorkflowRun({ id: "d-clean", createdAt: 7_000 });
|
|
109
|
+
for (const r of [over, unsettled, clean])
|
|
110
|
+
await store.put(r.id, r);
|
|
111
|
+
const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.id, s]));
|
|
112
|
+
assert.equal(byId.get("d-over").agentFailures, 2);
|
|
113
|
+
assert.deepEqual(byId.get("d-over").budgetOvershoot, { budgetTokens: 150, spentTokens: 1_200 });
|
|
114
|
+
assert.deepEqual(byId.get("d-unsettled").budgetOvershoot, { budgetTokens: 100, spentTokens: 0, unsettledTokens: 500 });
|
|
115
|
+
assert.equal("agentFailures" in byId.get("d-clean"), false, "no failures ⇒ the key is OMITTED, not 0/null");
|
|
116
|
+
assert.equal("budgetOvershoot" in byId.get("d-clean"), false, "within (or without) a ceiling ⇒ the key is OMITTED, not null/zeroed");
|
|
117
|
+
assert.equal(byId.get("d-over").tokens, over.stats.tokens + over.stats.nested.tokens);
|
|
118
|
+
byId.get("d-over").budgetOvershoot.spentTokens = 7;
|
|
119
|
+
assert.equal((await store.get("d-over")).budgetOvershoot.spentTokens, 1_200);
|
|
120
|
+
});
|
|
104
121
|
run("reap: only terminal runs, NEVER running; maxAgeMs + keep retention; no policy → no-op", async () => {
|
|
105
122
|
const store = make();
|
|
106
123
|
const running = createWorkflowRun({ id: "k-run", status: "running", createdAt: 100, endedAt: undefined });
|
|
@@ -49,6 +49,24 @@ export interface WorkflowRunSummary {
|
|
|
49
49
|
/** Count of agent-runs that ended failed ({@link WorkflowRun.agentFailures}) — present only when > 0.
|
|
50
50
|
* Lets a list view flag a "completed, with failures" run without an N+1 `get` of the full run. */
|
|
51
51
|
agentFailures?: number;
|
|
52
|
+
/** The run's TERMINAL token-budget overshoot ({@link WorkflowRun.budgetOvershoot}) — present ONLY when the
|
|
53
|
+
* run actually spent MORE than its ceiling, absent on every run that stayed within one and on every run
|
|
54
|
+
* that set no budget at all. The list-view twin of {@link agentFailures}: it lets a row be flagged
|
|
55
|
+
* "completed, and over budget" without an N+1 `get` of the full run — the *other* way a `completed` row is
|
|
56
|
+
* not the unqualified good news it looks like. It cannot be DERIVED from anything else on this projection:
|
|
57
|
+
* {@link tokens} is a different axis (see its note), so without this seat an over-budget run reads on the
|
|
58
|
+
* list face exactly like a clean one.
|
|
59
|
+
*
|
|
60
|
+
* Projected as a de-aliased shallow COPY — every other member here is a primitive, so the projection must
|
|
61
|
+
* not hand a caller a live reference into a stored run (the same anti-aliasing posture the stores' `get`
|
|
62
|
+
* keeps with `structuredClone`). Read the members per {@link WorkflowRun.budgetOvershoot}: the overshoot is
|
|
63
|
+
* `spentTokens + unsettledTokens - budgetTokens`, and a run resting entirely on `unsettledTokens` is a
|
|
64
|
+
* best-effort disclosure rather than a measurement. */
|
|
65
|
+
budgetOvershoot?: {
|
|
66
|
+
budgetTokens: number;
|
|
67
|
+
spentTokens: number;
|
|
68
|
+
unsettledTokens?: number;
|
|
69
|
+
};
|
|
52
70
|
/** Title of the latest phase recorded ({@link WorkflowRun.phases}`.at(-1).title`) — what the run is on RIGHT
|
|
53
71
|
* NOW for a `running` row (so the `/workflows` list shows the live phase without subscribing to the event
|
|
54
72
|
* stream). Absent when no phase has started yet. */
|
|
@@ -58,7 +76,10 @@ export interface WorkflowRunSummary {
|
|
|
58
76
|
/** Number of agent-runs recorded so far ({@link WorkflowRun.agents}`.length`). */
|
|
59
77
|
agentCount: number;
|
|
60
78
|
/** Total tokens spent = own + nested (`stats.tokens + stats.nested.tokens`) — the figure a triage view
|
|
61
|
-
* sorts/compares by. own/nested stay SEPARATE on the full run (R-5); the summary folds them for display.
|
|
79
|
+
* sorts/compares by. own/nested stay SEPARATE on the full run (R-5); the summary folds them for display.
|
|
80
|
+
* ⚠️ NOT the budget axis's figure: run stats also count REPLAYED work, which the budget deliberately never
|
|
81
|
+
* charges, so comparing this against a ceiling proves nothing in either direction — whether a run overran
|
|
82
|
+
* its budget is read off {@link budgetOvershoot}, never inferred from here. */
|
|
62
83
|
tokens: number;
|
|
63
84
|
/** When the run started ({@link WorkflowRun.startedAt}, epoch ms). */
|
|
64
85
|
startedAt: number;
|
|
@@ -9,6 +9,7 @@ export function summarizeWorkflowRun(run) {
|
|
|
9
9
|
...(run.description !== undefined ? { description: run.description } : {}),
|
|
10
10
|
status: run.status,
|
|
11
11
|
...(run.agentFailures !== undefined ? { agentFailures: run.agentFailures } : {}),
|
|
12
|
+
...(run.budgetOvershoot !== undefined ? { budgetOvershoot: { ...run.budgetOvershoot } } : {}),
|
|
12
13
|
...(latestPhase !== undefined ? { currentPhase: latestPhase.title } : {}),
|
|
13
14
|
phaseCount: run.phases.length,
|
|
14
15
|
agentCount: run.agents.length,
|
|
@@ -70,7 +70,8 @@ export interface UserMessageProvenance {
|
|
|
70
70
|
* (e.g. a forced compaction) — otherwise the next turn's `createTurnState()` rebuilds the context
|
|
71
71
|
* from the session and the in-memory shrink evaporates. So the harness contract is: `recover`
|
|
72
72
|
* persists the reduction to the session and returns whether anything shrank; the harness then
|
|
73
|
-
* rebuilds the turn state FROM the session and hands the loop the rebuilt transcript
|
|
73
|
+
* rebuilds the turn state FROM the session and hands the loop the rebuilt context — transcript AND
|
|
74
|
+
* system prompt (#474 ①), because the reduction pass may commit a prompt-epoch change of its own.
|
|
74
75
|
*/
|
|
75
76
|
export interface HarnessLoopRecovery {
|
|
76
77
|
truncatedOutput?: LoopTruncatedOutputRecovery;
|
|
@@ -86,8 +87,12 @@ export interface HarnessLoopRecovery {
|
|
|
86
87
|
maxContinues?: number;
|
|
87
88
|
};
|
|
88
89
|
promptTooLong?: {
|
|
89
|
-
/** Persist a context reduction to the SESSION; return true if anything shrank. Must not throw.
|
|
90
|
-
|
|
90
|
+
/** Persist a context reduction to the SESSION; return true if anything shrank. Must not throw.
|
|
91
|
+
* `turnSignal` is the TURN-scoped abort (#474 ②): a policy whose work is expensive (a forced
|
|
92
|
+
* compaction's summary call) must ride it so a turn interrupt cuts the pass short instead of
|
|
93
|
+
* being waited out — and a policy that declines under it leaves the loop settling the turn as
|
|
94
|
+
* an interrupted one, never as the provider error's terminal. */
|
|
95
|
+
recover: (attempt: number, turnSignal?: AbortSignal) => Promise<boolean>;
|
|
91
96
|
/** Override the loop's default prompt-too-long classifier. */
|
|
92
97
|
detect?: (message: AssistantMessage) => boolean;
|
|
93
98
|
/** Max recovery retries per turn (loop default: 2). */
|
|
@@ -547,7 +547,7 @@ export class AgentHarness {
|
|
|
547
547
|
userInputParkRecords.delete(message);
|
|
548
548
|
engineNotePayloads.delete(message);
|
|
549
549
|
}
|
|
550
|
-
createLoopConfig(getTurnState, setTurnState) {
|
|
550
|
+
createLoopConfig(getTurnState, setTurnState, runPromptOverride) {
|
|
551
551
|
const turnState = getTurnState();
|
|
552
552
|
const self = this;
|
|
553
553
|
return {
|
|
@@ -564,9 +564,24 @@ export class AgentHarness {
|
|
|
564
564
|
: {}),
|
|
565
565
|
convertToLlm: (messages) => stripEngineMetadata(convertToLlm(messages)),
|
|
566
566
|
shouldStopAfterTurn: () => this._stopAfterTurn,
|
|
567
|
-
transformContext: async (messages) => {
|
|
568
|
-
const result = await this.emitHook({ type: "context", messages: [...messages] });
|
|
569
|
-
|
|
567
|
+
transformContext: async (messages, signal) => {
|
|
568
|
+
const result = await this.emitHook({ type: "context", messages: [...messages], ...(signal !== undefined ? { signal } : {}) });
|
|
569
|
+
if (result?.adoptSessionRebuild !== true) {
|
|
570
|
+
return result?.messages ?? messages;
|
|
571
|
+
}
|
|
572
|
+
await this.flushPendingSessionWrites();
|
|
573
|
+
const nextTurnState = await this.createTurnState();
|
|
574
|
+
setTurnState(nextTurnState);
|
|
575
|
+
const rebuiltContext = this.createContext(nextTurnState, runPromptOverride);
|
|
576
|
+
const recheck = await this.emitHook({ type: "context", messages: [...rebuiltContext.messages], ...(signal !== undefined ? { signal } : {}), recheck: true });
|
|
577
|
+
return {
|
|
578
|
+
messages: recheck?.messages ?? rebuiltContext.messages,
|
|
579
|
+
adoptedContext: {
|
|
580
|
+
messages: rebuiltContext.messages,
|
|
581
|
+
systemPrompt: rebuiltContext.systemPrompt,
|
|
582
|
+
...(rebuiltContext.systemBlocks !== undefined ? { systemBlocks: rebuiltContext.systemBlocks } : {}),
|
|
583
|
+
},
|
|
584
|
+
};
|
|
570
585
|
},
|
|
571
586
|
beforeToolCall: async ({ toolCall, args }) => {
|
|
572
587
|
const result = await this.emitHook({
|
|
@@ -595,7 +610,7 @@ export class AgentHarness {
|
|
|
595
610
|
const nextTurnState = await this.createTurnState();
|
|
596
611
|
setTurnState(nextTurnState);
|
|
597
612
|
return {
|
|
598
|
-
context: this.createContext(nextTurnState),
|
|
613
|
+
context: this.createContext(nextTurnState, runPromptOverride),
|
|
599
614
|
model: nextTurnState.model,
|
|
600
615
|
thinkingLevel: nextTurnState.thinkingLevel,
|
|
601
616
|
};
|
|
@@ -633,10 +648,10 @@ export class AgentHarness {
|
|
|
633
648
|
detect: ptl.detect,
|
|
634
649
|
maxRetries: ptl.maxRetries,
|
|
635
650
|
withholdErrorEvents: ptl.withholdErrorEvents,
|
|
636
|
-
recover: async (_messages, attempt) => {
|
|
651
|
+
recover: async (_messages, attempt, turnSignal) => {
|
|
637
652
|
let shrank = false;
|
|
638
653
|
try {
|
|
639
|
-
shrank = await ptl.recover(attempt);
|
|
654
|
+
shrank = await ptl.recover(attempt, turnSignal);
|
|
640
655
|
}
|
|
641
656
|
catch {
|
|
642
657
|
shrank = false;
|
|
@@ -645,7 +660,12 @@ export class AgentHarness {
|
|
|
645
660
|
return undefined;
|
|
646
661
|
const nextTurnState = await self.createTurnState();
|
|
647
662
|
setTurnState(nextTurnState);
|
|
648
|
-
|
|
663
|
+
const rebuilt = self.createContext(nextTurnState, runPromptOverride);
|
|
664
|
+
return {
|
|
665
|
+
messages: rebuilt.messages,
|
|
666
|
+
systemPrompt: rebuilt.systemPrompt,
|
|
667
|
+
...(rebuilt.systemBlocks !== undefined ? { systemBlocks: rebuilt.systemBlocks } : {}),
|
|
668
|
+
};
|
|
649
669
|
},
|
|
650
670
|
};
|
|
651
671
|
},
|
|
@@ -780,7 +800,7 @@ export class AgentHarness {
|
|
|
780
800
|
this.runAbortController = abortController;
|
|
781
801
|
const runResultPromise = (async () => {
|
|
782
802
|
try {
|
|
783
|
-
return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
|
|
803
|
+
return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
|
|
784
804
|
}
|
|
785
805
|
catch (error) {
|
|
786
806
|
try {
|
|
@@ -82,12 +82,38 @@ export interface AgentHarnessStreamOptionsPatch extends Omit<Partial<AgentHarnes
|
|
|
82
82
|
}
|
|
83
83
|
/** Kind of filesystem object as addressed by a {@link FileSystem}. Symlinks are not followed automatically. */
|
|
84
84
|
export type FileKind = "file" | "directory" | "symlink";
|
|
85
|
+
/** design/380 O13 — the precondition a {@link FileSystem.writeFileGuarded} call verifies IN the same
|
|
86
|
+
* atomic backend step as the write (minted by the caller from its canonicalization/adjudication pass). */
|
|
87
|
+
export interface WriteExpectation {
|
|
88
|
+
/** Refuse (nothing written) unless the FINAL open target canonicalizes to this path. */
|
|
89
|
+
canonicalPath: string;
|
|
90
|
+
/** Backend object identity (inode/file-id) when the precondition minter observed one; opaque string. */
|
|
91
|
+
fileId?: string;
|
|
92
|
+
/** Target must not exist (atomic exclusive create — the writeFileExclusive semantics folded in). */
|
|
93
|
+
exclusive?: boolean;
|
|
94
|
+
/** Open with no-follow semantics: a symlink at the leaf fails instead of being traversed. */
|
|
95
|
+
noFollow?: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** design/380 O13 — the receipt a successful {@link FileSystem.writeFileGuarded} returns: facts about
|
|
98
|
+
* the REAL on-disk object the atomic step produced (minter-stated, post-op). */
|
|
99
|
+
export interface WriteReceipt {
|
|
100
|
+
/** The ACTUAL canonical path written (post-op fact, minter-stated). */
|
|
101
|
+
canonicalPath: string;
|
|
102
|
+
fileId?: string;
|
|
103
|
+
created: boolean;
|
|
104
|
+
}
|
|
85
105
|
/** Stable, backend-independent file error codes returned by {@link FileSystem} file operations. */
|
|
86
106
|
export type FileErrorCode = "aborted" | "not_found" | "permission_denied" | "not_directory" | "is_directory" | "invalid" | "not_supported"
|
|
87
107
|
/** Exclusive create refused because the path already exists ({@link FileSystem.writeFileExclusive},
|
|
88
108
|
* POSIX `O_EXCL` shape). Discriminable so a caller can distinguish "lost the create race" from a
|
|
89
109
|
* write failure and give honest guidance instead of overwriting. */
|
|
90
110
|
| "already_exists"
|
|
111
|
+
/** design/380 O13 — {@link FileSystem.writeFileGuarded}'s precondition did not hold at the atomic
|
|
112
|
+
* write step (the final open target did not canonicalize to the expected path / the backend object
|
|
113
|
+
* identity moved / an `exclusive` create found the target present). NOTHING was written —
|
|
114
|
+
* discriminable so a caller can tell "the world moved between adjudication and write" from an
|
|
115
|
+
* ordinary write failure and re-read instead of blindly retrying. */
|
|
116
|
+
| "precondition_failed"
|
|
91
117
|
/** A remote fs RPC exceeded its liveness/idle bound (a hung provider call) — retryable by the caller per
|
|
92
118
|
* the op's idempotency. Mirrors `RemoteExecutionError` code `"timeout"` so a remote `FileSystem` op can
|
|
93
119
|
* surface a hang as a typed, retryable error instead of `"unknown"` (core src/core/remote-env.ts contract;
|
|
@@ -97,7 +123,15 @@ export type FileErrorCode = "aborted" | "not_found" | "permission_denied" | "not
|
|
|
97
123
|
* retryable after reconnect. Mirrors `RemoteExecutionErrorCode` `"transport_lost"`, same precedent as
|
|
98
124
|
* `"timeout"` above (VENDOR.md mod #9 / service[63] SSH/ADB adapters), so a retry whitelist can see it
|
|
99
125
|
* instead of an opaque `"unknown"`. */
|
|
100
|
-
| "transport_lost"
|
|
126
|
+
| "transport_lost"
|
|
127
|
+
/** design/380 O12 — a WRITE-face op (writeFile/appendFile/remove…) was COMMITTED to a remote target and
|
|
128
|
+
* its outcome is UNKNOWABLE (the target went unreachable / restarted / was revoked after
|
|
129
|
+
* dispatch-commit). NEVER auto-retry and NEVER map onto `timeout`/`transport_lost` (both sit in retry
|
|
130
|
+
* whitelists): the bytes may already be on the target. Message MUST carry the verify-first sentence
|
|
131
|
+
* ("may have already executed on the target; verify its effect before re-running"). A device adapter's
|
|
132
|
+
* WRITE face uses this instead of `transport_lost` (whose retryable-after-reconnect contract is safe
|
|
133
|
+
* only for idempotent reads). */
|
|
134
|
+
| "outcome_unknown" | "unknown";
|
|
101
135
|
/** Error returned by {@link FileSystem} file operations. */
|
|
102
136
|
export declare class FileError extends Error {
|
|
103
137
|
/** Backend-independent error code. */
|
|
@@ -131,7 +165,22 @@ export type ExecutionErrorCode = "aborted" | "timeout"
|
|
|
131
165
|
* command never started, and every retry will fail the same way (and may lock the account). Mirrors
|
|
132
166
|
* `RemoteExecutionErrorCode` `"auth_failed"`; deliberately NOT a mirror of `auth_transient`, which an
|
|
133
167
|
* adapter retries internally per its own bounded policy rather than surfacing as an exec outcome. */
|
|
134
|
-
| "auth_failed"
|
|
168
|
+
| "auth_failed"
|
|
169
|
+
/** design/380 O12 — the op was COMMITTED to a remote target and its outcome is UNKNOWABLE (the target
|
|
170
|
+
* went unreachable / restarted / was revoked after dispatch-commit). NEVER auto-retry and NEVER map
|
|
171
|
+
* onto `timeout`/`transport_lost` (both sit in retry whitelists): the command may have already
|
|
172
|
+
* executed. Message MUST carry the verify-first sentence ("may have already executed on the target;
|
|
173
|
+
* verify its effect before re-running"). Distinct from `transport_lost` (connection story known,
|
|
174
|
+
* idempotency-gated retry) — this word is for the arm where the SERVER of the target protocol has
|
|
175
|
+
* already ruled the outcome unknowable. */
|
|
176
|
+
| "outcome_unknown"
|
|
177
|
+
/** design/380 O12 — the target refused the op BEFORE commit (device offline / busy / not bound): the
|
|
178
|
+
* command NEVER STARTED, so a retry is safe once the target is back. The honest never-started word for
|
|
179
|
+
* a device/remote adapter — `spawn_error` is contractually reserved for OS-level spawn failures (its
|
|
180
|
+
* 2026-07-13 contract above; the Bash dead-cwd diagnosis hangs off it) and `suspended` claims a paused
|
|
181
|
+
* workspace this arm does not have. Spelling note: landed per design/380's arm (b); the device lane's
|
|
182
|
+
* wire-word mapping may re-spell at the server's wire freeze. */
|
|
183
|
+
| "target_unavailable" | "unknown";
|
|
135
184
|
/** Error returned by {@link ExecutionEnv.exec}. */
|
|
136
185
|
export declare class ExecutionError extends Error {
|
|
137
186
|
/** Backend-independent error code. */
|
|
@@ -288,6 +337,25 @@ export interface FileSystem {
|
|
|
288
337
|
* emulation and own the residual race.
|
|
289
338
|
*/
|
|
290
339
|
writeFileExclusive?(path: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
340
|
+
/**
|
|
341
|
+
* design/380 O13 — OPTIONAL guarded write: verify `expect` and write in ONE atomic backend step
|
|
342
|
+
* (openat/O_NOFOLLOW family on a local backend; a single wire instruction on a remote one), returning
|
|
343
|
+
* a receipt of the real on-disk object. Precondition mismatch ⇒ FileError `"precondition_failed"`
|
|
344
|
+
* (closed-set addition), nothing written. Degradation law VERBATIM from writeFileExclusive: a backend
|
|
345
|
+
* with no such primitive MUST leave this `undefined` rather than emulate in two steps — a two-step
|
|
346
|
+
* emulation re-opens the very TOCTOU window this method exists to close. Callers fall back to plain
|
|
347
|
+
* writeFile and OWN the residual race (which the write gate then discloses as "advisory
|
|
348
|
+
* adjudication" instead of claiming TOCTOU closure).
|
|
349
|
+
*
|
|
350
|
+
* SCOPE OF THE CLOSURE (stated so nothing overclaims): the window closed is the one between the
|
|
351
|
+
* CALLER's own precondition mint (its canonicalization at execute time) and the write. A policy
|
|
352
|
+
* adjudication that happened EARLIER — e.g. a write-gate verdict that waited on a human approval —
|
|
353
|
+
* is bound to that policy's own resolution, which is not carried into the caller's expectation
|
|
354
|
+
* today; a symlink swapped during the approval wait is therefore still re-resolved at execute time.
|
|
355
|
+
* Binding the GATE's adjudicated key across the approval wait needs a trusted receipt-carriage seat
|
|
356
|
+
* (policy verdict → approval record → tool execution) that is a separate design, not this method.
|
|
357
|
+
*/
|
|
358
|
+
writeFileGuarded?(path: string, content: string | Uint8Array, expect: WriteExpectation, abortSignal?: AbortSignal): Promise<Result<WriteReceipt, FileError>>;
|
|
291
359
|
/** Create or append to a file, creating parent directories when supported. */
|
|
292
360
|
appendFile(path: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
293
361
|
/** Return metadata for the addressed path without following symlinks. */
|
|
@@ -919,6 +987,25 @@ export interface BeforeAgentStartEvent<TSkill extends Skill = Skill, TPromptTemp
|
|
|
919
987
|
export interface ContextEvent {
|
|
920
988
|
type: "context";
|
|
921
989
|
messages: AgentMessage[];
|
|
990
|
+
/**
|
|
991
|
+
* The TURN-scoped abort signal of the request build being transformed (present when the loop
|
|
992
|
+
* handed one — it always does in production; absent only for bare-harness callers). A handler
|
|
993
|
+
* that runs an EXPENSIVE reduction (the slice-3 arm-B in-turn compaction is the standing case)
|
|
994
|
+
* must honor it: a turn interrupt (design/373) fires this signal, and a reduction that only
|
|
995
|
+
* watches the RUN-level signal would make the interrupt wait out a long summary call and commit
|
|
996
|
+
* a session mutation the interrupted turn no longer needs (adversarial review r3).
|
|
997
|
+
*/
|
|
998
|
+
signal?: AbortSignal;
|
|
999
|
+
/**
|
|
1000
|
+
* design/374 slice 3 — set on the ONE bounded re-dispatch of the adopt seam
|
|
1001
|
+
* ({@link ContextResult.adoptSessionRebuild}): `messages` are the just-adopted session rebuild,
|
|
1002
|
+
* re-presented so the handler's request pipeline applies to what the provider will actually
|
|
1003
|
+
* receive (the "recheck" step of build→reduce→adopt→recheck). A handler must not request a
|
|
1004
|
+
* second adoption on this dispatch — the harness does not honor it (see the result member's
|
|
1005
|
+
* doc) — so a reduction that is still over budget takes the handler's own next arm (the trim
|
|
1006
|
+
* last resort) instead of looping. Absent on every ordinary dispatch.
|
|
1007
|
+
*/
|
|
1008
|
+
recheck?: boolean;
|
|
922
1009
|
}
|
|
923
1010
|
export interface TurnBoundaryEvent {
|
|
924
1011
|
/** Fires between model-request boundaries, after session flush, before the context rebuild. */
|
|
@@ -991,12 +1078,49 @@ export type AgentHarnessEvent<TSkill extends Skill = Skill, TPromptTemplate exte
|
|
|
991
1078
|
export interface BeforeAgentStartResult {
|
|
992
1079
|
/** Replacement messages for the prompt run. */
|
|
993
1080
|
messages?: AgentMessage[];
|
|
994
|
-
/**
|
|
1081
|
+
/**
|
|
1082
|
+
* Replacement system prompt for the prompt run — RUN-scoped, not turn-scoped: it is in force from
|
|
1083
|
+
* the first provider request until the run ends, across every turn boundary and every mid-request
|
|
1084
|
+
* context rebuild (the turn-boundary rebuild, the arm-B session adopt, the prompt-too-long
|
|
1085
|
+
* recovery retry). While it is in force it OUTRANKS the harness/session prompt, including a
|
|
1086
|
+
* prompt-epoch change committed mid-run: the epoch restatement lands in the session and governs
|
|
1087
|
+
* once no override is in force, it does not preempt the caller's replacement mid-run.
|
|
1088
|
+
*
|
|
1089
|
+
* The system prompt therefore has exactly three legal change points, and a deployment can rely on
|
|
1090
|
+
* it being byte-stable between them (the prefix-cache-friendly property):
|
|
1091
|
+
* 1. run start — this hook's return, or the harness/session prompt when it returns none;
|
|
1092
|
+
* 2. a compaction boundary that adopts a new prompt epoch — live for the NEXT run without an
|
|
1093
|
+
* override, and for the current run's remaining turns only if no override is in force;
|
|
1094
|
+
* 3. a session clear — the next run starts a fresh prompt.
|
|
1095
|
+
*
|
|
1096
|
+
* Returning a replacement also makes the run string-only on the prompt face: the block face
|
|
1097
|
+
* (`systemBlocks`) attaches only when it provably corresponds to the string being shipped, so an
|
|
1098
|
+
* override run ships no blocks on any of its turns.
|
|
1099
|
+
*/
|
|
995
1100
|
systemPrompt?: string;
|
|
996
1101
|
}
|
|
997
1102
|
/** Hook result for replacing the full context message list before provider conversion. */
|
|
998
1103
|
export interface ContextResult {
|
|
999
1104
|
messages: AgentMessage[];
|
|
1105
|
+
/**
|
|
1106
|
+
* design/374 slice 3 (arm B, the adopt seam) — the handler PERSISTED a session-level reduction
|
|
1107
|
+
* during this dispatch (e.g. an in-turn forced compaction appended to the session) and asks the
|
|
1108
|
+
* harness to adopt it MID-BUILD, the same three-step form as the two existing adoption flows
|
|
1109
|
+
* (turn_boundary / prompt-too-long recovery): flush pending session writes, rebuild the turn
|
|
1110
|
+
* context from the session (`createTurnState`), adopt it (`setTurnState` + the loop-context
|
|
1111
|
+
* adoption via {@link import("../loop/types.js").TransformedContext}), then re-dispatch the
|
|
1112
|
+
* context hook ONCE on the rebuilt view (`ContextEvent.recheck`) so the request pipeline applies
|
|
1113
|
+
* to it. Result: the provider request, every subsequent hook dispatch, and the active turn state
|
|
1114
|
+
* all see the SAME reduced transcript — the three-party co-view the seam exists for. `messages`
|
|
1115
|
+
* on this arm are the handler's best unadopted view and are superseded by the rebuild.
|
|
1116
|
+
*
|
|
1117
|
+
* Honored AT MOST ONCE per request build: on the recheck dispatch this member is ignored (its
|
|
1118
|
+
* `messages` are used verbatim) — the bound that keeps a reduction that cannot get under budget
|
|
1119
|
+
* from re-entering forever. Typed on the hook RESULT deliberately (not a closure side channel):
|
|
1120
|
+
* the adoption is part of the hook's answer, and the one prior closure-ref transport in this
|
|
1121
|
+
* area (`requestLossyRef`) is a recorded bypass shape, not a precedent to grow.
|
|
1122
|
+
*/
|
|
1123
|
+
adoptSessionRebuild?: boolean;
|
|
1000
1124
|
}
|
|
1001
1125
|
/** Hook result for patching provider request options before payload construction. */
|
|
1002
1126
|
export interface BeforeProviderRequestResult {
|
|
@@ -256,6 +256,7 @@ async function runTurnPhases(state, signal, turnController, publishSeat, retract
|
|
|
256
256
|
await executor.settle();
|
|
257
257
|
}
|
|
258
258
|
raiseFatalCancellation(executor);
|
|
259
|
+
let interruptedInRecovery = false;
|
|
259
260
|
{
|
|
260
261
|
if (ptl) {
|
|
261
262
|
const detect = ptlDetect;
|
|
@@ -273,18 +274,29 @@ async function runTurnPhases(state, signal, turnController, publishSeat, retract
|
|
|
273
274
|
}
|
|
274
275
|
let replaced;
|
|
275
276
|
try {
|
|
276
|
-
replaced = await ptl.recover(state.context.messages, attempt);
|
|
277
|
+
replaced = await ptl.recover(state.context.messages, attempt, turnController.signal);
|
|
277
278
|
}
|
|
278
279
|
catch {
|
|
279
280
|
replaced = undefined;
|
|
280
281
|
}
|
|
281
282
|
if (!replaced) {
|
|
282
283
|
state.context.messages.push(message);
|
|
284
|
+
interruptedInRecovery = turnController.signal.aborted && !signal?.aborted;
|
|
283
285
|
break;
|
|
284
286
|
}
|
|
285
287
|
withhold?.discard();
|
|
286
288
|
withhold = withholdEnabled ? createPtlWithholdBuffer(emit, ptlDetect) : undefined;
|
|
287
|
-
|
|
289
|
+
if (Array.isArray(replaced)) {
|
|
290
|
+
state.context.messages = replaced;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
adoptRebuiltContext(state.context, replaced);
|
|
294
|
+
}
|
|
295
|
+
if (turnController.signal.aborted && !signal?.aborted) {
|
|
296
|
+
state.context.messages.push(message);
|
|
297
|
+
interruptedInRecovery = true;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
288
300
|
trace?.({ kind: "continue", reason: "reactive_compact_retry" });
|
|
289
301
|
executor = new StreamToolExecutor(state.context, state.config, turnController.signal, emit);
|
|
290
302
|
try {
|
|
@@ -297,11 +309,21 @@ async function runTurnPhases(state, signal, turnController, publishSeat, retract
|
|
|
297
309
|
}
|
|
298
310
|
}
|
|
299
311
|
}
|
|
312
|
+
const unrecoveredPtlFailureUnderInterrupt = ptl !== undefined &&
|
|
313
|
+
message.stopReason === "error" &&
|
|
314
|
+
ptlDetect(message) &&
|
|
315
|
+
isBlankFailureContent(message) &&
|
|
316
|
+
executor.admittedCount === 0;
|
|
317
|
+
const interruptOwnsTurn = (message.stopReason === "aborted" || interruptedInRecovery || unrecoveredPtlFailureUnderInterrupt) &&
|
|
318
|
+
turnController.signal.aborted &&
|
|
319
|
+
!signal?.aborted;
|
|
320
|
+
if (interruptOwnsTurn && message.stopReason === "error")
|
|
321
|
+
withhold?.discard();
|
|
300
322
|
await withhold?.flush();
|
|
301
323
|
state.newMessages.push(message);
|
|
302
324
|
if (message.staticReasoningCut === true)
|
|
303
325
|
state.staticReasoningCutRecoveries++;
|
|
304
|
-
if (
|
|
326
|
+
if (interruptOwnsTurn) {
|
|
305
327
|
retractSeat();
|
|
306
328
|
return await settleInterruptedTurn(state, message, executor, [], signal, emit);
|
|
307
329
|
}
|
|
@@ -425,6 +447,18 @@ async function runTurnPhases(state, signal, turnController, publishSeat, retract
|
|
|
425
447
|
}
|
|
426
448
|
return { kind: "ran" };
|
|
427
449
|
}
|
|
450
|
+
function adoptRebuiltContext(context, adopted) {
|
|
451
|
+
context.messages = adopted.messages;
|
|
452
|
+
if (adopted.systemPrompt === undefined)
|
|
453
|
+
return;
|
|
454
|
+
context.systemPrompt = adopted.systemPrompt;
|
|
455
|
+
if (adopted.systemBlocks !== undefined) {
|
|
456
|
+
context.systemBlocks = adopted.systemBlocks;
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
delete context.systemBlocks;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
428
462
|
function adoptNextTurnSnapshot(state, nextTurnSnapshot) {
|
|
429
463
|
if (!nextTurnSnapshot)
|
|
430
464
|
return;
|
|
@@ -524,7 +558,16 @@ async function settleInterruptedTurn(state, message, executor, committedResults,
|
|
|
524
558
|
async function streamAssistantResponse(context, config, signal, emit, streamFn, runtime, executor, staticReasoningCutDowngrade) {
|
|
525
559
|
let messages = context.messages;
|
|
526
560
|
if (config.transformContext) {
|
|
527
|
-
|
|
561
|
+
const transformed = await config.transformContext(messages, signal);
|
|
562
|
+
if (Array.isArray(transformed)) {
|
|
563
|
+
messages = transformed;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
if (transformed.adoptedContext !== undefined) {
|
|
567
|
+
adoptRebuiltContext(context, transformed.adoptedContext);
|
|
568
|
+
}
|
|
569
|
+
messages = transformed.messages;
|
|
570
|
+
}
|
|
528
571
|
}
|
|
529
572
|
const llmMessages = await config.convertToLlm(messages);
|
|
530
573
|
const llmContext = {
|