@sema-agent/core 5.61.0 → 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 +67 -0
- 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 +2 -0
- package/dist/core/hooks.d.ts +83 -4
- package/dist/core/hooks.js +3 -3
- 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 +46 -7
- package/dist/core/runner/prepare-task.js +77 -42
- package/dist/core/runner/runtask.d.ts +7 -0
- package/dist/core/runner/runtask.js +198 -13
- 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/tool-errors.d.ts +2 -1
- package/dist/core/tool-policy.d.ts +27 -0
- package/dist/core/types.d.ts +138 -12
- 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/orchestration/run-workflow-tool.d.ts +2 -2
- 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/scheduler-tools.js +9 -1
- package/package.json +1 -1
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* backends equivalent by construction rather than by two hand-written copies that drift.
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
16
|
-
* One governed window.
|
|
17
|
-
* `maxTokens` the
|
|
18
|
-
* the wild:
|
|
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. `anchor` picks between the two window
|
|
18
|
+
* shapes that actually exist in the wild:
|
|
19
19
|
* - `"first-use"` — the window OPENS at the key's first charge and lasts `windowMs`; when it lapses with
|
|
20
20
|
* no further use, the next charge opens a fresh one. This is the "5 hours from when you started"
|
|
21
21
|
* shape: bursty use is admitted at full width, and an idle key is never penalized for old traffic.
|
|
@@ -29,6 +29,28 @@ export interface UsageWindow {
|
|
|
29
29
|
/** Tokens the window admits before it is exhausted. Must be finite and >= 0; `0` is a real, always-full
|
|
30
30
|
* window (an operator lock-out), not "unset". */
|
|
31
31
|
maxTokens: number;
|
|
32
|
+
/**
|
|
33
|
+
* OPTIONAL second ceiling: the MONEY the window admits before it is exhausted, in absolute USD — the
|
|
34
|
+
* 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 and >= 0; `0` is a real,
|
|
36
|
+
* always-full window, exactly as it is for `maxTokens`.
|
|
37
|
+
*
|
|
38
|
+
* The two ceilings are INDEPENDENT and both bind: a window is exhausted when EITHER its tokens reach
|
|
39
|
+
* `maxTokens` or its cost reaches `maxCostUsd` — whichever fills first. Absent (the default, and every
|
|
40
|
+
* pre-existing deployment) means THIS window governs tokens only — its own reading never asks for or
|
|
41
|
+
* enforces a cost. Whether cost is tracked/recorded at all is a DEPLOYMENT-level fact, not a
|
|
42
|
+
* per-window one (the `UsageSlot.costMicroUsd` / `UsageBucketRow` in-presence condition: "while the
|
|
43
|
+
* deployment governs at least one `maxCostUsd` window"): in a MIXED declaration the shared ledger
|
|
44
|
+
* rows carry cost under every window's bucket, and only a deployment with NO $ window anywhere keeps
|
|
45
|
+
* its rows byte-identical to the pre-cost-arm shape.
|
|
46
|
+
*
|
|
47
|
+
* **Requires a PRICED run.** Cost is not a number the engine can invent: a run whose model has neither a
|
|
48
|
+
* `RunnerDeps.pricing` entry nor a `Model.cost` declaration produces NO cost figure at all (not a zero),
|
|
49
|
+
* so a $ ceiling over it is unevaluable and the task is refused at the door
|
|
50
|
+
* (`config.usage_window_unpriced`) rather than charged a fabricated 0 that would let the ceiling
|
|
51
|
+
* silently stop applying.
|
|
52
|
+
*/
|
|
53
|
+
maxCostUsd?: number;
|
|
32
54
|
/** Which of the two window shapes above this is. */
|
|
33
55
|
anchor: "first-use" | "rolling";
|
|
34
56
|
}
|
|
@@ -36,6 +58,19 @@ export interface UsageWindow {
|
|
|
36
58
|
export interface UsageSlot {
|
|
37
59
|
at: number;
|
|
38
60
|
tokens: number;
|
|
61
|
+
/** The charge's cost in integer micro-USD. **In-presence condition:** written only while the deployment
|
|
62
|
+
* governs at least one {@link UsageWindow.maxCostUsd} window AND the charge had a cost figure at all — a
|
|
63
|
+
* token-only deployment's ledger rows are byte-for-byte what they were before the cost arm existed, and
|
|
64
|
+
* so is the row for spend nothing could price. Absent therefore reads as "no money recorded here": a row
|
|
65
|
+
* written BEFORE a $ window was declared, or one whose model had no price table, contributes 0 to the
|
|
66
|
+
* money lane rather than back-dating a number nobody recorded. */
|
|
67
|
+
costMicroUsd?: number;
|
|
68
|
+
/** This charge's spend could NOT be priced (see {@link UsageWindowStore.charge}'s `null` arm).
|
|
69
|
+
* **In-presence condition:** only ever `true`, only on a row written while a $ window was governed and
|
|
70
|
+
* nothing could price the spend. It is what keeps "no money here" apart from "no money spent here": a
|
|
71
|
+
* window holding one of these reports a money figure that is a LOWER BOUND, and says so
|
|
72
|
+
* ({@link UsageWindowReading.costUnknown}). */
|
|
73
|
+
costUnknown?: true;
|
|
39
74
|
}
|
|
40
75
|
/** The OPEN first-use window for one `windowMs` on one key. Rows for other widths coexist, so a
|
|
41
76
|
* deployment governing 5h and 7d windows keeps one row per width rather than one blended counter. */
|
|
@@ -46,6 +81,11 @@ export interface UsageBucketRow {
|
|
|
46
81
|
openedAt: number;
|
|
47
82
|
/** Tokens charged into it since it opened. */
|
|
48
83
|
tokens: number;
|
|
84
|
+
/** Integer micro-USD charged into it since it opened. Same in-presence condition as {@link UsageSlot.costMicroUsd}. */
|
|
85
|
+
costMicroUsd?: number;
|
|
86
|
+
/** Some spend in this open window could not be priced. Same meaning and in-presence condition as
|
|
87
|
+
* {@link UsageSlot.costUnknown}; sticky for the life of the open bucket. */
|
|
88
|
+
costUnknown?: true;
|
|
49
89
|
}
|
|
50
90
|
/** Everything one key's governance state consists of: the rolling lane's slots and the first-use lane's
|
|
51
91
|
* open buckets. Persisted verbatim by every backend (plain JSON, no methods). */
|
|
@@ -59,7 +99,20 @@ export interface UsageWindowReading {
|
|
|
59
99
|
window: UsageWindow;
|
|
60
100
|
/** Tokens the window currently holds. */
|
|
61
101
|
tokens: number;
|
|
62
|
-
/**
|
|
102
|
+
/** Integer micro-USD the window currently holds. **In-presence condition:** present exactly when
|
|
103
|
+
* `window.maxCostUsd !== undefined` — a reading for a token-only window is byte-identical to what it
|
|
104
|
+
* was before the cost arm existed, and a MISSING value on a $ window means the store did not evaluate
|
|
105
|
+
* the ceiling at all (refused loudly by {@link usageRetryAfterMs}, never read as an open ceiling). */
|
|
106
|
+
costMicroUsd?: number;
|
|
107
|
+
/** The window holds spend nothing could price, so `costMicroUsd` is a LOWER BOUND rather than the total.
|
|
108
|
+
* **In-presence condition:** only ever `true`, and only on a window that declares `maxCostUsd`. The
|
|
109
|
+
* ceiling still binds on what IS countable (an under-count can only postpone exhaustion, never invent
|
|
110
|
+
* it), and the engine announces the gap on every run that reads it — the alternative, treating the
|
|
111
|
+
* whole window as exhausted until the unpriced charge ages out, would lock a key out for as long as
|
|
112
|
+
* `windowMs` over one degraded turn, which is a larger outage than the gap it answers. */
|
|
113
|
+
costUnknown?: true;
|
|
114
|
+
/** The window admits no further work: `tokens >= window.maxTokens`, OR (when the window declares one)
|
|
115
|
+
* `costMicroUsd >= window.maxCostUsd` — the two ceilings are independent and either one binds. */
|
|
63
116
|
exhausted: boolean;
|
|
64
117
|
/** Ms until this window next frees capacity: for `first-use`, when the open window lapses; for
|
|
65
118
|
* `rolling`, when its OLDEST in-window slot ages out (which frees that slot's tokens, not
|
|
@@ -82,8 +135,19 @@ export interface UsageWindowStore {
|
|
|
82
135
|
* needs it to know which first-use buckets to keep open and how far back the rolling lane must
|
|
83
136
|
* remember, so the ledger stays bounded instead of growing for the life of the deployment.
|
|
84
137
|
* Called once per accounting point; never with a negative or non-finite `tokens`.
|
|
138
|
+
*
|
|
139
|
+
* `costMicroUsd` is the same charge's MONEY, in integer micro-USD, or `null` when the spend is real but
|
|
140
|
+
* NOTHING could price it. **In-presence condition:** one of the two is supplied exactly when the
|
|
141
|
+
* deployment governs at least one {@link UsageWindow.maxCostUsd} window — a token-only deployment is
|
|
142
|
+
* called with the pre-cost-arm argument list and must behave identically.
|
|
143
|
+
*
|
|
144
|
+
* A store that governs $ windows MUST FORWARD it (into `chargeUsageRecord`, or into its own equivalent)
|
|
145
|
+
* and answer {@link UsageWindowReading.costMicroUsd} from what it persisted. A decorator written against
|
|
146
|
+
* the pre-cost-arm signature drops the argument silently — which is why the arithmetic refuses an absent
|
|
147
|
+
* cost outright (`usage_window.store_cost_unanswered`) instead of folding it to a 0 that would leave the
|
|
148
|
+
* money ceiling open forever with nothing able to notice.
|
|
85
149
|
*/
|
|
86
|
-
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[]): Promise<void>;
|
|
150
|
+
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[], costMicroUsd?: number | null): Promise<void>;
|
|
87
151
|
/** Read `key`'s state for each window as of `now`, in the order the windows were given. */
|
|
88
152
|
read(key: string, windows: readonly UsageWindow[], now: number): Promise<readonly UsageWindowReading[]>;
|
|
89
153
|
}
|
|
@@ -99,6 +163,10 @@ export declare const EMPTY_USAGE_WINDOW_RECORD: UsageWindowRecord;
|
|
|
99
163
|
* would govern a deployment by a number nobody chose. Refuses with `config.usage_window_invalid`.
|
|
100
164
|
*/
|
|
101
165
|
export declare function resolveUsageWindows(windows: readonly UsageWindow[] | undefined): readonly UsageWindow[] | undefined;
|
|
166
|
+
/** Does the deployment govern MONEY at all? The one predicate that decides whether a cost figure is asked
|
|
167
|
+
* of the engine, written into the ledger, or answered in a reading — so a token-only deployment stays
|
|
168
|
+
* byte-for-byte what it was before the cost arm existed. */
|
|
169
|
+
export declare function windowsGovernCost(windows: readonly UsageWindow[]): boolean;
|
|
102
170
|
/**
|
|
103
171
|
* Fold one charge into a key's record and return the NEW record (pure — the input is never mutated, so a
|
|
104
172
|
* store that keeps records in a Map cannot be corrupted by a half-applied charge).
|
|
@@ -107,8 +175,23 @@ export declare function resolveUsageWindows(windows: readonly UsageWindow[] | un
|
|
|
107
175
|
* can never be read again, and a first-use bucket for a width the deployment no longer governs can never
|
|
108
176
|
* be read again either, so both are dropped here. A record therefore stays bounded by (charges within the
|
|
109
177
|
* widest rolling window) + (one row per governed first-use width).
|
|
178
|
+
*
|
|
179
|
+
* `costMicroUsd` rides the same fold as `tokens` (integer micro-USD, never a float USD: the ledger sums
|
|
180
|
+
* thousands of charges and a float would accumulate error into a money ceiling), and it is a THREE-state
|
|
181
|
+
* argument while the deployment governs a $ window:
|
|
182
|
+
* - a NUMBER — the charge's cost, folded into the money lane;
|
|
183
|
+
* - `null` — the tokens are known but their cost is NOT (an unpriced model served the spend). The row is
|
|
184
|
+
* written with its tokens and NO cost field: the token ceiling stays exact, and the money lane counts
|
|
185
|
+
* nothing rather than a fabricated 0. The engine refuses such a run anyway — this keeps the ledger
|
|
186
|
+
* honest about the spend that already happened before the refusal;
|
|
187
|
+
* - `undefined` — REFUSED. Core always supplies one of the two above when a $ window is governed, so an
|
|
188
|
+
* absent argument means some store in the chain (a pre-cost-arm decorator, most likely) dropped it, and
|
|
189
|
+
* folding it to 0 would leave the money ceiling permanently open with nothing able to detect it.
|
|
190
|
+
* With NO $ window governed the argument is ignored entirely and rows keep their pre-cost-arm bytes.
|
|
191
|
+
* Rows that already carry a cost keep it, so removing and re-adding a $ window neither loses the history
|
|
192
|
+
* nor rewrites rows that predate it.
|
|
110
193
|
*/
|
|
111
|
-
export declare function chargeUsageRecord(record: UsageWindowRecord, tokens: number, at: number, windows: readonly UsageWindow[]): UsageWindowRecord;
|
|
194
|
+
export declare function chargeUsageRecord(record: UsageWindowRecord, tokens: number, at: number, windows: readonly UsageWindow[], costMicroUsd?: number | null): UsageWindowRecord;
|
|
112
195
|
/**
|
|
113
196
|
* Read a key's record against the governed windows as of `now` (pure). The `first-use` lane reads the open
|
|
114
197
|
* bucket; the `rolling` lane sums the slots inside `(now − windowMs, now]`.
|
|
@@ -119,8 +202,26 @@ export declare function readUsageRecord(record: UsageWindowRecord, windows: read
|
|
|
119
202
|
* window would admit work again. `undefined` when nothing is exhausted. The MAXIMUM (not the minimum) of
|
|
120
203
|
* the exhausted windows' hints — resuming when the shortest one clears would immediately re-suspend on
|
|
121
204
|
* the longer one.
|
|
205
|
+
*
|
|
206
|
+
* It is also the ONE choke point every governance read in the engine goes through, so the integrity
|
|
207
|
+
* checks live here, in two arms:
|
|
208
|
+
* - **coverage** (when the caller supplies the declared `windows`): one reading per declared window,
|
|
209
|
+
* in the order the windows were given, with the window echoed verbatim on the fields that name the
|
|
210
|
+
* allowance (`windowMs`/`anchor`/`maxTokens`/`maxCostUsd`). The per-reading arm below is
|
|
211
|
+
* structurally blind to a reading that is MISSING — a decorator store that maps only the windows
|
|
212
|
+
* it recognizes, reorders the answer, or echoes a normalized copy with `maxCostUsd` stripped would
|
|
213
|
+
* otherwise un-govern a declared ceiling with zero symptoms. Every engine read passes its windows
|
|
214
|
+
* through; the parameter stays optional so a bare-readings caller keeps the pre-coverage face.
|
|
215
|
+
* - **per-reading**: a reading for a window that DECLARES `maxCostUsd` while carrying no
|
|
216
|
+
* `costMicroUsd` means the store never evaluated the money ceiling — its `exhausted` verdict then
|
|
217
|
+
* covers the token axis alone.
|
|
218
|
+
* Both arms refuse with ONE code (`usage_window.store_cost_unanswered`) rather than read as an open
|
|
219
|
+
* ceiling — an unanswered window and an answered-without-cost window are the same fact ("the store
|
|
220
|
+
* never proved it evaluated the declared ceiling"): a governance ceiling that silently stops applying
|
|
221
|
+
* is worse than one that never existed, which is the same posture the file backend takes when its
|
|
222
|
+
* ledger is unreadable.
|
|
122
223
|
*/
|
|
123
|
-
export declare function usageRetryAfterMs(readings: readonly UsageWindowReading[]): number | undefined;
|
|
224
|
+
export declare function usageRetryAfterMs(readings: readonly UsageWindowReading[], windows?: readonly UsageWindow[]): number | undefined;
|
|
124
225
|
/**
|
|
125
226
|
* Process-local {@link UsageWindowStore} reference implementation. Governs correctly within ONE Runner
|
|
126
227
|
* process and loses its ledger on restart — the right choice for tests and single-process deployments,
|
|
@@ -129,6 +230,6 @@ export declare function usageRetryAfterMs(readings: readonly UsageWindowReading[
|
|
|
129
230
|
*/
|
|
130
231
|
export declare class InMemoryUsageWindowStore implements UsageWindowStore {
|
|
131
232
|
private readonly records;
|
|
132
|
-
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[]): Promise<void>;
|
|
233
|
+
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[], costMicroUsd?: number | null): Promise<void>;
|
|
133
234
|
read(key: string, windows: readonly UsageWindow[], now: number): Promise<readonly UsageWindowReading[]>;
|
|
134
235
|
}
|
|
@@ -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);
|
|
@@ -278,8 +278,8 @@ export interface RunWorkflowToolDeps {
|
|
|
278
278
|
* (`RunnerDeps.onAsk`, else the fail-closed headless auto-deny). */
|
|
279
279
|
parentOnAsk?: import("../core/tool-policy.js").OnAsk;
|
|
280
280
|
/** The HOST run's display sink (its `RunInternals.onForwardEvent` behind the runner's ctx wrapper:
|
|
281
|
-
* `task_progress` always, plus the children's content events — `text_delta`/`
|
|
282
|
-
* `tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
|
|
281
|
+
* `task_progress` always, plus the children's content events — `text_delta`/`text_end`/
|
|
282
|
+
* `reasoning_delta`/`tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
|
|
283
283
|
* `forwardSubagentEvents: true`) — threaded via `startWorkflow` into every spawned agent's trusted
|
|
284
284
|
* internals so a workflow child's events bubble to the deployment's one sink, the same
|
|
285
285
|
* channel a `createSubagentTool` delegation threads. Display-only; absent ⇒ ticks stay in each
|
|
@@ -384,8 +384,8 @@ export interface RunWorkflowOptions {
|
|
|
384
384
|
parentCenterArtifactDigest?: string;
|
|
385
385
|
parentCenterSourceRevision?: string;
|
|
386
386
|
/** The launching run's display sink (`RunInternals.onForwardEvent` behind the runner's ctx wrapper:
|
|
387
|
-
* `task_progress` always, PLUS the children's content events — `text_delta`/`
|
|
388
|
-
* `tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
|
|
387
|
+
* `task_progress` always, PLUS the children's content events — `text_delta`/`text_end`/
|
|
388
|
+
* `reasoning_delta`/`tool_start`/`tool_end`, UNTRUSTED-RAW: the consumer must redact — when the HOST spec set
|
|
389
389
|
* `forwardSubagentEvents: true`) — threaded into every spawned agent's trusted internals so the
|
|
390
390
|
* children's events bubble out of their isolated streams to the deployment's one sink (fleet
|
|
391
391
|
* footer/monitor rows). Display-only; absent ⇒ ticks stay in each child's own stream. */
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export const EVENT_PROMPT_REGISTRY = new Map([
|
|
2
2
|
{ kind: "todo_reminder", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#TODO_REMINDER_BASE" },
|
|
3
3
|
{ kind: "task_reminder", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#TASK_REMINDER_BASE" },
|
|
4
|
+
{ kind: "tool_search_usage_reminder", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderToolSearchUsageReminder" },
|
|
4
5
|
{ kind: "changed_files", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderChangedFiles" },
|
|
5
6
|
{ kind: "plan_mode", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#PLAN_MODE_FULL_BODY" },
|
|
6
7
|
{ kind: "date_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "always", rendererRef: "turn-attachments.ts#renderDateChange" },
|
|
7
8
|
{ kind: "instructions_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", maxBytes: 512, defaultPolicy: "always", rendererRef: "turn-attachments.ts#collectInstructionsChange" },
|
|
8
9
|
{ kind: "workflow_size_guideline_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "always", rendererRef: "runtask.ts#workflowSizeGuidelineChangeNotice" },
|
|
9
10
|
{ kind: "budget_usd", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderBudgetUsd" },
|
|
11
|
+
{ kind: "total_tokens_reminder", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderTotalTokensReminder" },
|
|
10
12
|
{ kind: "background_tasks", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderBackgroundTasks" },
|
|
11
13
|
{ kind: "tools_delta", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderToolsDelta" },
|
|
12
14
|
{ kind: "agent_listing", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderAgentListingDelta" },
|
package/dist/server/http.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface TaskServerOptions {
|
|
|
27
27
|
/**
|
|
28
28
|
* Create an HTTP server exposing the runner over two endpoints:
|
|
29
29
|
* POST /task → run to completion, returns TaskResult JSON
|
|
30
|
-
* POST /task/stream → Server-Sent Events of TaskEvent (text_delta / reasoning_delta / tool_* / done)
|
|
30
|
+
* POST /task/stream → Server-Sent Events of TaskEvent (text_delta / text_end / reasoning_delta / tool_* / done)
|
|
31
31
|
*
|
|
32
32
|
* The request body provides { objective, sessionId?, images? }; `resolveSpec` supplies the rest
|
|
33
33
|
* (model, tools, mcp, systemPrompt, limits) server-side.
|
|
@@ -22,6 +22,6 @@ export declare class FileUsageWindowStore implements UsageWindowStore {
|
|
|
22
22
|
* distinct principals can never share a ledger file). */
|
|
23
23
|
private pathFor;
|
|
24
24
|
private loadRecord;
|
|
25
|
-
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[]): Promise<void>;
|
|
25
|
+
charge(key: string, tokens: number, at: number, windows: readonly UsageWindow[], costMicroUsd?: number | null): Promise<void>;
|
|
26
26
|
read(key: string, windows: readonly UsageWindow[], now: number): Promise<readonly UsageWindowReading[]>;
|
|
27
27
|
}
|
|
@@ -38,8 +38,8 @@ export class FileUsageWindowStore {
|
|
|
38
38
|
const buckets = validateBuckets(rec.buckets, path);
|
|
39
39
|
return { slots, buckets };
|
|
40
40
|
}
|
|
41
|
-
async charge(key, tokens, at, windows) {
|
|
42
|
-
const next = chargeUsageRecord(this.loadRecord(key), tokens, at, windows);
|
|
41
|
+
async charge(key, tokens, at, windows, costMicroUsd) {
|
|
42
|
+
const next = chargeUsageRecord(this.loadRecord(key), tokens, at, windows, costMicroUsd);
|
|
43
43
|
atomicWriteFile(join(this.dir, "tmp"), this.pathFor(key), JSON.stringify(next));
|
|
44
44
|
}
|
|
45
45
|
async read(key, windows, now) {
|
|
@@ -59,11 +59,21 @@ function validateSlots(value, path) {
|
|
|
59
59
|
return value.map((s) => {
|
|
60
60
|
if (s === null || typeof s !== "object")
|
|
61
61
|
throw corrupt(path, "`slots` entry is not an object");
|
|
62
|
-
const { at, tokens } = s;
|
|
62
|
+
const { at, tokens, costMicroUsd, costUnknown } = s;
|
|
63
63
|
if (typeof at !== "number" || !Number.isFinite(at) || typeof tokens !== "number" || !Number.isFinite(tokens)) {
|
|
64
64
|
throw corrupt(path, "`slots` entry has a non-numeric `at`/`tokens`");
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
if (costMicroUsd !== undefined && (typeof costMicroUsd !== "number" || !Number.isFinite(costMicroUsd))) {
|
|
67
|
+
throw corrupt(path, "`slots` entry has a non-numeric `costMicroUsd`");
|
|
68
|
+
}
|
|
69
|
+
if (costUnknown !== undefined && costUnknown !== true)
|
|
70
|
+
throw corrupt(path, "`slots` entry has a `costUnknown` that is not `true`");
|
|
71
|
+
return {
|
|
72
|
+
at,
|
|
73
|
+
tokens,
|
|
74
|
+
...(costMicroUsd === undefined ? {} : { costMicroUsd }),
|
|
75
|
+
...(costUnknown === true ? { costUnknown: true } : {}),
|
|
76
|
+
};
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
79
|
function validateBuckets(value, path) {
|
|
@@ -74,7 +84,7 @@ function validateBuckets(value, path) {
|
|
|
74
84
|
return value.map((b) => {
|
|
75
85
|
if (b === null || typeof b !== "object")
|
|
76
86
|
throw corrupt(path, "`buckets` entry is not an object");
|
|
77
|
-
const { windowMs, openedAt, tokens } = b;
|
|
87
|
+
const { windowMs, openedAt, tokens, costMicroUsd, costUnknown } = b;
|
|
78
88
|
if (typeof windowMs !== "number" ||
|
|
79
89
|
!Number.isFinite(windowMs) ||
|
|
80
90
|
typeof openedAt !== "number" ||
|
|
@@ -83,6 +93,17 @@ function validateBuckets(value, path) {
|
|
|
83
93
|
!Number.isFinite(tokens)) {
|
|
84
94
|
throw corrupt(path, "`buckets` entry has a non-numeric `windowMs`/`openedAt`/`tokens`");
|
|
85
95
|
}
|
|
86
|
-
|
|
96
|
+
if (costMicroUsd !== undefined && (typeof costMicroUsd !== "number" || !Number.isFinite(costMicroUsd))) {
|
|
97
|
+
throw corrupt(path, "`buckets` entry has a non-numeric `costMicroUsd`");
|
|
98
|
+
}
|
|
99
|
+
if (costUnknown !== undefined && costUnknown !== true)
|
|
100
|
+
throw corrupt(path, "`buckets` entry has a `costUnknown` that is not `true`");
|
|
101
|
+
return {
|
|
102
|
+
windowMs,
|
|
103
|
+
openedAt,
|
|
104
|
+
tokens,
|
|
105
|
+
...(costMicroUsd === undefined ? {} : { costMicroUsd }),
|
|
106
|
+
...(costUnknown === true ? { costUnknown: true } : {}),
|
|
107
|
+
};
|
|
87
108
|
});
|
|
88
109
|
}
|
package/dist/tools/loop-tick.js
CHANGED
|
@@ -68,7 +68,7 @@ function dynamicTick(push) {
|
|
|
68
68
|
|
|
69
69
|
Run the autonomous check using the loop instructions established earlier in this conversation. If you cannot find them, treat this as a no-op tick.
|
|
70
70
|
|
|
71
|
-
You scheduled this tick via the ${SCHEDULE_WAKEUP_TOOL_NAME} tool (not a recurring cron). To keep the loop alive, call ${SCHEDULE_WAKEUP_TOOL_NAME} again at the end of this turn with \`prompt\` set to the literal sentinel \`${AUTONOMOUS_LOOP_DYNAMIC_SENTINEL}\` — otherwise the loop ends after this tick.${DYNAMIC_APPENDIX}${push}`;
|
|
71
|
+
You scheduled this tick via the ${SCHEDULE_WAKEUP_TOOL_NAME} tool (not a recurring cron). To keep the loop alive, call ${SCHEDULE_WAKEUP_TOOL_NAME} again at the end of this turn with \`prompt\` set to the literal sentinel \`${AUTONOMOUS_LOOP_DYNAMIC_SENTINEL}\` and \`noop\` set to \`true\` if this tick changed nothing (or \`false\` if it did) — otherwise the loop ends after this tick.${DYNAMIC_APPENDIX}${push}`;
|
|
72
72
|
}
|
|
73
73
|
export function resolveAutonomousLoopPrompt(prompt, opts) {
|
|
74
74
|
if (prompt !== AUTONOMOUS_LOOP_SENTINEL && prompt !== AUTONOMOUS_LOOP_DYNAMIC_SENTINEL)
|
|
@@ -13,6 +13,8 @@ Do NOT schedule a short-interval wakeup to poll for background work you started
|
|
|
13
13
|
|
|
14
14
|
Pass the same /loop prompt back via \`prompt\` each turn so the next firing repeats the task. For an autonomous /loop (no user prompt), pass the literal sentinel \`${AUTONOMOUS_LOOP_DYNAMIC_SENTINEL}\` as \`prompt\` instead — the runtime resolves it back to the autonomous-loop instructions at fire time. (There is a similar \`${AUTONOMOUS_LOOP_SENTINEL}\` sentinel for CronCreate-based autonomous loops; do not confuse the two — ${SCHEDULE_WAKEUP_TOOL_NAME} always uses the \`-dynamic\` variant.) To end the loop, call this tool with \`stop: true\` (omit every other field) — the loop ends immediately and no further wakeups fire.
|
|
15
15
|
|
|
16
|
+
Set \`noop: true\` if nothing changed — you checked and there's nothing to report ("no change", "still waiting", "quiet hold"). Set \`noop: false\` if something happened worth keeping — you edited a file, posted a message, advanced state, or surfaced a finding. Consecutive \`noop: true\` ticks are collapsed in the user's terminal view and tracked as a streak, so long quiet holds stay legible to the user without scrolling. Omit \`noop\` when stopping (\`stop: true\`).
|
|
17
|
+
|
|
16
18
|
## Picking delaySeconds
|
|
17
19
|
|
|
18
20
|
The provider prompt cache decides how expensive a wake-up is: waking inside the cache TTL re-reads your conversation context cached (fast, cheap); waking past it re-reads everything uncached. The TTL depends on the provider route this session uses — Anthropic-family routes default to about 5 minutes (1-hour optional), while some routes (e.g. DeepSeek) typically retain unused prefixes for hours, with no guaranteed TTL.
|
|
@@ -422,6 +424,9 @@ MOUNT NOTE: this host does not vouch for session-scoped scheduling, so a wakeup
|
|
|
422
424
|
stop: Type.Optional(Type.Boolean({
|
|
423
425
|
description: "Immediately end the dynamic loop: cancel this session's pending wakeup(s) and schedule nothing. All other fields are ignored when true.",
|
|
424
426
|
})),
|
|
427
|
+
noop: Type.Optional(Type.Boolean({
|
|
428
|
+
description: "true = nothing changed (you checked and there is nothing to report). false = something happened worth keeping (edited a file, posted a message, advanced state, surfaced a finding). Consecutive noop:true ticks are collapsed in the user's terminal view and tracked as a streak. Required unless `stop` is true.",
|
|
429
|
+
})),
|
|
425
430
|
}),
|
|
426
431
|
effect: "write",
|
|
427
432
|
execute: async (args) => serializedWakeupOp(async () => {
|
|
@@ -439,6 +444,9 @@ MOUNT NOTE: this host does not vouch for session-scoped scheduling, so a wakeup
|
|
|
439
444
|
if (a.delaySeconds === undefined || a.reason === undefined || a.prompt === undefined) {
|
|
440
445
|
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): delaySeconds, reason and prompt are required unless \`stop\` is true.`);
|
|
441
446
|
}
|
|
447
|
+
if (a.noop === undefined) {
|
|
448
|
+
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): \`noop\` is required unless \`stop\` is true — pass \`noop: true\` if this tick changed nothing, \`noop: false\` if something happened worth keeping.`);
|
|
449
|
+
}
|
|
442
450
|
if (sched.schedulerCapabilities.supportsSessionWakeup === false) {
|
|
443
451
|
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form.`);
|
|
444
452
|
}
|
|
@@ -468,7 +476,7 @@ MOUNT NOTE: this host does not vouch for session-scoped scheduling, so a wakeup
|
|
|
468
476
|
: " Note: this scheduler does not vouch for session-lifetime reap, so this wakeup is scheduled as a persistent one and may outlive the session — end the loop explicitly with `stop: true` rather than relying on the session ending.";
|
|
469
477
|
return {
|
|
470
478
|
content: `Next wakeup scheduled for ${hhmmss} (in ${clampedDelaySeconds}s)${clampNote}. Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.${reapNote}${cleanupNote}`,
|
|
471
|
-
details: { type: "schedule-wakeup", stopped: false, scheduledFor, clampedDelaySeconds, wasClamped, reason: a.reason },
|
|
479
|
+
details: { type: "schedule-wakeup", stopped: false, scheduledFor, clampedDelaySeconds, wasClamped, reason: a.reason, noop: a.noop },
|
|
472
480
|
};
|
|
473
481
|
}),
|
|
474
482
|
});
|