@sema-agent/core 5.63.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 +56 -0
- package/dist/agents/cascade.d.ts +5 -1
- package/dist/agents/cascade.js +6 -1
- package/dist/agents/subagent.js +1 -0
- package/dist/agents/verify.d.ts +5 -1
- package/dist/agents/verify.js +5 -2
- package/dist/core/checkpoint-store.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.d.ts +21 -0
- package/dist/core/fs-write-gate-policy.js +14 -3
- package/dist/core/remote-env.d.ts +34 -2
- package/dist/core/runner/prepare-task.d.ts +20 -0
- package/dist/core/runner/prepare-task.js +12 -3
- package/dist/core/runner/prepare-workspace-restore.js +13 -0
- package/dist/core/runner/runtask.js +23 -17
- package/dist/core/types.d.ts +13 -3
- 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 +9 -4
- package/dist/engine/harness/types.d.ts +89 -3
- package/dist/engine/loop/agent-loop.js +39 -15
- package/dist/engine/loop/types.d.ts +43 -22
- package/dist/index.d.ts +1 -1
- 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.d.ts +6 -0
- package/dist/orchestration/workflow.js +1 -0
- 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 +5 -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). */
|
|
@@ -610,7 +610,7 @@ export class AgentHarness {
|
|
|
610
610
|
const nextTurnState = await this.createTurnState();
|
|
611
611
|
setTurnState(nextTurnState);
|
|
612
612
|
return {
|
|
613
|
-
context: this.createContext(nextTurnState),
|
|
613
|
+
context: this.createContext(nextTurnState, runPromptOverride),
|
|
614
614
|
model: nextTurnState.model,
|
|
615
615
|
thinkingLevel: nextTurnState.thinkingLevel,
|
|
616
616
|
};
|
|
@@ -648,10 +648,10 @@ export class AgentHarness {
|
|
|
648
648
|
detect: ptl.detect,
|
|
649
649
|
maxRetries: ptl.maxRetries,
|
|
650
650
|
withholdErrorEvents: ptl.withholdErrorEvents,
|
|
651
|
-
recover: async (_messages, attempt) => {
|
|
651
|
+
recover: async (_messages, attempt, turnSignal) => {
|
|
652
652
|
let shrank = false;
|
|
653
653
|
try {
|
|
654
|
-
shrank = await ptl.recover(attempt);
|
|
654
|
+
shrank = await ptl.recover(attempt, turnSignal);
|
|
655
655
|
}
|
|
656
656
|
catch {
|
|
657
657
|
shrank = false;
|
|
@@ -660,7 +660,12 @@ export class AgentHarness {
|
|
|
660
660
|
return undefined;
|
|
661
661
|
const nextTurnState = await self.createTurnState();
|
|
662
662
|
setTurnState(nextTurnState);
|
|
663
|
-
|
|
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
|
+
};
|
|
664
669
|
},
|
|
665
670
|
};
|
|
666
671
|
},
|
|
@@ -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. */
|
|
@@ -1010,7 +1078,25 @@ export type AgentHarnessEvent<TSkill extends Skill = Skill, TPromptTemplate exte
|
|
|
1010
1078
|
export interface BeforeAgentStartResult {
|
|
1011
1079
|
/** Replacement messages for the prompt run. */
|
|
1012
1080
|
messages?: AgentMessage[];
|
|
1013
|
-
/**
|
|
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
|
+
*/
|
|
1014
1100
|
systemPrompt?: string;
|
|
1015
1101
|
}
|
|
1016
1102
|
/** Hook result for replacing the full context message list before provider conversion. */
|
|
@@ -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;
|
|
@@ -529,18 +563,8 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn,
|
|
|
529
563
|
messages = transformed;
|
|
530
564
|
}
|
|
531
565
|
else {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
context.messages = adopted.messages;
|
|
535
|
-
if (adopted.systemPrompt !== undefined) {
|
|
536
|
-
context.systemPrompt = adopted.systemPrompt;
|
|
537
|
-
if (adopted.systemBlocks !== undefined) {
|
|
538
|
-
context.systemBlocks = adopted.systemBlocks;
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
delete context.systemBlocks;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
566
|
+
if (transformed.adoptedContext !== undefined) {
|
|
567
|
+
adoptRebuiltContext(context, transformed.adoptedContext);
|
|
544
568
|
}
|
|
545
569
|
messages = transformed.messages;
|
|
546
570
|
}
|
|
@@ -159,8 +159,22 @@ export interface LoopPromptTooLongRecovery {
|
|
|
159
159
|
* Produce a replacement transcript to retry the provider request with (attempt starts at 1).
|
|
160
160
|
* Return undefined to give up — the loop then surfaces the original error unchanged.
|
|
161
161
|
* Contract: must not throw or reject.
|
|
162
|
+
*
|
|
163
|
+
* The return may be a bare transcript (the historic form — messages only) or an
|
|
164
|
+
* {@link AdoptedLoopContext}, which additionally adopts the rebuilt SYSTEM PROMPT for the retry:
|
|
165
|
+
* the recovery pass can commit a prompt-epoch change (a compaction-boundary center-prompt
|
|
166
|
+
* adoption/rollback rides the very pass this lane runs), and a retry built from the rebuilt
|
|
167
|
+
* transcript under the PRE-epoch prompt ships one request under the policy the session just
|
|
168
|
+
* superseded. Same adoption rule as the transform seam's `adoptedContext` — one helper, both
|
|
169
|
+
* lanes.
|
|
170
|
+
*
|
|
171
|
+
* `turnSignal` is the TURN-scoped abort (design/373 S1): a turn interrupt fired while the policy
|
|
172
|
+
* runs, and a policy whose work is expensive (a forced compaction's summary call) must ride it so
|
|
173
|
+
* the interjection is served now instead of after the whole pass. A policy that DECLINES while
|
|
174
|
+
* this signal is aborted does not leave the run holding the provider error: the loop settles the
|
|
175
|
+
* turn as an interrupted one and continues.
|
|
162
176
|
*/
|
|
163
|
-
recover: (messages: AgentMessage[], attempt: number) => Promise<AgentMessage[] | undefined>;
|
|
177
|
+
recover: (messages: AgentMessage[], attempt: number, turnSignal?: AbortSignal) => Promise<AgentMessage[] | AdoptedLoopContext | undefined>;
|
|
164
178
|
/** Override the prompt-too-long classifier. Default (design/374 slice 2): the TYPED cause first
|
|
165
179
|
* (`errorKind: "input_too_long"`, stamped by the brains from provenance-checked provider
|
|
166
180
|
* signals), then the conservative provider-message prose pattern as the fallback for brains
|
|
@@ -219,10 +233,10 @@ export interface LoopThinkingOnlyRecovery {
|
|
|
219
233
|
* this shape — `truncatedOutput`/`malformedToolUse`/`thinkingOnly` already reference the named types
|
|
220
234
|
* below by direct import (no drift risk); `degenerateOutput` (2 fields) is hand-copied there and is a
|
|
221
235
|
* candidate for a future `Pick`-style extraction; `promptTooLong` is
|
|
222
|
-
* INTENTIONALLY forked — the harness side is a session-level `recover(attempt)` reduction,
|
|
223
|
-
* side is `recover(messages, attempt)` and also carries `withholdErrorEvents`, so
|
|
224
|
-
* one type. (`agent-harness.ts` is outside this file's edit scope — its side of
|
|
225
|
-
* note is pending.) */
|
|
236
|
+
* INTENTIONALLY forked — the harness side is a session-level `recover(attempt, turnSignal)` reduction,
|
|
237
|
+
* this loop side is `recover(messages, attempt, turnSignal)` and also carries `withholdErrorEvents`, so
|
|
238
|
+
* the two can never share one type. (`agent-harness.ts` is outside this file's edit scope — its side of
|
|
239
|
+
* this mutual-reference note is pending.) */
|
|
226
240
|
/**
|
|
227
241
|
* design/374 slice 3 — the transform's ADOPTING return shape (see
|
|
228
242
|
* {@link AgentLoopConfig.transformContext}). Two arrays with different standings, on purpose:
|
|
@@ -239,23 +253,30 @@ export interface TransformedContext {
|
|
|
239
253
|
/** The wire view for THIS request (what `convertToLlm` receives). */
|
|
240
254
|
messages: AgentMessage[];
|
|
241
255
|
/** Present ⇒ adopt this rebuilt context as the loop's live state before streaming. */
|
|
242
|
-
adoptedContext?:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
adoptedContext?: AdoptedLoopContext;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A session-rebuilt context the loop adopts as its LIVE state. Minted by the two adoption seams —
|
|
260
|
+
* the transform's {@link TransformedContext.adoptedContext} (design/374 slice 3, arm B) and the ④b
|
|
261
|
+
* prompt-too-long {@link LoopPromptTooLongRecovery.recover} return (#474 ①) — and applied by ONE
|
|
262
|
+
* rule inside the loop, so the two lanes can never drift on what "adopt" means.
|
|
263
|
+
*/
|
|
264
|
+
export interface AdoptedLoopContext {
|
|
265
|
+
/** The rebuilt transcript the loop adopts as `state.context.messages`. */
|
|
266
|
+
messages: AgentMessage[];
|
|
267
|
+
/** When present, the rebuilt SYSTEM PROMPT is adopted too — and it applies to THIS very
|
|
268
|
+
* request (the loop reads `context.systemPrompt` after the transform / before the retry): an
|
|
269
|
+
* in-turn compaction can commit a prompt-epoch change (center-prompt adoption/rollback rides the
|
|
270
|
+
* compaction pass), and a request built from the rebuilt transcript under the PRE-epoch prompt
|
|
271
|
+
* would disagree with the epoch the session just recorded (adversarial review r1). `systemBlocks`
|
|
272
|
+
* must only ever accompany the prompt they byte-correspond to; when the prompt is adopted
|
|
273
|
+
* WITHOUT blocks, stale blocks are dropped (the string face is the truth source — same
|
|
274
|
+
* degrade-to-string posture as the harness's atomic-face guard). */
|
|
275
|
+
systemPrompt?: string;
|
|
276
|
+
systemBlocks?: Array<{
|
|
277
|
+
text: string;
|
|
278
|
+
cacheControlBoundary: boolean;
|
|
279
|
+
}>;
|
|
259
280
|
}
|
|
260
281
|
export interface LoopRecoveryOptions {
|
|
261
282
|
promptTooLong?: LoopPromptTooLongRecovery;
|
package/dist/index.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
|
|
|
75
75
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
76
76
|
export type { SecretEnvFinding, SecretEnvFindingKind } from "./core/secret-env.js";
|
|
77
77
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
78
|
-
export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode } from "./internal/harness.js";
|
|
78
|
+
export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode, WriteExpectation, WriteReceipt } from "./internal/harness.js";
|
|
79
79
|
export type { ExecResult } from "./internal/harness.js";
|
|
80
80
|
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
|
|
81
81
|
export { withRetry } from "./core/with-retry.js";
|
|
@@ -8,7 +8,7 @@ export type { CompactionPreparation, SummarizationClampDryRun } from "../engine/
|
|
|
8
8
|
export type { InvokedSkillRetention } from "../engine/compaction/utils.js";
|
|
9
9
|
export type { AgentCoreRuntimeDeps } from "../engine/loop/runtime-deps.js";
|
|
10
10
|
export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, } from "../engine/loop/types.js";
|
|
11
|
-
export type { AgentHarnessEvent, CompactionSettings, ExecutionEnv, ExecutionErrorCode, FileError, FileErrorCode, FileInfo, Result, Session, SessionMetadata, SessionRepo, SessionStorage, SessionTreeEntry, Skill, } from "../engine/harness/types.js";
|
|
11
|
+
export type { AgentHarnessEvent, CompactionSettings, ExecutionEnv, ExecutionErrorCode, FileError, FileErrorCode, FileInfo, Result, WriteExpectation, WriteReceipt, Session, SessionMetadata, SessionRepo, SessionStorage, SessionTreeEntry, Skill, } from "../engine/harness/types.js";
|
|
12
12
|
export type { ExecutionEnvExecOptions, ExecResult } from "../engine/harness/types.js";
|
|
13
13
|
export type { SessionWriteOptions, CompactionEntry } from "../engine/harness/types.js";
|
|
14
14
|
export type { ActiveWorktreeSession, WorkspaceState } from "../engine/harness/types.js";
|
|
@@ -186,6 +186,12 @@ export interface RunWorkflowToolDeps {
|
|
|
186
186
|
* root MUST ride deps; `ctx.rootSessionId` stays first for a deployment-composed mount that
|
|
187
187
|
* does get the enriched ctx. */
|
|
188
188
|
rootSessionId?: string;
|
|
189
|
+
/** design/380 O1② (C12) — the host run's EXPLICIT placement fixed point
|
|
190
|
+
* (`RunInternals.placementRoot`), riding deps for the same auto-mounted-ctx reason as
|
|
191
|
+
* `rootSessionId` above; threaded into the workflow run so every workflow-spawned agent keeps
|
|
192
|
+
* the ladder/gate placement. Absent ⇒ nothing extra travels (the rootSessionId chain is the
|
|
193
|
+
* placement root through prepare's mint middle segment). */
|
|
194
|
+
placementRoot?: string;
|
|
189
195
|
/** Process-local unified task registry. When present, RunWorkflow returns `task_id === runId` with a `w*` id. */
|
|
190
196
|
taskRegistry?: TaskRegistry;
|
|
191
197
|
/** design/115 P2 core slice: run-local task-notification sink for SDK event + live model XML injection.
|