@foldrun/core 2.0.0 → 2.1.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/src/catalog.d.ts +7 -0
- package/dist/src/catalog.js +16 -1
- package/dist/src/deploy.d.ts +6 -1
- package/dist/src/deploy.js +8 -5
- package/dist/src/model-credential.js +1 -1
- package/dist/src/model-env.js +1 -1
- package/dist/src/run-container.d.ts +2 -5
- package/dist/src/run-container.js +8 -1
- package/dist/src/runner.d.ts +2 -0
- package/dist/src/runner.js +11 -11
- package/dist/src/step-exec.d.ts +23 -5
- package/dist/src/step-exec.js +30 -23
- package/dist/src/store.d.ts +46 -12
- package/dist/src/store.js +60 -4
- package/dist/src/tool-loop.d.ts +4 -1
- package/dist/src/tool-loop.js +2 -0
- package/dist/src/translator.d.ts +12 -0
- package/dist/src/translator.js +36 -13
- package/package.json +1 -1
- package/src/catalog.ts +18 -2
- package/src/deploy.ts +11 -4
- package/src/model-credential.ts +1 -1
- package/src/model-env.ts +1 -1
- package/src/run-container.ts +10 -3
- package/src/runner.ts +12 -11
- package/src/step-exec.ts +44 -27
- package/src/store.ts +90 -10
- package/src/tool-loop.ts +6 -1
- package/src/translator.ts +39 -15
package/dist/src/catalog.d.ts
CHANGED
|
@@ -83,7 +83,14 @@ export declare function clampEffort(catalog: Catalog | null, modelId: string, ef
|
|
|
83
83
|
export declare function catalogCost(catalog: Catalog | null, modelId: string, usage: {
|
|
84
84
|
inputTokens: number;
|
|
85
85
|
outputTokens: number;
|
|
86
|
+
cacheReadTokens?: number;
|
|
87
|
+
cacheWriteTokens?: number;
|
|
86
88
|
}): number | null;
|
|
89
|
+
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
90
|
+
* quarter more than fresh input, a read a tenth of it. One source for the
|
|
91
|
+
* mid-step ceiling (priceTurn) and the record's repricing (catalogCost). */
|
|
92
|
+
export declare const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
93
|
+
export declare const CACHE_READ_MULTIPLIER = 0.1;
|
|
87
94
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
88
95
|
* one capability that decides agent-worthiness. Tool-capable models first —
|
|
89
96
|
* on an agent platform they are almost always what the author wants. */
|
package/dist/src/catalog.js
CHANGED
|
@@ -200,8 +200,23 @@ export function catalogCost(catalog, modelId, usage) {
|
|
|
200
200
|
const m = findModel(catalog, modelId);
|
|
201
201
|
if (!m || m.promptPrice == null || m.completionPrice == null)
|
|
202
202
|
return null;
|
|
203
|
-
|
|
203
|
+
// A catalogue quotes one input price; cache traffic is priced off it with
|
|
204
|
+
// the multipliers every gateway that bills for caching has copied from
|
|
205
|
+
// Anthropic. Before the counts were split (18 Oct 2026) every cached
|
|
206
|
+
// token was billed here at the full rate — a long cached step cost ten
|
|
207
|
+
// times its price on the ledger.
|
|
208
|
+
// inputTokens is all input; the cache fields are parts of it.
|
|
209
|
+
const read = usage.cacheReadTokens ?? 0;
|
|
210
|
+
const written = usage.cacheWriteTokens ?? 0;
|
|
211
|
+
const fresh = Math.max(0, usage.inputTokens - read - written);
|
|
212
|
+
const input = fresh + written * CACHE_WRITE_MULTIPLIER + read * CACHE_READ_MULTIPLIER;
|
|
213
|
+
return input * m.promptPrice + usage.outputTokens * m.completionPrice;
|
|
204
214
|
}
|
|
215
|
+
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
216
|
+
* quarter more than fresh input, a read a tenth of it. One source for the
|
|
217
|
+
* mid-step ceiling (priceTurn) and the record's repricing (catalogCost). */
|
|
218
|
+
export const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
219
|
+
export const CACHE_READ_MULTIPLIER = 0.1;
|
|
205
220
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
206
221
|
* one capability that decides agent-worthiness. Tool-capable models first —
|
|
207
222
|
* on an agent platform they are almost always what the author wants. */
|
package/dist/src/deploy.d.ts
CHANGED
|
@@ -97,7 +97,9 @@ export declare function deployIssues(files: DeployFile[]): DeployIssue[];
|
|
|
97
97
|
*/
|
|
98
98
|
export declare function runsInFlight(tenant: string, workspace: string): string[];
|
|
99
99
|
/** What a deploy would do, without doing it. */
|
|
100
|
-
export declare function planDeploy(tenant: string, workspace: string, files: DeployFile[]
|
|
100
|
+
export declare function planDeploy(tenant: string, workspace: string, files: DeployFile[], opts?: {
|
|
101
|
+
keepUntracked?: boolean;
|
|
102
|
+
}): DeployPlan;
|
|
101
103
|
export interface DeployOptions {
|
|
102
104
|
/** The commit these files came from, recorded so the dashboard can show it. */
|
|
103
105
|
commit?: string | null;
|
|
@@ -115,6 +117,9 @@ export interface DeployOptions {
|
|
|
115
117
|
* no window between the check and the swap. Absent: no check, as before.
|
|
116
118
|
*/
|
|
117
119
|
expectRemoved?: string[];
|
|
120
|
+
/** Keep what is not editable source (planDeploy, saveWorkspace): a
|
|
121
|
+
* restore takes history's tree, and history tracks source only. */
|
|
122
|
+
keepUntracked?: boolean;
|
|
118
123
|
}
|
|
119
124
|
/**
|
|
120
125
|
* Deploy a file list to a workspace, if it checks out.
|
package/dist/src/deploy.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import os from "node:os";
|
|
22
22
|
import path from "node:path";
|
|
23
|
-
import { saveWorkspace, keptOnDeploy, workspaceDir, listRunSummaries, parseFlow, parseToolDef, WORKSPACE_DIRS, assertSafeName, } from "./store.js";
|
|
23
|
+
import { saveWorkspace, keptOnDeploy, workspaceDir, listRunSummaries, listWorkspaceFiles, parseFlow, parseToolDef, WORKSPACE_DIRS, assertSafeName, } from "./store.js";
|
|
24
24
|
import { lintFlow } from "./flow-lint.js";
|
|
25
25
|
import { refNames } from "./refs.js";
|
|
26
26
|
import matter from "gray-matter";
|
|
@@ -438,7 +438,7 @@ export function runsInFlight(tenant, workspace) {
|
|
|
438
438
|
.map((r) => r.id);
|
|
439
439
|
}
|
|
440
440
|
/** What a deploy would do, without doing it. */
|
|
441
|
-
export function planDeploy(tenant, workspace, files) {
|
|
441
|
+
export function planDeploy(tenant, workspace, files, opts = {}) {
|
|
442
442
|
const dir = workspaceDir(tenant, workspace);
|
|
443
443
|
const shipped = new Map(files.map((f) => [path.normalize(f.path), f.content]));
|
|
444
444
|
const present = new Set();
|
|
@@ -480,7 +480,10 @@ export function planDeploy(tenant, workspace, files) {
|
|
|
480
480
|
// memory, the platform's own files. Reporting these as removals would be
|
|
481
481
|
// a lie, and the alarming kind; one predicate serves both.
|
|
482
482
|
const shippedPaths = new Set(shipped.keys());
|
|
483
|
-
|
|
483
|
+
// keepUntracked (a restore): what is not editable source stays, as
|
|
484
|
+
// saveWorkspace keeps it — see its meta.keepUntracked.
|
|
485
|
+
const tracked = opts.keepUntracked ? new Set(listWorkspaceFiles(tenant, workspace)) : null;
|
|
486
|
+
const removed = [...present].filter((rel) => !shipped.has(rel) && !keptOnDeploy(rel, shippedPaths) && (!tracked || tracked.has(rel)));
|
|
484
487
|
return {
|
|
485
488
|
files,
|
|
486
489
|
added: added.sort(),
|
|
@@ -502,7 +505,7 @@ export function planDeploy(tenant, workspace, files) {
|
|
|
502
505
|
export function deployWorkspace(tenant, workspace, files, opts = {}) {
|
|
503
506
|
assertSafeName(tenant, "tenant");
|
|
504
507
|
assertSafeName(workspace, "workspace");
|
|
505
|
-
const plan = planDeploy(tenant, workspace, files);
|
|
508
|
+
const plan = planDeploy(tenant, workspace, files, { keepUntracked: opts.keepUntracked });
|
|
506
509
|
const blocked = plan.blockedBy.length > 0 && !opts.force;
|
|
507
510
|
if (plan.issues.length > 0 || blocked) {
|
|
508
511
|
return { ...plan, applied: false, commit: null, preserved: 0 };
|
|
@@ -514,7 +517,7 @@ export function deployWorkspace(tenant, workspace, files, opts = {}) {
|
|
|
514
517
|
return { ...plan, applied: false, commit: null, preserved: 0, unexpectedRemovals };
|
|
515
518
|
}
|
|
516
519
|
}
|
|
517
|
-
const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message });
|
|
520
|
+
const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message, keepUntracked: opts.keepUntracked });
|
|
518
521
|
const commit = opts.commit ?? null;
|
|
519
522
|
if (commit)
|
|
520
523
|
writeDeployedCommit(tenant, workspace, commit);
|
|
@@ -60,7 +60,7 @@ export function credentialLine(c) {
|
|
|
60
60
|
//
|
|
61
61
|
// An account brings its own model key without writing a provider: block:
|
|
62
62
|
// three secrets in the ACCOUNT vault (set from Settings → Model, the API's
|
|
63
|
-
// /api/account/model, or `foldrun model set`). A provider: block at any
|
|
63
|
+
// /api/account/model, or `foldrun account model set`). A provider: block at any
|
|
64
64
|
// level still wins; this is what a step falls to when none names one, before
|
|
65
65
|
// the platform's own credential.
|
|
66
66
|
/** The account vault names the account's own model key lives under. */
|
package/dist/src/model-env.js
CHANGED
|
@@ -19,7 +19,7 @@ export class ModelKeyError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
export const NO_MODEL_KEY = "no model key: this account runs on its own API key, and none is set — add one in Settings → Model " +
|
|
22
|
-
"(or `foldrun model set <provider> --key …`), or write a provider: block in AGENTS.md";
|
|
22
|
+
"(or `foldrun account model set <provider> --key …`), or write a provider: block in AGENTS.md";
|
|
23
23
|
/**
|
|
24
24
|
* `platform` is a function: the caller's own way to build the platform's env
|
|
25
25
|
* (a step's, or the SDK's host env), called only when this account may use
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type HookRecord } from "./hooks.ts";
|
|
2
2
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { type ApiSpec, type Effort } from "./store.ts";
|
|
4
|
-
import type { EventExtra } from "./step-exec.ts";
|
|
4
|
+
import type { EventExtra, ExecUsage } from "./step-exec.ts";
|
|
5
5
|
import { type RuntimeSpec } from "./runtime.ts";
|
|
6
6
|
import { type NetworkLevel } from "./network.ts";
|
|
7
7
|
import type { ScriptSpec } from "./script-tools.ts";
|
|
@@ -146,10 +146,7 @@ export interface ContainerStepOutcome {
|
|
|
146
146
|
/** The parsed JSON of an `output: json` step, crossing back as a value. */
|
|
147
147
|
data?: unknown;
|
|
148
148
|
costUsd: number | null;
|
|
149
|
-
usage?:
|
|
150
|
-
inputTokens: number;
|
|
151
|
-
outputTokens: number;
|
|
152
|
-
} | null;
|
|
149
|
+
usage?: ExecUsage | null;
|
|
153
150
|
/** Every turn priced at its own model, when sub-agents ran on others —
|
|
154
151
|
* see ExecOutcome.turnsCostUsd. */
|
|
155
152
|
turnsCostUsd?: number;
|
|
@@ -1068,7 +1068,14 @@ export function parseDriverLine(line) {
|
|
|
1068
1068
|
usage: parsed.usage &&
|
|
1069
1069
|
typeof parsed.usage.inputTokens === "number" &&
|
|
1070
1070
|
typeof parsed.usage.outputTokens === "number"
|
|
1071
|
-
? {
|
|
1071
|
+
? {
|
|
1072
|
+
inputTokens: parsed.usage.inputTokens,
|
|
1073
|
+
outputTokens: parsed.usage.outputTokens,
|
|
1074
|
+
// A driver from before the split sends neither; zero, not
|
|
1075
|
+
// unknown, keeps the record's shape one thing.
|
|
1076
|
+
cacheReadTokens: typeof parsed.usage.cacheReadTokens === "number" ? parsed.usage.cacheReadTokens : 0,
|
|
1077
|
+
cacheWriteTokens: typeof parsed.usage.cacheWriteTokens === "number" ? parsed.usage.cacheWriteTokens : 0,
|
|
1078
|
+
}
|
|
1072
1079
|
: null,
|
|
1073
1080
|
res: parsed.res && typeof parsed.res === "object"
|
|
1074
1081
|
? {
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -354,6 +354,8 @@ grantsOnly?: boolean): {
|
|
|
354
354
|
export declare function repriced(catalog: Catalog | null, model: string, usage: {
|
|
355
355
|
inputTokens: number;
|
|
356
356
|
outputTokens: number;
|
|
357
|
+
cacheReadTokens?: number;
|
|
358
|
+
cacheWriteTokens?: number;
|
|
357
359
|
} | null, sdkCost: number | null, push: (type: "info" | "error", text: string) => void,
|
|
358
360
|
/** The step's turns each priced at its own model — set when a sub-agent
|
|
359
361
|
* ran on a different model (ExecOutcome.turnsCostUsd). The lump `usage`
|
package/dist/src/runner.js
CHANGED
|
@@ -33,7 +33,7 @@ import { ASK_TOOL, applyOperatorEvent, askPromptLine, askTimeoutSec, buildAskToo
|
|
|
33
33
|
import { sendPlainNotification } from "./notify.js";
|
|
34
34
|
import { TOOL_MAP, BUILTIN_TOOLS, ownToolNames, toolRefs, legacyUseNames, legacyUseError, retiredToolNames, retiredToolError } from "./tool-names.js";
|
|
35
35
|
import { refNames } from "./refs.js";
|
|
36
|
-
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, runSummary, verdictOf, sumAttempts, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
36
|
+
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, runSummary, verdictOf, sumAttempts, tokensOf, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
37
37
|
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel } from "./catalog.js";
|
|
38
38
|
import { resolveSecrets, getSecret, materializeSecrets, secretSites } from "./secrets.js";
|
|
39
39
|
import { resolveNetwork, networkLine, proxyEnv, EGRESS_CA_ENV } from "./network.js";
|
|
@@ -1876,8 +1876,12 @@ stepIndex) {
|
|
|
1876
1876
|
// that names a provider uses its own; otherwise the platform's (in a
|
|
1877
1877
|
// run container) or this process's — never a login that just happens
|
|
1878
1878
|
// to be on the machine (model-credential.ts).
|
|
1879
|
-
|
|
1880
|
-
|
|
1879
|
+
// An account refused the platform's credential (below) is never shown
|
|
1880
|
+
// running on it: the record said "Claude login token" on a step that
|
|
1881
|
+
// was then refused for having no key (FOL-14, 18 Oct 2026).
|
|
1882
|
+
const noKey = !providerLabel && !platform.platformKeyAllowed(tenant);
|
|
1883
|
+
const credential = providerLabel || noKey ? null : resolveModelCredential(isolatedRun() ? platformModelEnv() : process.env);
|
|
1884
|
+
step.credential = credential ? credential.label : noKey ? "none — this account has no model key" : `provider: ${providerLabel}`;
|
|
1881
1885
|
if (credential) {
|
|
1882
1886
|
const line = credentialLine(credential);
|
|
1883
1887
|
push(line.type, line.text);
|
|
@@ -1890,7 +1894,7 @@ stepIndex) {
|
|
|
1890
1894
|
const own = Object.values(providerSecrets).concat(Object.values(providerEnv));
|
|
1891
1895
|
if (!providerLabel) {
|
|
1892
1896
|
push("error", `no model key: this account runs on its own API key, and none is set. Add one in Settings → Model ` +
|
|
1893
|
-
`(or \`foldrun model set <provider> --key …\`), or write a provider: block in AGENTS.md`);
|
|
1897
|
+
`(or \`foldrun account model set <provider> --key …\`), or write a provider: block in AGENTS.md`);
|
|
1894
1898
|
step.status = "failed";
|
|
1895
1899
|
save();
|
|
1896
1900
|
return;
|
|
@@ -2621,7 +2625,7 @@ stepIndex) {
|
|
|
2621
2625
|
step.podLostTry = {
|
|
2622
2626
|
status: "lost",
|
|
2623
2627
|
costUsd: repriced(catalog, wireModel, first.usage ?? null, first.costUsd, push, first.turnsCostUsd),
|
|
2624
|
-
tokens: first.usage
|
|
2628
|
+
tokens: tokensOf(first.usage),
|
|
2625
2629
|
computeSecs: first.timing ? Math.round(first.timing.totalMs) / 1000 : null,
|
|
2626
2630
|
finishedAt: new Date().toISOString(),
|
|
2627
2631
|
error: decision.note.slice(0, 500),
|
|
@@ -2671,9 +2675,7 @@ stepIndex) {
|
|
|
2671
2675
|
if (outcome.data !== undefined)
|
|
2672
2676
|
step.data = redactData(outcome.data);
|
|
2673
2677
|
step.costUsd = repriced(catalog, wireModel, outcome.usage ?? null, outcome.costUsd, push, outcome.turnsCostUsd);
|
|
2674
|
-
step.tokens = outcome.usage
|
|
2675
|
-
? { input: outcome.usage.inputTokens, output: outcome.usage.outputTokens }
|
|
2676
|
-
: null;
|
|
2678
|
+
step.tokens = tokensOf(outcome.usage);
|
|
2677
2679
|
step.actual = outcome.res ?? null;
|
|
2678
2680
|
if (outcome.res?.peakMemBytes || outcome.res?.rxBytes) {
|
|
2679
2681
|
const mb = (n) => (n === null ? "?" : (n / 1024 / 1024).toFixed(0));
|
|
@@ -2870,9 +2872,7 @@ stepIndex) {
|
|
|
2870
2872
|
const consultCost = consultTools.drainCost();
|
|
2871
2873
|
const stepCost = repriced(catalog, wireModel, outcome.usage, outcome.costUsd, push, outcome.turnsCostUsd);
|
|
2872
2874
|
step.costUsd = stepCost === null && consultCost === 0 ? null : (stepCost ?? 0) + consultCost;
|
|
2873
|
-
step.tokens = outcome.usage
|
|
2874
|
-
? { input: outcome.usage.inputTokens, output: outcome.usage.outputTokens }
|
|
2875
|
-
: null;
|
|
2875
|
+
step.tokens = tokensOf(outcome.usage);
|
|
2876
2876
|
for (const line of apiTools.drainLog())
|
|
2877
2877
|
push("info", `api: ${line}`);
|
|
2878
2878
|
for (const line of scriptTools.drainLog())
|
package/dist/src/step-exec.d.ts
CHANGED
|
@@ -6,6 +6,17 @@ import type { OperatorEvent } from "./operator.ts";
|
|
|
6
6
|
import { type SubagentSpec } from "./subagents.ts";
|
|
7
7
|
import { type Limits } from "./limits.ts";
|
|
8
8
|
import { type BrowserPodOutcome, type ClassifyContext } from "./browser-pod.ts";
|
|
9
|
+
export interface ExecUsage {
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
cacheReadTokens: number;
|
|
13
|
+
cacheWriteTokens: number;
|
|
14
|
+
}
|
|
15
|
+
/** The four counts from one SDK usage object (snake_case, as the API
|
|
16
|
+
* reports it). The API counts fresh input apart from the cache's; here
|
|
17
|
+
* `inputTokens` is all of it, as it always was on the record, and the
|
|
18
|
+
* cache fields say how much of that the cache served or stored. */
|
|
19
|
+
export declare function usageFromSdk(u: Record<string, number | undefined>): ExecUsage;
|
|
9
20
|
export interface ExecOutcome {
|
|
10
21
|
status: "completed" | "failed";
|
|
11
22
|
result: string | null;
|
|
@@ -20,11 +31,12 @@ export interface ExecOutcome {
|
|
|
20
31
|
costUsd: number | null;
|
|
21
32
|
/** Token counts off the SDK's result message. costUsd is priced from
|
|
22
33
|
* Anthropic's table, which is wrong for a routed model — these are the
|
|
23
|
-
* raw numbers a caller with a gateway's own prices can reprice from.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
* raw numbers a caller with a gateway's own prices can reprice from.
|
|
35
|
+
* `inputTokens` is every input token; `cacheReadTokens` and
|
|
36
|
+
* `cacheWriteTokens` are the part of it the prompt cache served or
|
|
37
|
+
* stored, so a usage page can say how much of a step's context was
|
|
38
|
+
* re-read for a tenth of the price. */
|
|
39
|
+
usage: ExecUsage | null;
|
|
28
40
|
/** Set only when some turns ran on a different model from the step's — a
|
|
29
41
|
* sub-agent with its own `model:`. The sum of every turn, each priced at
|
|
30
42
|
* its own model. `usage` above is one lump and cannot be repriced at the
|
|
@@ -178,6 +190,12 @@ export declare function knownPrice(model: string | undefined): {
|
|
|
178
190
|
/**
|
|
179
191
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
180
192
|
*
|
|
193
|
+
* Cache reads and writes are priced at Anthropic's multipliers
|
|
194
|
+
* (catalog.ts). Counting both at the full input rate — as this did —
|
|
195
|
+
* makes a long cached prompt read about ten times its price, which on a
|
|
196
|
+
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
197
|
+
* killed for spending money it never spent.
|
|
198
|
+
*
|
|
181
199
|
* A gateway whose cache is priced differently is over- or under-counted
|
|
182
200
|
* here by that difference alone; the run record's own cost is repriced
|
|
183
201
|
* from the catalogue afterwards and stays the authority on the bill. This
|
package/dist/src/step-exec.js
CHANGED
|
@@ -12,6 +12,7 @@ import fs from "node:fs";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
15
|
+
import { CACHE_READ_MULTIPLIER, CACHE_WRITE_MULTIPLIER } from "./catalog.js";
|
|
15
16
|
import { spawn } from "node:child_process";
|
|
16
17
|
import { checkPaths, checkBash, isFilesystemTool, linkWorkspace, workspaceLinkEnv, resolveAgentPath, isWithin } from "./confine.js";
|
|
17
18
|
import { DELEGATE_TOOLS, subagentGuard, toAgentDefinitions } from "./subagents.js";
|
|
@@ -19,6 +20,18 @@ import { hostSafeEnv } from "./host-env.js";
|
|
|
19
20
|
import { validateSchema, describeSchemaErrors, looksLikeSchema } from "./json-schema.js";
|
|
20
21
|
import { CallCounter, limitKeysFor } from "./limits.js";
|
|
21
22
|
import { classifyCall, podAt, readPodEvents } from "./browser-pod.js";
|
|
23
|
+
/** The four counts from one SDK usage object (snake_case, as the API
|
|
24
|
+
* reports it). The API counts fresh input apart from the cache's; here
|
|
25
|
+
* `inputTokens` is all of it, as it always was on the record, and the
|
|
26
|
+
* cache fields say how much of that the cache served or stored. */
|
|
27
|
+
export function usageFromSdk(u) {
|
|
28
|
+
return {
|
|
29
|
+
inputTokens: (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0),
|
|
30
|
+
outputTokens: u.output_tokens ?? 0,
|
|
31
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
32
|
+
cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
22
35
|
/** How often the stop flag is read while a step runs. */
|
|
23
36
|
export const STOP_POLL_MS = 2000;
|
|
24
37
|
/** The least a `verify:` gets when the step used up most of its `timeout:` —
|
|
@@ -65,17 +78,15 @@ export function knownPrice(model) {
|
|
|
65
78
|
return null;
|
|
66
79
|
return KNOWN_PRICES.find(([re]) => re.test(model))?.[1] ?? null;
|
|
67
80
|
}
|
|
68
|
-
/** Anthropic's cache multipliers, and the shape every gateway that bills
|
|
69
|
-
* for caching at all has copied: a write costs a quarter more than fresh
|
|
70
|
-
* input, a read a tenth of it. Counting both at the full input rate — as
|
|
71
|
-
* this did — makes a long cached prompt read about ten times its price,
|
|
72
|
-
* which on a ceiling is not "the honest direction to be wrong in", it is
|
|
73
|
-
* a step killed for spending money it never spent. */
|
|
74
|
-
const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
75
|
-
const CACHE_READ_MULTIPLIER = 0.1;
|
|
76
81
|
/**
|
|
77
82
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
78
83
|
*
|
|
84
|
+
* Cache reads and writes are priced at Anthropic's multipliers
|
|
85
|
+
* (catalog.ts). Counting both at the full input rate — as this did —
|
|
86
|
+
* makes a long cached prompt read about ten times its price, which on a
|
|
87
|
+
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
88
|
+
* killed for spending money it never spent.
|
|
89
|
+
*
|
|
79
90
|
* A gateway whose cache is priced differently is over- or under-counted
|
|
80
91
|
* here by that difference alone; the run record's own cost is repriced
|
|
81
92
|
* from the catalogue afterwards and stays the authority on the bill. This
|
|
@@ -639,16 +650,10 @@ async function executeStepInner(opts, runQuery) {
|
|
|
639
650
|
costUsd = "total_cost_usd" in message ? (message.total_cost_usd ?? null) : null;
|
|
640
651
|
stopReason = "stop_reason" in message && typeof message.stop_reason === "string" ? message.stop_reason : null;
|
|
641
652
|
if ("usage" in message && message.usage) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
// gateways with cheaper cache reads, which errs on the honest side.
|
|
647
|
-
inputTokens: (u.input_tokens ?? 0) +
|
|
648
|
-
(u.cache_creation_input_tokens ?? 0) +
|
|
649
|
-
(u.cache_read_input_tokens ?? 0),
|
|
650
|
-
outputTokens: u.output_tokens ?? 0,
|
|
651
|
-
};
|
|
653
|
+
// Cache reads and writes counted beside the input total: with only
|
|
654
|
+
// the total (until 18 Oct 2026) the hit rate could not be read and
|
|
655
|
+
// the catalogue repriced every cached token at the full rate.
|
|
656
|
+
usage = usageFromSdk(message.usage);
|
|
652
657
|
}
|
|
653
658
|
}
|
|
654
659
|
}
|
|
@@ -671,16 +676,18 @@ async function executeStepInner(opts, runQuery) {
|
|
|
671
676
|
// died — means no total from the SDK. The turns it did take were billed by
|
|
672
677
|
// the provider all the same, so the step's cost is their sum.
|
|
673
678
|
if (costUsd === null && turns.size) {
|
|
674
|
-
|
|
675
|
-
let output = 0;
|
|
679
|
+
const summed = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
676
680
|
let priced = 0;
|
|
677
681
|
for (const { u: t, price } of turns.values()) {
|
|
678
|
-
|
|
679
|
-
|
|
682
|
+
const one = usageFromSdk(t);
|
|
683
|
+
summed.inputTokens += one.inputTokens;
|
|
684
|
+
summed.outputTokens += one.outputTokens;
|
|
685
|
+
summed.cacheReadTokens += one.cacheReadTokens;
|
|
686
|
+
summed.cacheWriteTokens += one.cacheWriteTokens;
|
|
680
687
|
priced += priceTurn(t, price);
|
|
681
688
|
}
|
|
682
689
|
costUsd = priced;
|
|
683
|
-
usage ??=
|
|
690
|
+
usage ??= summed;
|
|
684
691
|
emit("info", `cost from ${turns.size} model turn${turns.size === 1 ? "" : "s"}: $${priced.toFixed(4)} — the step ended before the model's closing total`);
|
|
685
692
|
}
|
|
686
693
|
if (ended === "timeout") {
|
package/dist/src/store.d.ts
CHANGED
|
@@ -125,6 +125,11 @@ export declare function saveWorkspace(tenant: string, workspace: string, files:
|
|
|
125
125
|
* commit, which becomes the revision's id. */
|
|
126
126
|
meta?: ChangeMeta & {
|
|
127
127
|
commit?: string | null;
|
|
128
|
+
/** Keep every file that is not editable source — what an agent wrote
|
|
129
|
+
* into its own folder at run time (a browser profile, a PDF it
|
|
130
|
+
* downloaded). A restore sets it: history holds source, so a file it
|
|
131
|
+
* never tracked is not one a restore may take away. */
|
|
132
|
+
keepUntracked?: boolean;
|
|
128
133
|
}): {
|
|
129
134
|
preserved: number;
|
|
130
135
|
};
|
|
@@ -948,10 +953,7 @@ export interface StepAttempt {
|
|
|
948
953
|
* of its own (image slim), before the full go's. */
|
|
949
954
|
status: "completed" | "failed" | "lost";
|
|
950
955
|
costUsd: number | null;
|
|
951
|
-
tokens:
|
|
952
|
-
input: number;
|
|
953
|
-
output: number;
|
|
954
|
-
} | null;
|
|
956
|
+
tokens: TokenUsage | null;
|
|
955
957
|
computeSecs: number | null;
|
|
956
958
|
startedAt: string;
|
|
957
959
|
finishedAt: string;
|
|
@@ -967,6 +969,44 @@ export interface StepAttempt {
|
|
|
967
969
|
* attempt reported the figure — an in-process step rents no sandbox, and
|
|
968
970
|
* billing reads null as zero, not as "unknown, charge something". */
|
|
969
971
|
export declare function sumAttempts(tries: StepAttempt[]): Pick<StepAttempt, "costUsd" | "tokens" | "computeSecs">;
|
|
972
|
+
/**
|
|
973
|
+
* The token counts behind a step's cost. `input` is every input token the
|
|
974
|
+
* model saw; `cacheRead` and `cacheWrite` are the part of it the prompt
|
|
975
|
+
* cache served or stored — a read costs a tenth of fresh input, and a usage
|
|
976
|
+
* page wants to say how much of a step's context was re-read rather than
|
|
977
|
+
* re-sent. Steps recorded before 18 Oct 2026 have no cache fields: their
|
|
978
|
+
* `input` is the same total, but how much of it was cached is unknown.
|
|
979
|
+
*/
|
|
980
|
+
export interface TokenUsage {
|
|
981
|
+
input: number;
|
|
982
|
+
output: number;
|
|
983
|
+
cacheRead?: number;
|
|
984
|
+
cacheWrite?: number;
|
|
985
|
+
}
|
|
986
|
+
/** The record's shape from what a step reports (ExecOutcome.usage). */
|
|
987
|
+
export declare function tokensOf(usage: {
|
|
988
|
+
inputTokens: number;
|
|
989
|
+
outputTokens: number;
|
|
990
|
+
cacheReadTokens?: number;
|
|
991
|
+
cacheWriteTokens?: number;
|
|
992
|
+
} | null | undefined): TokenUsage | null;
|
|
993
|
+
/** Field-by-field sum; the cache fields are present in the result whenever
|
|
994
|
+
* any input had them, so a run of old and new steps still reads whole. */
|
|
995
|
+
export declare function sumTokens(list: readonly TokenUsage[]): TokenUsage;
|
|
996
|
+
/** The share of all input that the prompt cache served, 0–1; null when
|
|
997
|
+
* there was no input, or the record predates the split and cannot say. */
|
|
998
|
+
export declare function cacheHitRate(t: TokenUsage | null | undefined): number | null;
|
|
999
|
+
/** The cache share over several counts, taking only those that report the
|
|
1000
|
+
* split: a step recorded before it holds its cache traffic inside `input`,
|
|
1001
|
+
* and counting that as uncached would understate the rate. The rule the
|
|
1002
|
+
* Usage page and the CLI's totals follow too. */
|
|
1003
|
+
export declare function measuredHitRate(list: readonly (TokenUsage | null | undefined)[]): number | null;
|
|
1004
|
+
/** A run's token counts, summed over its steps; null when no step reported any. */
|
|
1005
|
+
export declare function runTokens(run: {
|
|
1006
|
+
steps: readonly {
|
|
1007
|
+
tokens?: TokenUsage | null;
|
|
1008
|
+
}[];
|
|
1009
|
+
}): TokenUsage | null;
|
|
970
1010
|
export interface StepRecord {
|
|
971
1011
|
agent: string;
|
|
972
1012
|
instruction: string;
|
|
@@ -1154,10 +1194,7 @@ export interface StepRecord {
|
|
|
1154
1194
|
/** Token counts behind costUsd, kept so usage reports can say "how many"
|
|
1155
1195
|
* and not only "how much" — the price of a token changes, the count is
|
|
1156
1196
|
* the fact. Absent on steps recorded before this existed. */
|
|
1157
|
-
tokens?:
|
|
1158
|
-
input: number;
|
|
1159
|
-
output: number;
|
|
1160
|
-
} | null;
|
|
1197
|
+
tokens?: TokenUsage | null;
|
|
1161
1198
|
/** What the sandbox actually touched, against what it reserved: CPU
|
|
1162
1199
|
* busy-seconds and peak memory from the pod's own cgroup, bytes on the
|
|
1163
1200
|
* wire from /proc/net/dev. Null per-field where the kernel interface
|
|
@@ -1319,10 +1356,7 @@ export interface RunSummaryStep {
|
|
|
1319
1356
|
group: number;
|
|
1320
1357
|
status: StepRecord["status"];
|
|
1321
1358
|
costUsd: number | null;
|
|
1322
|
-
tokens?:
|
|
1323
|
-
input: number;
|
|
1324
|
-
output: number;
|
|
1325
|
-
} | null;
|
|
1359
|
+
tokens?: TokenUsage | null;
|
|
1326
1360
|
computeSecs?: number | null;
|
|
1327
1361
|
skipReason?: string;
|
|
1328
1362
|
/** The last error event, for a failed step — what runFailure reads. */
|
package/dist/src/store.js
CHANGED
|
@@ -385,7 +385,6 @@ meta = {}) {
|
|
|
385
385
|
// about it. Before this, every deploy silently erased everything the agents
|
|
386
386
|
// had learned.
|
|
387
387
|
const shipped = new Set(files.map((f) => path.normalize(f.path)));
|
|
388
|
-
const isAgentOwned = (rel) => keptOnDeploy(rel, shipped);
|
|
389
388
|
// What is there now, for the revision: every editable file's content
|
|
390
389
|
// before this save replaces the tree.
|
|
391
390
|
const before = new Map();
|
|
@@ -399,6 +398,7 @@ meta = {}) {
|
|
|
399
398
|
}
|
|
400
399
|
}
|
|
401
400
|
}
|
|
401
|
+
const isAgentOwned = (rel) => keptOnDeploy(rel, shipped) || (meta.keepUntracked === true && !before.has(rel) && !shipped.has(rel));
|
|
402
402
|
const snapshot = fs.existsSync(dir) ? fs.mkdtempSync(path.join(dataRoot(), ".keep-")) : null;
|
|
403
403
|
const preserved = [];
|
|
404
404
|
if (snapshot) {
|
|
@@ -406,11 +406,15 @@ meta = {}) {
|
|
|
406
406
|
const rel = String(entry).split(path.sep).join("/");
|
|
407
407
|
const from = path.join(dir, rel);
|
|
408
408
|
try {
|
|
409
|
-
|
|
409
|
+
// lstat, never stat: a link is never followed. An agent folder's link
|
|
410
|
+
// to a file outside the workspace was copied in as that file's
|
|
411
|
+
// contents — on a restore with keepUntracked, any such link
|
|
412
|
+
// (Codex, 18 Oct 2026).
|
|
413
|
+
if (!fs.lstatSync(from).isFile() || !isAgentOwned(rel))
|
|
410
414
|
continue;
|
|
411
415
|
}
|
|
412
416
|
catch {
|
|
413
|
-
continue; // a
|
|
417
|
+
continue; // a file that vanished mid-deploy
|
|
414
418
|
}
|
|
415
419
|
const to = path.join(snapshot, rel);
|
|
416
420
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
@@ -2573,10 +2577,62 @@ export function sumAttempts(tries) {
|
|
|
2573
2577
|
const secs = tries.map((t) => t.computeSecs).filter((c) => typeof c === "number");
|
|
2574
2578
|
return {
|
|
2575
2579
|
costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
|
|
2576
|
-
tokens: toks.length ? toks
|
|
2580
|
+
tokens: toks.length ? sumTokens(toks) : null,
|
|
2577
2581
|
computeSecs: secs.length ? secs.reduce((a, b) => a + b, 0) : null,
|
|
2578
2582
|
};
|
|
2579
2583
|
}
|
|
2584
|
+
/** The record's shape from what a step reports (ExecOutcome.usage). */
|
|
2585
|
+
export function tokensOf(usage) {
|
|
2586
|
+
if (!usage)
|
|
2587
|
+
return null;
|
|
2588
|
+
return {
|
|
2589
|
+
input: usage.inputTokens,
|
|
2590
|
+
output: usage.outputTokens,
|
|
2591
|
+
cacheRead: usage.cacheReadTokens ?? 0,
|
|
2592
|
+
cacheWrite: usage.cacheWriteTokens ?? 0,
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
/** Field-by-field sum; the cache fields are present in the result whenever
|
|
2596
|
+
* any input had them, so a run of old and new steps still reads whole. */
|
|
2597
|
+
export function sumTokens(list) {
|
|
2598
|
+
const out = { input: 0, output: 0 };
|
|
2599
|
+
for (const t of list) {
|
|
2600
|
+
out.input += t.input;
|
|
2601
|
+
out.output += t.output;
|
|
2602
|
+
if (t.cacheRead !== undefined || t.cacheWrite !== undefined) {
|
|
2603
|
+
out.cacheRead = (out.cacheRead ?? 0) + (t.cacheRead ?? 0);
|
|
2604
|
+
out.cacheWrite = (out.cacheWrite ?? 0) + (t.cacheWrite ?? 0);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
return out;
|
|
2608
|
+
}
|
|
2609
|
+
/** The share of all input that the prompt cache served, 0–1; null when
|
|
2610
|
+
* there was no input, or the record predates the split and cannot say. */
|
|
2611
|
+
export function cacheHitRate(t) {
|
|
2612
|
+
if (!t || t.cacheRead === undefined)
|
|
2613
|
+
return null;
|
|
2614
|
+
return t.input > 0 ? Math.min(1, t.cacheRead / t.input) : null;
|
|
2615
|
+
}
|
|
2616
|
+
/** The cache share over several counts, taking only those that report the
|
|
2617
|
+
* split: a step recorded before it holds its cache traffic inside `input`,
|
|
2618
|
+
* and counting that as uncached would understate the rate. The rule the
|
|
2619
|
+
* Usage page and the CLI's totals follow too. */
|
|
2620
|
+
export function measuredHitRate(list) {
|
|
2621
|
+
let read = 0;
|
|
2622
|
+
let all = 0;
|
|
2623
|
+
for (const t of list) {
|
|
2624
|
+
if (!t || t.cacheRead === undefined)
|
|
2625
|
+
continue;
|
|
2626
|
+
read += t.cacheRead;
|
|
2627
|
+
all += t.input;
|
|
2628
|
+
}
|
|
2629
|
+
return all > 0 ? read / all : null;
|
|
2630
|
+
}
|
|
2631
|
+
/** A run's token counts, summed over its steps; null when no step reported any. */
|
|
2632
|
+
export function runTokens(run) {
|
|
2633
|
+
const toks = run.steps.map((s) => s.tokens).filter((t) => !!t);
|
|
2634
|
+
return toks.length ? sumTokens(toks) : null;
|
|
2635
|
+
}
|
|
2580
2636
|
export function runFilePath(tenant, workspace, runId) {
|
|
2581
2637
|
assertSafeName(runId, "run id");
|
|
2582
2638
|
return path.join(workspaceDir(tenant, workspace), "runs", `${runId}.json`);
|
package/dist/src/tool-loop.d.ts
CHANGED
|
@@ -13,9 +13,12 @@ export interface LoopTool {
|
|
|
13
13
|
run: (args: Record<string, string | undefined>) => Promise<string> | string;
|
|
14
14
|
}
|
|
15
15
|
export interface LoopUsage {
|
|
16
|
-
/** Everything sent, cache reads and writes included
|
|
16
|
+
/** Everything sent, cache reads and writes included; the two cache
|
|
17
|
+
* fields say how much of it the prompt cache served or stored. */
|
|
17
18
|
inputTokens: number;
|
|
18
19
|
outputTokens: number;
|
|
20
|
+
cacheReadTokens: number;
|
|
21
|
+
cacheWriteTokens: number;
|
|
19
22
|
costUsd: number;
|
|
20
23
|
}
|
|
21
24
|
export interface LoopResult {
|
package/dist/src/tool-loop.js
CHANGED
|
@@ -170,6 +170,8 @@ export async function runToolLoop(o) {
|
|
|
170
170
|
usage = {
|
|
171
171
|
inputTokens: models.reduce((n, m) => n + (m.inputTokens ?? 0) + (m.cacheReadInputTokens ?? 0) + (m.cacheCreationInputTokens ?? 0), 0),
|
|
172
172
|
outputTokens: models.reduce((n, m) => n + (m.outputTokens ?? 0), 0),
|
|
173
|
+
cacheReadTokens: models.reduce((n, m) => n + (m.cacheReadInputTokens ?? 0), 0),
|
|
174
|
+
cacheWriteTokens: models.reduce((n, m) => n + (m.cacheCreationInputTokens ?? 0), 0),
|
|
173
175
|
costUsd,
|
|
174
176
|
};
|
|
175
177
|
if (message.subtype === "success") {
|
package/dist/src/translator.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
type Json = Record<string, unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* An OpenAI-shaped usage object as Anthropic's: fresh input, cached input,
|
|
4
|
+
* output. Both OpenAI shapes count the cached part INSIDE the prompt total
|
|
5
|
+
* (Chat's prompt_tokens_details.cached_tokens, Responses'
|
|
6
|
+
* input_tokens_details.cached_tokens; DeepSeek says prompt_cache_hit_tokens),
|
|
7
|
+
* so it is taken out of input here or the step would count it twice.
|
|
8
|
+
*/
|
|
9
|
+
export declare function anthropicUsage(u: Json | undefined, wire: TranslatorWire): {
|
|
10
|
+
input: number;
|
|
11
|
+
cached: number;
|
|
12
|
+
output: number;
|
|
13
|
+
};
|
|
2
14
|
export type TranslatorWire = "chat" | "responses";
|
|
3
15
|
export interface TranslatorSpec {
|
|
4
16
|
/** The provider's base, e.g. https://api.openai.com/v1 — `/chat/completions`
|