@coreplane/switchboard 1.251.0 → 1.252.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/config/config.example.yaml +4 -2
- package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
- package/dist/assets/deploy/cloudflare/worker.ts +6 -3
- package/dist/assets/deploy/cloudflare-memory/worker.ts +77 -12
- package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +3 -2
- package/dist/assets/project.json +13 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +5 -5
- package/dist/assets/src/core/budgets.ts +22 -0
- package/dist/assets/src/core/coordinator/contract.ts +42 -0
- package/dist/assets/src/core/coordinator/driver.ts +134 -10
- package/dist/assets/src/core/drain.ts +50 -0
- package/dist/assets/src/core/memory/engine.ts +98 -0
- package/dist/assets/src/core/memory/scorer.ts +12 -4
- package/dist/assets/src/core/memory/types.ts +69 -12
- package/dist/assets/src/core/modelCard.ts +32 -4
- package/dist/assets/src/core/modelPricing.ts +111 -1
- package/dist/assets/src/core/modelProxy/usage.ts +88 -0
- package/dist/assets/src/core/modelRegistry.ts +15 -1
- package/dist/assets/src/core/refusal.ts +4 -7
- package/dist/assets/src/core/reviewVerdict.ts +4 -0
- package/dist/assets/src/core/runEvents.ts +51 -2
- package/dist/assets/src/core/runFriction.ts +7 -2
- package/dist/assets/src/core/runLedger/types.ts +11 -0
- package/dist/assets/src/core/runUsage.ts +67 -13
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +41 -14
- package/dist/assets/src/core/ship/coordinator.ts +380 -53
- package/dist/assets/src/core/ship/renewal.ts +10 -5
- package/dist/assets/src/core/trace/attrs.ts +24 -0
- package/dist/assets/src/core/types.ts +5 -5
- package/dist/assets/src/core/verbosity.ts +48 -0
- package/dist/assets/src/deploy/liveGate.ts +40 -13
- package/dist/assets/src/deploy/restart.ts +11 -12
- package/dist/assets/src/execution/residentDepCache.ts +50 -1
- package/dist/assets/src/execution/residentDepsStore.ts +40 -2
- package/dist/assets/src/execution/residentRefresh.ts +55 -3
- package/dist/assets/src/execution/residentSteps.ts +4 -0
- package/dist/assets/src/execution/sandboxErrors.ts +8 -0
- package/dist/assets/web/dist/.vite/manifest.json +55 -55
- package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-3ELQWM0r.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-BG_ok-K2.js +2 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-ChCQOLgZ.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-C9y3nbo8.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-i1RG9e7g.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-9klVWhSF.js → RunRoutePage-B3IirUVi.js} +4 -4
- package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-DvYwM2TE.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-Bo6yCyXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-CAfS1AUi.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-tZoum_T-.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-BmdOHwNn.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
- package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-DfbEpHXR.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +1 -0
- package/dist/assets/web/dist/assets/{main-Dkcbtu3u.js → main-5Gm_1Gv8.js} +2 -2
- package/dist/assets/web/dist/assets/sseReplay-DmyMXfRC.js +11 -0
- package/dist/cli.js +2470 -902
- package/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
- package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
- package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { parseModelRef } from "./provider.js";
|
|
1
|
+
import { parseModelRef, type TokenUsage } from "./provider.js";
|
|
2
|
+
import type { CardPrice } from "./modelCard.js";
|
|
3
|
+
import type { ReportedCost } from "./modelProxy/usage.js";
|
|
2
4
|
import type { ModelUsage, RunUsage } from "./runUsage.js";
|
|
3
5
|
|
|
4
6
|
// The price of a model's tokens (docs/reference/specs/costs.md): one table
|
|
@@ -148,6 +150,105 @@ export function modelPriceOf(ref: string, prices: ModelPriceTable = NO_PRICES):
|
|
|
148
150
|
return { input: list.input, output: list.output, cacheRead: list.cacheRead, cacheWrite: list.cacheWrite5m };
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
// ---- one turn, priced at the proxy (model-proxy.md item 6) -----------------------
|
|
154
|
+
|
|
155
|
+
/** Which layer priced the turn: the provider's own reported cost, the
|
|
156
|
+
* operator's table (`costs.prices` or a block's `models.<id>.price`), the
|
|
157
|
+
* registry card (the Anthropic family list folds into this layer), or none —
|
|
158
|
+
* an unpriced turn is never $0. */
|
|
159
|
+
export type PriceSource = "provider" | "operator" | "registry" | "none";
|
|
160
|
+
|
|
161
|
+
/** The meter row's dollars: `usd` when a layer priced the turn, `feeUsd` on a
|
|
162
|
+
* BYOK turn (the aggregator's fee — inside `usd` when the vendor's charge was
|
|
163
|
+
* reported beside it, otherwise beside the layer that priced the tokens),
|
|
164
|
+
* and the layer. */
|
|
165
|
+
export interface TurnPrice {
|
|
166
|
+
usd?: number;
|
|
167
|
+
feeUsd?: number;
|
|
168
|
+
priceSource: PriceSource;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** What `priceTurn` reads of the run's model card: the ref the spans name,
|
|
172
|
+
* the card's rate when a layer named one, and which layer did
|
|
173
|
+
* (`ModelCard.provenance.price`). */
|
|
174
|
+
export interface TurnPriceCard {
|
|
175
|
+
ref: string;
|
|
176
|
+
price?: CardPrice;
|
|
177
|
+
pricedBy?: "operator" | "registry" | "wire";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const turnTokensUsd = (u: TokenUsage, p: { input: number; output: number; cacheRead: number; cacheWrite: number }) =>
|
|
181
|
+
(u.inputTokens * p.input +
|
|
182
|
+
u.outputTokens * p.output +
|
|
183
|
+
(u.cacheReadTokens ?? 0) * p.cacheRead +
|
|
184
|
+
(u.cacheWriteTokens ?? 0) * p.cacheWrite) /
|
|
185
|
+
1_000_000;
|
|
186
|
+
|
|
187
|
+
/** A card rate over one turn's counts, tiers by pi's rule (`calculateCost`):
|
|
188
|
+
* the input side is input + cache reads + cache writes, and the WHOLE request
|
|
189
|
+
* re-rates at the highest tier whose threshold it exceeds. */
|
|
190
|
+
export function cardPriceUsd(usage: TokenUsage, price: CardPrice): number {
|
|
191
|
+
const inputSide = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
192
|
+
let rates: { input: number; output: number; cacheRead: number; cacheWrite: number } = price;
|
|
193
|
+
let matched = -1;
|
|
194
|
+
for (const tier of price.tiers ?? []) {
|
|
195
|
+
if (inputSide > tier.inputTokensAbove && tier.inputTokensAbove > matched) {
|
|
196
|
+
rates = tier;
|
|
197
|
+
matched = tier.inputTokensAbove;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return turnTokensUsd(usage, rates);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* One turn's price, by record 0052's precedence: a provider-reported cost is
|
|
205
|
+
* stored as reported (`provider`; on BYOK the fee and the vendor's upstream
|
|
206
|
+
* charge sum into `usd` with `feeUsd` beside), else the operator's layer
|
|
207
|
+
* (`costs.prices` for the exact ref, or the card's operator-named rate), else
|
|
208
|
+
* the registry layer (the card's rate with its tiers, or the Anthropic family
|
|
209
|
+
* list — costs.md item 4b's fallback, folded here), else `none` — a turn no
|
|
210
|
+
* layer prices, and a stream broken before its usage arrived, stays unpriced.
|
|
211
|
+
*/
|
|
212
|
+
export function priceTurn(
|
|
213
|
+
card: TurnPriceCard,
|
|
214
|
+
reported: ReportedCost | undefined,
|
|
215
|
+
usage: TokenUsage | undefined,
|
|
216
|
+
prices: ModelPriceTable = NO_PRICES,
|
|
217
|
+
): TurnPrice {
|
|
218
|
+
if (reported) {
|
|
219
|
+
if (!reported.byok) return { usd: reported.cost, priceSource: "provider" };
|
|
220
|
+
if (reported.upstreamCost !== undefined) {
|
|
221
|
+
return { usd: reported.cost + reported.upstreamCost, priceSource: "provider", feeUsd: reported.cost };
|
|
222
|
+
}
|
|
223
|
+
// A BYOK chunk without the vendor's charge: the fee is not the turn's
|
|
224
|
+
// dollars, so it rides beside whatever layer below prices the tokens —
|
|
225
|
+
// never as an authoritative figure that understates the turn.
|
|
226
|
+
return { ...priceTurn(card, undefined, usage, prices), feeUsd: reported.cost };
|
|
227
|
+
}
|
|
228
|
+
if (!usage) return { priceSource: "none" };
|
|
229
|
+
const operator = prices[card.ref];
|
|
230
|
+
if (operator) return { usd: turnTokensUsd(usage, operator), priceSource: "operator" };
|
|
231
|
+
if (card.price) {
|
|
232
|
+
return {
|
|
233
|
+
usd: cardPriceUsd(usage, card.price),
|
|
234
|
+
priceSource: card.pricedBy === "operator" ? "operator" : "registry",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const list = anthropicPriceOf(modelIdOf(card.ref));
|
|
238
|
+
if (list) {
|
|
239
|
+
return {
|
|
240
|
+
usd: turnTokensUsd(usage, {
|
|
241
|
+
input: list.input,
|
|
242
|
+
output: list.output,
|
|
243
|
+
cacheRead: list.cacheRead,
|
|
244
|
+
cacheWrite: list.cacheWrite5m,
|
|
245
|
+
}),
|
|
246
|
+
priceSource: "registry",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { priceSource: "none" };
|
|
250
|
+
}
|
|
251
|
+
|
|
151
252
|
/** What a model's counted tokens cost at a price, USD. */
|
|
152
253
|
export function modelUsageUsd(m: ModelUsage, p: ModelPrice): number {
|
|
153
254
|
return (
|
|
@@ -211,6 +312,15 @@ export function llmUsdOfUsage(
|
|
|
211
312
|
let unpricedTokens = 0;
|
|
212
313
|
const byModel: Record<string, PricedModelUsage> = {};
|
|
213
314
|
for (const [ref, m] of Object.entries(usage.byModel)) {
|
|
315
|
+
// A model whose spans priced their own turns (model-proxy item 6) keeps
|
|
316
|
+
// the recorded figure — a provider-reported cost beats any table — and
|
|
317
|
+
// null (a turn without a figure) reads as unpriced, never re-priced here.
|
|
318
|
+
if (m.usd !== undefined) {
|
|
319
|
+
if (m.usd === null) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
|
|
320
|
+
else usd += m.usd;
|
|
321
|
+
byModel[ref] = { ...m, usd: m.usd };
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
214
324
|
const price = modelPriceOf(ref, prices);
|
|
215
325
|
const priced = price ? modelUsageUsd(m, price) : undefined;
|
|
216
326
|
if (priced === undefined) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// A provider's usage object as the proxy's meter reads it
|
|
2
|
+
// (docs/reference/specs/model-proxy.md item 6): the three wire shapes the proxy
|
|
3
|
+
// forwards — Anthropic's `message.usage`, the Chat Completions `usage` and the
|
|
4
|
+
// Responses API's `usage` — each
|
|
5
|
+
// normalized to the one `TokenUsage` every `model.turn` span carries. Undefined
|
|
6
|
+
// unless both core counts are numbers: a malformed or absent usage never fails
|
|
7
|
+
// a completion, it only leaves the turn unmetered. The first two readers were the
|
|
8
|
+
// native adapters' until record 0032's series deleted the adapters; the proxy
|
|
9
|
+
// is their one reader now.
|
|
10
|
+
|
|
11
|
+
import type { TokenUsage } from "../provider.js";
|
|
12
|
+
|
|
13
|
+
/** Anthropic `message.usage` → TokenUsage. */
|
|
14
|
+
export function usageFromAnthropic(u: unknown): TokenUsage | undefined {
|
|
15
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
16
|
+
const o = u as Record<string, unknown>;
|
|
17
|
+
if (typeof o.input_tokens !== "number" || typeof o.output_tokens !== "number") return undefined;
|
|
18
|
+
const usage: TokenUsage = { inputTokens: o.input_tokens, outputTokens: o.output_tokens };
|
|
19
|
+
if (typeof o.cache_read_input_tokens === "number") usage.cacheReadTokens = o.cache_read_input_tokens;
|
|
20
|
+
if (typeof o.cache_creation_input_tokens === "number") usage.cacheWriteTokens = o.cache_creation_input_tokens;
|
|
21
|
+
return usage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** OpenAI-style `usage` → TokenUsage. `prompt_tokens_details.cached_tokens` is
|
|
25
|
+
* the cache-read count where the server reports one. */
|
|
26
|
+
export function usageFromOpenAI(u: unknown): TokenUsage | undefined {
|
|
27
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
28
|
+
const o = u as Record<string, unknown>;
|
|
29
|
+
if (typeof o.prompt_tokens !== "number" || typeof o.completion_tokens !== "number") return undefined;
|
|
30
|
+
const usage: TokenUsage = { inputTokens: o.prompt_tokens, outputTokens: o.completion_tokens };
|
|
31
|
+
const details = o.prompt_tokens_details;
|
|
32
|
+
if (typeof details === "object" && details !== null) {
|
|
33
|
+
const cached = (details as Record<string, unknown>).cached_tokens;
|
|
34
|
+
if (typeof cached === "number") usage.cacheReadTokens = cached;
|
|
35
|
+
}
|
|
36
|
+
return usage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A provider-reported cost beside the counters (model-proxy.md item 6): the
|
|
40
|
+
* OpenRouter final chunk's `usage.cost` in USD — on a BYOK turn (`is_byok`)
|
|
41
|
+
* the aggregator's fee alone, with what the vendor billed the operator's own
|
|
42
|
+
* key in `cost_details.upstream_inference_cost`. Read off the same usage
|
|
43
|
+
* object the counters come from; a usage without a finite `cost` reports
|
|
44
|
+
* none (Anthropic's wire never carries one). */
|
|
45
|
+
export interface ReportedCost {
|
|
46
|
+
/** The figure as reported, stored as reported (record 0052). */
|
|
47
|
+
cost: number;
|
|
48
|
+
/** OpenRouter's `is_byok`: the turn billed the operator's own vendor key. */
|
|
49
|
+
byok?: boolean;
|
|
50
|
+
/** `cost_details.upstream_inference_cost`: the vendor's own charge on a BYOK turn. */
|
|
51
|
+
upstreamCost?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The cost fields of one wire `usage` object, or undefined when it carries none. */
|
|
55
|
+
export function reportedCostOf(u: unknown): ReportedCost | undefined {
|
|
56
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
57
|
+
const o = u as Record<string, unknown>;
|
|
58
|
+
if (typeof o.cost !== "number" || !Number.isFinite(o.cost)) return undefined;
|
|
59
|
+
const details = o.cost_details;
|
|
60
|
+
const upstream =
|
|
61
|
+
typeof details === "object" && details !== null
|
|
62
|
+
? (details as Record<string, unknown>).upstream_inference_cost
|
|
63
|
+
: undefined;
|
|
64
|
+
return {
|
|
65
|
+
cost: o.cost,
|
|
66
|
+
...(o.is_byok === true ? { byok: true } : {}),
|
|
67
|
+
...(typeof upstream === "number" && Number.isFinite(upstream) ? { upstreamCost: upstream } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The Responses API's `usage` → TokenUsage. `input_tokens_details.cached_tokens`
|
|
72
|
+
* is the cache-read count and `input_tokens_details.cache_write_tokens` the
|
|
73
|
+
* cache-write count where a server reports one (OpenAI itself never does; a
|
|
74
|
+
* gateway on the wire may). `output_tokens_details.reasoning_tokens` rides
|
|
75
|
+
* inside `output_tokens` — the shape is read, never a fifth counter. */
|
|
76
|
+
export function usageFromResponses(u: unknown): TokenUsage | undefined {
|
|
77
|
+
if (typeof u !== "object" || u === null) return undefined;
|
|
78
|
+
const o = u as Record<string, unknown>;
|
|
79
|
+
if (typeof o.input_tokens !== "number" || typeof o.output_tokens !== "number") return undefined;
|
|
80
|
+
const usage: TokenUsage = { inputTokens: o.input_tokens, outputTokens: o.output_tokens };
|
|
81
|
+
const details = o.input_tokens_details;
|
|
82
|
+
if (typeof details === "object" && details !== null) {
|
|
83
|
+
const { cached_tokens, cache_write_tokens } = details as Record<string, unknown>;
|
|
84
|
+
if (typeof cached_tokens === "number") usage.cacheReadTokens = cached_tokens;
|
|
85
|
+
if (typeof cache_write_tokens === "number") usage.cacheWriteTokens = cache_write_tokens;
|
|
86
|
+
}
|
|
87
|
+
return usage;
|
|
88
|
+
}
|
|
@@ -23,7 +23,21 @@ export interface RegistryCard {
|
|
|
23
23
|
* not named (the card's reader applies pi's rule). */
|
|
24
24
|
thinkingLevelMap?: Record<string, string | null>;
|
|
25
25
|
input?: string[];
|
|
26
|
-
cost?: {
|
|
26
|
+
cost?: {
|
|
27
|
+
input?: number;
|
|
28
|
+
output?: number;
|
|
29
|
+
cacheRead?: number;
|
|
30
|
+
cacheWrite?: number;
|
|
31
|
+
/** pi's long-context tiers: the whole request re-rates at the highest tier
|
|
32
|
+
* whose threshold the input side (input + cache reads + cache writes) exceeds. */
|
|
33
|
+
tiers?: Array<{
|
|
34
|
+
inputTokensAbove?: number;
|
|
35
|
+
input?: number;
|
|
36
|
+
output?: number;
|
|
37
|
+
cacheRead?: number;
|
|
38
|
+
cacheWrite?: number;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
27
41
|
contextWindow?: number;
|
|
28
42
|
maxTokens?: number;
|
|
29
43
|
compat?: Record<string, unknown>;
|
|
@@ -87,6 +87,7 @@ const CAUSE_OF = {
|
|
|
87
87
|
ship_preflight_head_unknown: "system",
|
|
88
88
|
ship_preflight_closed_resume: "request",
|
|
89
89
|
ship_preflight_no_task: "request",
|
|
90
|
+
ship_preflight_base_missing: "request",
|
|
90
91
|
// the plan hand-off's fifteen sentences (two share `plan_history_unavailable`)
|
|
91
92
|
plan_base_unknown: "request",
|
|
92
93
|
plan_routed_seed: "request",
|
|
@@ -102,13 +103,9 @@ const CAUSE_OF = {
|
|
|
102
103
|
plan_runner_conflict: "system",
|
|
103
104
|
plan_instance_orphaned: "system",
|
|
104
105
|
plan_start_failed: "system",
|
|
105
|
-
// the
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
directive_budget: "request",
|
|
109
|
-
directive_severity: "request",
|
|
110
|
-
directive_renewals: "request",
|
|
111
|
-
directive_verbosity: "request",
|
|
106
|
+
// the resolve parser's thrown errors (A6 of the inventory); the directive
|
|
107
|
+
// parser no longer refuses — a token whose value is outside its vocabulary
|
|
108
|
+
// is text (the interim grammar, src/directives.ts)
|
|
112
109
|
provider_unknown: "request",
|
|
113
110
|
// the model card's refusal (record 0052, model-proxy item 11): a control the
|
|
114
111
|
// resolved card does not take, named before any card or model call
|
|
@@ -108,6 +108,10 @@ export interface Finding {
|
|
|
108
108
|
line?: number;
|
|
109
109
|
/** One line naming the issue; the full explanation lives in the prose. */
|
|
110
110
|
title: string;
|
|
111
|
+
/** Machine provenance: true only on a check finding the ship round's checks
|
|
112
|
+
* step itself appended (ship/coordinator.ts `checkFinding`) — never set from
|
|
113
|
+
* a reviewer's input, whatever id the reviewer chose. */
|
|
114
|
+
check?: true;
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
export interface ReviewVerdict {
|
|
@@ -397,6 +397,10 @@ export type ShipRoundOutcome =
|
|
|
397
397
|
| "approve"
|
|
398
398
|
| "request_changes"
|
|
399
399
|
| "no_verdict"
|
|
400
|
+
/** The round's checks step read a failed check at the reviewed head (record
|
|
401
|
+
* 0055): the failures become check findings and the findings step runs as
|
|
402
|
+
* for any changes-requested round. */
|
|
403
|
+
| "checks_failed"
|
|
400
404
|
| "aborted"
|
|
401
405
|
| "stopped"
|
|
402
406
|
/** The coding round ended at its lease with the unit unfinished and the row
|
|
@@ -536,6 +540,11 @@ export type RunEvent =
|
|
|
536
540
|
callId?: string;
|
|
537
541
|
spanId?: string;
|
|
538
542
|
logIndex?: number;
|
|
543
|
+
/** The bound the call itself declared, in ms (a bash `timeout`, clamped
|
|
544
|
+
* by nothing here — pi runs an unbounded call until the loop's end).
|
|
545
|
+
* The stall signal (docs/reference/specs/live-view.md item 32) marks a
|
|
546
|
+
* call past it; absent when the call declared none. */
|
|
547
|
+
boundMs?: number;
|
|
539
548
|
seq?: number;
|
|
540
549
|
at?: number;
|
|
541
550
|
}
|
|
@@ -671,8 +680,12 @@ export type RunEvent =
|
|
|
671
680
|
* and — for a repo run — the repo, ref, PR number and PR head as resolved
|
|
672
681
|
* BEFORE the first model turn (`RepoContext`). Published by the dispatcher
|
|
673
682
|
* right after `input`, once per run, so the run page can head its Request
|
|
674
|
-
* block with linked `owner/repo · ref · #PR · sha`.
|
|
675
|
-
*
|
|
683
|
+
* block with linked `owner/repo · ref · #PR · sha`. A HOSTED ship parent
|
|
684
|
+
* (record 0060) carries a second one at the hand-off naming its runner
|
|
685
|
+
* instance (`instanceId`): `run_meta` may repeat on such a run, and every
|
|
686
|
+
* reader of a run's instance id resolves it from the LAST `run_meta`
|
|
687
|
+
* carrying one. Additive: every consumer that only knows the other types
|
|
688
|
+
* keeps working. */
|
|
676
689
|
| {
|
|
677
690
|
type: "run_meta";
|
|
678
691
|
agent: string;
|
|
@@ -865,6 +878,23 @@ export type RunEvent =
|
|
|
865
878
|
* `coordinator_tag` is — so the thread's owner rule can find the instance
|
|
866
879
|
* from the page's ship run. Additive: unknown → ignored. */
|
|
867
880
|
| { type: "ship_handoff"; instanceId: string; seq?: number; at?: number }
|
|
881
|
+
/** A coordinator child interrupted under a deploy roll (docs/reference/specs/run-history.md
|
|
882
|
+
* item 47a): the run closes `interrupted` for a restart from its request —
|
|
883
|
+
* a workspace lost across a bot roll, the resident's container replaced
|
|
884
|
+
* with the relaunch refused — and the record says so as a typed fact
|
|
885
|
+
* beside the `resumed`/`sandbox_restarted` notes, `reason` the refusal or
|
|
886
|
+
* note in one line. Published straight to the registry by the reattach
|
|
887
|
+
* path and the run loop's interruption; the parent's wait settles on its
|
|
888
|
+
* Workflow twin (`child-interrupted-<runId>`) beside `run-finished-<runId>`
|
|
889
|
+
* and confirms by `read-record`. Additive: unknown → ignored. */
|
|
890
|
+
| { type: "child_interrupted"; parentInstanceId: string; reason: string; seq?: number; at?: number }
|
|
891
|
+
/** A coordinator child resumed across a deploy roll (run-history item 47a):
|
|
892
|
+
* the same run re-attached its workspace and carries on under its record,
|
|
893
|
+
* tag and budget. Published straight to the registry by the reattach path;
|
|
894
|
+
* the Workflow twin (`child-resumed-<runId>`) tells the parent's wait to
|
|
895
|
+
* keep waiting rather than walk out the chunk asking. Additive: unknown →
|
|
896
|
+
* ignored. */
|
|
897
|
+
| { type: "child_resumed"; parentInstanceId: string; summary: string; seq?: number; at?: number }
|
|
868
898
|
/** The review post-step's outcome when the verdict landed
|
|
869
899
|
* (docs/reference/specs/agent-review.md item 18): the pull request it was
|
|
870
900
|
* posted to, the head it was pinned to (the carried head after a rebase,
|
|
@@ -905,6 +935,25 @@ export type RunEvent =
|
|
|
905
935
|
seq?: number;
|
|
906
936
|
at?: number;
|
|
907
937
|
}
|
|
938
|
+
/** A hosted ship parent's unit fact (record 0060; docs/reference/specs/agent-ship.md
|
|
939
|
+
* item 17): the runner's routes write it to the parent run through the
|
|
940
|
+
* coordinator's `hostPublish` — `unit-start` publishes state `started` with
|
|
941
|
+
* the unit's thread key and lead, `round` the round's outcome as the unit's
|
|
942
|
+
* state, `unit-end` the ending's kind with its report — carrying the pull
|
|
943
|
+
* request number when the unit's row knows one. The run page draws it as a
|
|
944
|
+
* step whose detail is the report; the friction analyzer counts it as a
|
|
945
|
+
* side fact, never activity. Additive: unknown → ignored. */
|
|
946
|
+
| {
|
|
947
|
+
type: "ship_unit";
|
|
948
|
+
unit: string;
|
|
949
|
+
state: string;
|
|
950
|
+
threadKey?: string;
|
|
951
|
+
lead?: string;
|
|
952
|
+
report?: string;
|
|
953
|
+
pr?: number;
|
|
954
|
+
seq?: number;
|
|
955
|
+
at?: number;
|
|
956
|
+
}
|
|
908
957
|
/** The request router's decision (docs/reference/specs/routing-and-config.md
|
|
909
958
|
* item 21): the preset a plain message was routed to, the one-line reason
|
|
910
959
|
* the router gave (redacted, capped — the same text the card's `routed:`
|
|
@@ -413,8 +413,10 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
413
413
|
// call that already produced its own tool pair, and artifact beside the
|
|
414
414
|
// attach_file call (or the dispatcher's staging) that moved the file;
|
|
415
415
|
// review_artifact, pr_description, pr_opened, review_posted, the ship_round
|
|
416
|
-
// boundaries
|
|
417
|
-
//
|
|
416
|
+
// boundaries, the hosted parent's ship_unit facts (record 0060 — written by
|
|
417
|
+
// the runner's routes, not the model) and the router's route are published
|
|
418
|
+
// by the dispatcher/pipeline outside the model loop entirely. Counting any
|
|
419
|
+
// of them would distort the story.
|
|
418
420
|
if (
|
|
419
421
|
ev.type === "skill_use" ||
|
|
420
422
|
ev.type === "artifact" ||
|
|
@@ -423,8 +425,11 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
|
|
|
423
425
|
ev.type === "pr_opened" ||
|
|
424
426
|
ev.type === "coordinator_tag" ||
|
|
425
427
|
ev.type === "ship_handoff" ||
|
|
428
|
+
ev.type === "child_interrupted" ||
|
|
429
|
+
ev.type === "child_resumed" ||
|
|
426
430
|
ev.type === "review_posted" ||
|
|
427
431
|
ev.type === "ship_round" ||
|
|
432
|
+
ev.type === "ship_unit" ||
|
|
428
433
|
ev.type === "route" ||
|
|
429
434
|
ev.type === "refusal" ||
|
|
430
435
|
ev.type === "reference" ||
|
|
@@ -121,6 +121,17 @@ export interface LiveRunMeta {
|
|
|
121
121
|
* the checklist, the pushed branch, the review head, the infra counters). */
|
|
122
122
|
export type RunState = Record<string, unknown>;
|
|
123
123
|
|
|
124
|
+
/** A hosted ship parent's hosting fact on the row's state (record 0060): the
|
|
125
|
+
* runner instance the run hosts and the deadline past which a reclaim closes
|
|
126
|
+
* the row `interrupted` instead of re-hosting it. Set by the ship branch once
|
|
127
|
+
* the instance exists; read by the boot reclaim. */
|
|
128
|
+
export interface HostingState {
|
|
129
|
+
instanceId: string;
|
|
130
|
+
/** Epoch ms: the hand-off time plus the instance's `caps.maxMinutes` plus
|
|
131
|
+
* `HOSTED_DEADLINE_MARGIN_MINUTES`. */
|
|
132
|
+
until: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
124
135
|
export interface LiveRunRow {
|
|
125
136
|
runId: string;
|
|
126
137
|
threadKey: string;
|
|
@@ -23,6 +23,17 @@ export interface ModelUsage {
|
|
|
23
23
|
outputTokens: number;
|
|
24
24
|
cacheReadTokens: number;
|
|
25
25
|
cacheWriteTokens: number;
|
|
26
|
+
/** The model's dollars summed from its turns' own `usd` attrs (the proxy's
|
|
27
|
+
* meter row, model-proxy item 6): a number when every turn that counted
|
|
28
|
+
* tokens carried one, null when such a turn lacked it (unpriced — a sum
|
|
29
|
+
* that left counted tokens out would understate the model; a turn that
|
|
30
|
+
* counted none, an errored call or a retry, leaves nothing out), absent on
|
|
31
|
+
* a record whose turns predate the meter row (the price table then prices
|
|
32
|
+
* the tokens, costs.md item 4b). */
|
|
33
|
+
usd?: number | null;
|
|
34
|
+
/** The distinct `priceSource` words the turns carried, sorted — what the
|
|
35
|
+
* by-model view names as the source. Absent when no turn carried one. */
|
|
36
|
+
priceSources?: string[];
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export interface RunUsage {
|
|
@@ -46,6 +57,14 @@ export const emptyUsage = (): RunUsage => ({ turns: 0, byModel: {} });
|
|
|
46
57
|
/** The run's usage from its events: one `model.turn` span end per provider call. */
|
|
47
58
|
export function usageOfEvents(events: readonly RunEvent[]): RunUsage {
|
|
48
59
|
const usage = emptyUsage();
|
|
60
|
+
// The meter row per model (model-proxy item 6): the turns' own dollars and
|
|
61
|
+
// sources. `usd` sums only when every turn that counted tokens carried one —
|
|
62
|
+
// a turn that counted none (an upstream error, a retry the harness's SDK
|
|
63
|
+
// spent, a stream broken before its usage) has nothing a sum could leave
|
|
64
|
+
// out, so it never turns the model unpriced; a model none of whose turns
|
|
65
|
+
// carried price attrs stays without the fields (a record from before the
|
|
66
|
+
// meter row — the table prices it, costs.md item 4b).
|
|
67
|
+
const priced = new Map<string, { usd: number; pricedTurns: number; tokenTurns: number; sources: Set<string> }>();
|
|
49
68
|
for (const e of events) {
|
|
50
69
|
if (e.type !== "span_end" || e.name !== MODEL_TURN) continue;
|
|
51
70
|
const attrs = (e.attrs ?? {}) as Record<string, unknown>;
|
|
@@ -57,6 +76,8 @@ export function usageOfEvents(events: readonly RunEvent[]): RunUsage {
|
|
|
57
76
|
cacheReadTokens: 0,
|
|
58
77
|
cacheWriteTokens: 0,
|
|
59
78
|
};
|
|
79
|
+
const counted =
|
|
80
|
+
num(attrs.inputTokens) + num(attrs.outputTokens) + num(attrs.cacheReadTokens) + num(attrs.cacheWriteTokens);
|
|
60
81
|
m.turns += 1;
|
|
61
82
|
m.inputTokens += num(attrs.inputTokens);
|
|
62
83
|
m.outputTokens += num(attrs.outputTokens);
|
|
@@ -64,38 +85,71 @@ export function usageOfEvents(events: readonly RunEvent[]): RunUsage {
|
|
|
64
85
|
m.cacheWriteTokens += num(attrs.cacheWriteTokens);
|
|
65
86
|
usage.byModel[model] = m;
|
|
66
87
|
usage.turns += 1;
|
|
88
|
+
const hasUsd = typeof attrs.usd === "number" && Number.isFinite(attrs.usd);
|
|
89
|
+
const hasSource = typeof attrs.priceSource === "string" && attrs.priceSource !== "";
|
|
90
|
+
const p = priced.get(model) ?? { usd: 0, pricedTurns: 0, tokenTurns: 0, sources: new Set<string>() };
|
|
91
|
+
if (counted > 0) p.tokenTurns += 1;
|
|
92
|
+
if (hasUsd) {
|
|
93
|
+
p.usd += attrs.usd as number;
|
|
94
|
+
p.pricedTurns += 1;
|
|
95
|
+
}
|
|
96
|
+
if (hasSource) p.sources.add(attrs.priceSource as string);
|
|
97
|
+
priced.set(model, p);
|
|
98
|
+
}
|
|
99
|
+
for (const [model, p] of priced) {
|
|
100
|
+
if (p.pricedTurns === 0 && p.sources.size === 0) continue;
|
|
101
|
+
const m = usage.byModel[model]!;
|
|
102
|
+
m.usd = p.pricedTurns >= p.tokenTurns ? p.usd : null;
|
|
103
|
+
if (p.sources.size > 0) m.priceSources = [...p.sources].sort();
|
|
67
104
|
}
|
|
68
105
|
return usage;
|
|
69
106
|
}
|
|
70
107
|
|
|
108
|
+
/** The two sides' `usd` folded: a sum when both carry one, null when either
|
|
109
|
+
* has an unpriced turn, absent when either predates the meter row (the whole
|
|
110
|
+
* cell then prices from the table, never half a figure). */
|
|
111
|
+
const foldUsd = (a: ModelUsage["usd"], b: ModelUsage["usd"]): ModelUsage["usd"] =>
|
|
112
|
+
a === undefined || b === undefined ? undefined : a === null || b === null ? null : a + b;
|
|
113
|
+
|
|
71
114
|
export function addUsage(a: RunUsage, b: RunUsage): RunUsage {
|
|
72
115
|
const out: RunUsage = { turns: a.turns + b.turns, byModel: {} };
|
|
73
116
|
for (const src of [a.byModel, b.byModel]) {
|
|
74
117
|
for (const [model, m] of Object.entries(src)) {
|
|
75
|
-
const acc = out.byModel[model]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
cacheWriteTokens: 0,
|
|
81
|
-
};
|
|
118
|
+
const acc = out.byModel[model];
|
|
119
|
+
if (!acc) {
|
|
120
|
+
out.byModel[model] = { ...m, ...(m.priceSources ? { priceSources: [...m.priceSources] } : {}) };
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
82
123
|
acc.turns += m.turns;
|
|
83
124
|
acc.inputTokens += m.inputTokens;
|
|
84
125
|
acc.outputTokens += m.outputTokens;
|
|
85
126
|
acc.cacheReadTokens += m.cacheReadTokens;
|
|
86
127
|
acc.cacheWriteTokens += m.cacheWriteTokens;
|
|
87
|
-
|
|
128
|
+
const usd = foldUsd(acc.usd, m.usd);
|
|
129
|
+
if (usd === undefined) delete acc.usd;
|
|
130
|
+
else acc.usd = usd;
|
|
131
|
+
const sources = new Set([...(acc.priceSources ?? []), ...(m.priceSources ?? [])]);
|
|
132
|
+
if (sources.size > 0) acc.priceSources = [...sources].sort();
|
|
88
133
|
}
|
|
89
134
|
}
|
|
90
135
|
return out;
|
|
91
136
|
}
|
|
92
137
|
|
|
93
|
-
const isModelUsage = (v: unknown): v is ModelUsage =>
|
|
94
|
-
typeof v
|
|
95
|
-
v
|
|
96
|
-
|
|
97
|
-
|
|
138
|
+
const isModelUsage = (v: unknown): v is ModelUsage => {
|
|
139
|
+
if (typeof v !== "object" || v === null) return false;
|
|
140
|
+
const m = v as Record<string, unknown>;
|
|
141
|
+
if (
|
|
142
|
+
!["turns", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"].every(
|
|
143
|
+
(k) => typeof m[k] === "number",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return false;
|
|
147
|
+
if (m.usd !== undefined && m.usd !== null && typeof m.usd !== "number") return false;
|
|
148
|
+
return (
|
|
149
|
+
m.priceSources === undefined ||
|
|
150
|
+
(Array.isArray(m.priceSources) && m.priceSources.every((s) => typeof s === "string"))
|
|
98
151
|
);
|
|
152
|
+
};
|
|
99
153
|
|
|
100
154
|
export function isRunUsage(v: unknown): v is RunUsage {
|
|
101
155
|
if (typeof v !== "object" || v === null) return false;
|
|
@@ -379,6 +379,9 @@ export interface WatchdogSummary {
|
|
|
379
379
|
action?: unknown;
|
|
380
380
|
error?: string;
|
|
381
381
|
disk?: unknown;
|
|
382
|
+
/** The resident's last memory reading (`{usedBytes, capBytes, percent, …}`,
|
|
383
|
+
* resident-repos.md item 70) when it has one. */
|
|
384
|
+
memory?: unknown;
|
|
382
385
|
/** What the pass did about the resident's refresh Workflow instance: `{id, action, why}`. */
|
|
383
386
|
instance?: unknown;
|
|
384
387
|
}>;
|
|
@@ -439,28 +439,55 @@ export const TIMEOUT_ON_LONG_COMMANDS =
|
|
|
439
439
|
"State a timeout on any command you expect to run longer than a minute: a timeout that reaches past the loop's " +
|
|
440
440
|
"end is refused before the command runs, never cut midway.";
|
|
441
441
|
|
|
442
|
-
/** The fast gates a plan child runs before every push, named one by one
|
|
443
|
-
* (agent-ship item 13; agent-coding item 9).
|
|
444
|
-
* left the choice to the child, and children
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
442
|
+
/** The fast gates a plan child runs before every push, named one by one and
|
|
443
|
+
* each scoped to the changed set (agent-ship item 13; agent-coding item 9).
|
|
444
|
+
* "Its cheapest proving checks" left the choice to the child, and children
|
|
445
|
+
* chose wrong in both directions: prettier was reported clean while
|
|
446
|
+
* `format:check` was red, hygiene imprints reached CI that `hygiene:check`
|
|
447
|
+
* would have caught locally — and children ran the whole suite and the whole
|
|
448
|
+
* typecheck on the shared resident, minutes each call, time-sliced against
|
|
449
|
+
* every other run. So the gates are the changed-set forms, the full runs are
|
|
450
|
+
* said to be CI's alone in the same breath, and each gate is a receipt — the
|
|
451
|
+
* exit line goes into the PR description's validation table as the row's proof
|
|
452
|
+
* (the handoff has no verified list; parseHandoff carries deviations, followUps,
|
|
453
|
+
* unproven and landed), and a gate the child could not run goes under the
|
|
454
|
+
* handoff's unproven list, never claimed clean. The tests compare against the
|
|
455
|
+
* pull request's own base — `origin/<base>` — which the render substitutes
|
|
456
|
+
* from the rebase's `onto` when the contract knows it. */
|
|
457
|
+
/** The changed-set test command as the contract renders it before the base is
|
|
458
|
+
* known; the render substitutes the unit's base for `<base>` (`renderFirstInstruction`). */
|
|
459
|
+
export const CHANGED_SET_TEST_PLACEHOLDER = "`npx vitest run --changed origin/<base>`";
|
|
460
|
+
|
|
449
461
|
export const FAST_GATES_BEFORE_PUSH =
|
|
450
|
-
"The fast gates, before every push
|
|
451
|
-
|
|
452
|
-
"
|
|
453
|
-
"and
|
|
462
|
+
"The fast gates, before every push — each scoped to the changed set, never the whole project: " +
|
|
463
|
+
`${CHANGED_SET_TEST_PLACEHOLDER} (the pull request's base; or the touched test files), ` +
|
|
464
|
+
"`tsc --noEmit -p` the touched tsconfig under `NODE_OPTIONS=--max-old-space-size=6144`, " +
|
|
465
|
+
"`npx prettier --check` on the changed files, `npm run hygiene:check` and `npm run specs:check` — " +
|
|
466
|
+
"then your judgement on what else this change needs, not a longer checklist. Every CI pipeline runs the " +
|
|
467
|
+
"tests, the types, the formatting and the full verification on your push, so you never run them again: " +
|
|
468
|
+
"you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite " +
|
|
469
|
+
"and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run — on a " +
|
|
470
|
+
"shared resident they cost minutes that every other run pays for. " +
|
|
471
|
+
"Paste each command's exit line into the PR description's validation table as the row's proof; a gate you " +
|
|
472
|
+
"could not run goes under the handoff's unproven list and is never claimed clean.";
|
|
454
473
|
|
|
455
474
|
function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
456
475
|
const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
|
|
457
476
|
const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
|
|
477
|
+
// the changed-set test run compares against the unit's own base, which the
|
|
478
|
+
// contract knows as the rebase target; unknown, the placeholder stands
|
|
479
|
+
// split/join, never String.replace: a `$` in a branch name is literal text here
|
|
480
|
+
const gates = rebase.onto
|
|
481
|
+
? FAST_GATES_BEFORE_PUSH.split(`${CHANGED_SET_TEST_PLACEHOLDER} (the pull request's base; or`).join(
|
|
482
|
+
`\`npx vitest run --changed origin/${rebase.onto}\` (or`,
|
|
483
|
+
)
|
|
484
|
+
: FAST_GATES_BEFORE_PUSH;
|
|
458
485
|
return (
|
|
459
486
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
460
487
|
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|
|
461
|
-
`Push the branch as soon as the change exists and the fast gates pass —
|
|
462
|
-
`
|
|
463
|
-
`survive the run's end. ${
|
|
488
|
+
`Push the branch as soon as the change exists and the fast gates pass — the project's full verification ` +
|
|
489
|
+
`is CI's gate, run there after the push with any fix as a further commit; an unpushed tree does not ` +
|
|
490
|
+
`survive the run's end. ${gates} Right before each push, fetch ${onto} again and rebase ` +
|
|
464
491
|
`once more if it moved while you worked, so the pull request is not born conflicting. At the wind-down note, ` +
|
|
465
492
|
`commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
|
|
466
493
|
);
|