@foldrun/core 1.4.2 → 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/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/src/catalog.d.ts +7 -0
- package/dist/src/catalog.js +16 -1
- package/dist/src/deploy.d.ts +6 -27
- package/dist/src/deploy.js +14 -56
- package/dist/src/history.d.ts +30 -10
- package/dist/src/history.js +232 -99
- package/dist/src/library.d.ts +0 -13
- package/dist/src/library.js +0 -50
- package/dist/src/model-credential.js +1 -1
- package/dist/src/model-env.js +1 -1
- package/dist/src/platform.d.ts +1 -3
- package/dist/src/platform.js +0 -1
- package/dist/src/run-container.d.ts +31 -5
- package/dist/src/run-container.js +71 -3
- package/dist/src/runner.d.ts +2 -0
- package/dist/src/runner.js +20 -12
- package/dist/src/secrets.js +1 -12
- package/dist/src/starter.js +2 -2
- package/dist/src/step-exec.d.ts +23 -5
- package/dist/src/step-exec.js +30 -23
- package/dist/src/store.d.ts +61 -22
- package/dist/src/store.js +105 -25
- 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/index.ts +0 -3
- package/package.json +1 -3
- package/src/catalog.ts +18 -2
- package/src/deploy.ts +13 -74
- package/src/history.ts +230 -108
- package/src/library.ts +0 -44
- package/src/model-credential.ts +1 -1
- package/src/model-env.ts +1 -1
- package/src/platform.ts +1 -4
- package/src/run-container.ts +78 -5
- package/src/runner.ts +22 -12
- package/src/secrets.ts +1 -11
- package/src/starter.ts +2 -2
- package/src/step-exec.ts +44 -27
- package/src/store.ts +145 -37
- package/src/tool-loop.ts +6 -1
- package/src/translator.ts +39 -15
- package/dist/src/git-hook.d.ts +0 -1
- package/dist/src/git-hook.js +0 -70
- package/dist/src/git.d.ts +0 -37
- package/dist/src/git.js +0 -101
- package/dist/src/gitrepo.d.ts +0 -113
- package/dist/src/gitrepo.js +0 -404
- package/dist/src/tar.d.ts +0 -30
- package/dist/src/tar.js +0 -112
- package/src/git-hook.ts +0 -73
- package/src/git.ts +0 -117
- package/src/gitrepo.ts +0 -439
- package/src/tar.ts +0 -125
|
@@ -30,6 +30,42 @@ import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKE
|
|
|
30
30
|
import { egressCaSh } from "./network.js";
|
|
31
31
|
import { isOperatorEvent } from "./operator.js";
|
|
32
32
|
import { parseBrowserPod } from "./browser-pod.js";
|
|
33
|
+
/**
|
|
34
|
+
* The start-up of one step, in the order it happens. Each is the moment the
|
|
35
|
+
* phase ENDED, in ms since the executor began the step, so the gap between
|
|
36
|
+
* two neighbours is what the later one cost (FOL-29: "pod ready" was the
|
|
37
|
+
* only number, and the six seconds after it had no names).
|
|
38
|
+
*
|
|
39
|
+
* The executor marks its own: `services` (the account's search/browser pods
|
|
40
|
+
* up), `room` (room on the cluster to start — platform pod-room.ts),
|
|
41
|
+
* `applied` (pod created), `ready`, `files` (the workspace copied in),
|
|
42
|
+
* `go` (the step released). The sandbox marks the rest as `{"e":"mark"}`
|
|
43
|
+
* lines: `shim` (the shim saw go), `system` (system packages restored),
|
|
44
|
+
* `driver` (node up), `imports` (core loaded), `tools` (runtime and tools
|
|
45
|
+
* built, the SDK about to start) and `model` (the model's first text or tool
|
|
46
|
+
* call). A worker that predates a mark ignores the line — parseDriverLine
|
|
47
|
+
* answers null for an `e` it does not know.
|
|
48
|
+
*/
|
|
49
|
+
export const STARTUP_PHASES = [
|
|
50
|
+
"services", "room", "applied", "ready", "files", "go",
|
|
51
|
+
"shim", "system", "driver", "imports", "tools", "model",
|
|
52
|
+
];
|
|
53
|
+
/** A sandbox-side mark: which phase ended, and when (epoch ms, the
|
|
54
|
+
* sandbox's clock — the same machine's as the worker's on one node). */
|
|
55
|
+
export function parseDriverMark(line) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed.startsWith('{"e":"mark"'))
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(trimmed);
|
|
61
|
+
if (parsed.e !== "mark" || typeof parsed.at !== "number" || !Number.isFinite(parsed.at))
|
|
62
|
+
return null;
|
|
63
|
+
return STARTUP_PHASES.includes(parsed.name) ? { name: parsed.name, at: parsed.at } : null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
33
69
|
/**
|
|
34
70
|
* The one place the timing line is worded, so the docker and k8s traces
|
|
35
71
|
* read the same and a cold-start regression is greppable across both.
|
|
@@ -37,7 +73,11 @@ import { parseBrowserPod } from "./browser-pod.js";
|
|
|
37
73
|
export function timingLine(t) {
|
|
38
74
|
const s = (ms) => `${(ms / 1000).toFixed(2)}s`;
|
|
39
75
|
const first = t.firstOutputMs === null ? "no output" : `first output +${s(t.firstOutputMs)}`;
|
|
40
|
-
|
|
76
|
+
const phases = t.phases
|
|
77
|
+
? STARTUP_PHASES.filter((p) => typeof t.phases[p] === "number").map((p) => `${p} ${s(t.phases[p])}`)
|
|
78
|
+
: [];
|
|
79
|
+
return `timing: sandbox ready in ${s(t.sandboxMs)}, ${first}, ${s(t.totalMs)} total` +
|
|
80
|
+
(phases.length ? ` · start-up: ${phases.join(" → ")}` : "");
|
|
41
81
|
}
|
|
42
82
|
const cli = () => process.env.FOLDRUN_CONTAINER_CLI ?? "docker";
|
|
43
83
|
// Paths that must never come back from a container, whatever happened in
|
|
@@ -374,7 +414,17 @@ const DRIVER = `// The runner container's entrypoint. Reads one step, executes i
|
|
|
374
414
|
// same core the server uses, speaks JSONL on stdout, exits.
|
|
375
415
|
import fs from "node:fs";
|
|
376
416
|
const input = JSON.parse(fs.readFileSync("/opt/runner/job/input.json", "utf8"));
|
|
377
|
-
|
|
417
|
+
// Start-up marks (run-container.ts#STARTUP_PHASES): which phase just ended,
|
|
418
|
+
// on the sandbox's clock. A worker that does not know a mark skips the line.
|
|
419
|
+
const mark = (name) => process.stdout.write(JSON.stringify({ e: "mark", name, at: Date.now() }) + "\\n");
|
|
420
|
+
mark("driver");
|
|
421
|
+
let modelSpoke = false;
|
|
422
|
+
const emit = (type, text, extra) => {
|
|
423
|
+
// The model's first text or tool call is where start-up ends and the
|
|
424
|
+
// step's own work begins — runtime and tool lines before it are set-up.
|
|
425
|
+
if (!modelSpoke && (type === "text" || type === "tool")) { modelSpoke = true; mark("model"); }
|
|
426
|
+
process.stdout.write(JSON.stringify({ e: "event", type, text, ...extra }) + "\\n");
|
|
427
|
+
};
|
|
378
428
|
// What this sandbox actually touched, read at the last moment from the
|
|
379
429
|
// kernel's own books: cgroup v2 for CPU busy-time and peak memory,
|
|
380
430
|
// /proc/net/dev for bytes on the wire. Every read is best-effort — gVisor
|
|
@@ -430,6 +480,7 @@ try {
|
|
|
430
480
|
const { prepareRuntime } = await import("@foldrun/core/runtime");
|
|
431
481
|
const { materializeFileSecrets } = await import("@foldrun/core/secret-files");
|
|
432
482
|
const { paidWebActions } = await import("@foldrun/core/browser-pod");
|
|
483
|
+
mark("imports");
|
|
433
484
|
|
|
434
485
|
const agentDir = "/workspace/" + input.agentRel;
|
|
435
486
|
// HOME is where a bare relative path lands when anything resolves one
|
|
@@ -512,6 +563,7 @@ try {
|
|
|
512
563
|
const ask = input.ask ? buildAskTool({ channel, timeoutSec: input.ask.timeoutSec, emit }) : { server: null };
|
|
513
564
|
const inbox = channel ? inboxReader(channel, emit) : undefined;
|
|
514
565
|
|
|
566
|
+
mark("tools");
|
|
515
567
|
const outcome = await executeStep({
|
|
516
568
|
agentDir,
|
|
517
569
|
workspaceRoot: "/workspace",
|
|
@@ -1016,7 +1068,14 @@ export function parseDriverLine(line) {
|
|
|
1016
1068
|
usage: parsed.usage &&
|
|
1017
1069
|
typeof parsed.usage.inputTokens === "number" &&
|
|
1018
1070
|
typeof parsed.usage.outputTokens === "number"
|
|
1019
|
-
? {
|
|
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
|
+
}
|
|
1020
1079
|
: null,
|
|
1021
1080
|
res: parsed.res && typeof parsed.res === "object"
|
|
1022
1081
|
? {
|
|
@@ -1260,6 +1319,8 @@ export async function runStepInContainer(args) {
|
|
|
1260
1319
|
throw new Error(`container create failed:\n${create.stderr}`);
|
|
1261
1320
|
containerId = create.stdout.trim();
|
|
1262
1321
|
const sandboxMs = Date.now() - t0;
|
|
1322
|
+
// Docker's phases are measured from create, its rent clock.
|
|
1323
|
+
const phases = { ready: sandboxMs };
|
|
1263
1324
|
const cp = (from, to) => {
|
|
1264
1325
|
const out = spawnSync(cli(), ["cp", from, to], { encoding: "utf8" });
|
|
1265
1326
|
if (out.status !== 0)
|
|
@@ -1268,6 +1329,7 @@ export async function runStepInContainer(args) {
|
|
|
1268
1329
|
cp(`${wsIn}/.`, `${containerId}:/workspace`);
|
|
1269
1330
|
cp(`${libIn}/.`, `${containerId}:/library`);
|
|
1270
1331
|
cp(`${jobIn}/input.json`, `${containerId}:/opt/runner/job/input.json`);
|
|
1332
|
+
phases.files = Date.now() - t0;
|
|
1271
1333
|
// Start attached and read the driver's JSONL. The step timeout gets a
|
|
1272
1334
|
// backstop out here — a wedged container is killed, not waited on.
|
|
1273
1335
|
let firstOutputAt = null;
|
|
@@ -1292,6 +1354,11 @@ export async function runStepInContainer(args) {
|
|
|
1292
1354
|
break;
|
|
1293
1355
|
const line = buffer.slice(0, nl);
|
|
1294
1356
|
buffer = buffer.slice(nl + 1);
|
|
1357
|
+
const marked = parseDriverMark(line);
|
|
1358
|
+
if (marked) {
|
|
1359
|
+
phases[marked.name] ??= Math.max(0, marked.at - t0);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1295
1362
|
const parsed = parseDriverLine(line);
|
|
1296
1363
|
if (!parsed)
|
|
1297
1364
|
continue;
|
|
@@ -1339,6 +1406,7 @@ export async function runStepInContainer(args) {
|
|
|
1339
1406
|
sandboxMs,
|
|
1340
1407
|
firstOutputMs: firstOutputAt === null ? null : firstOutputAt - t0 - sandboxMs,
|
|
1341
1408
|
totalMs: Date.now() - t0,
|
|
1409
|
+
phases,
|
|
1342
1410
|
};
|
|
1343
1411
|
args.emit("info", timingLine(timing));
|
|
1344
1412
|
return { ...outcome, timing };
|
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, 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));
|
|
@@ -2687,6 +2689,12 @@ stepIndex) {
|
|
|
2687
2689
|
const secs = (ms) => Math.round(ms) / 1000;
|
|
2688
2690
|
step.computeSecs = outcome.timing ? secs(outcome.timing.totalMs) : null;
|
|
2689
2691
|
step.startupSecs = outcome.timing ? secs(outcome.timing.sandboxMs) : null;
|
|
2692
|
+
// Where the start-up went, phase by phase (run-container.ts#STARTUP_PHASES),
|
|
2693
|
+
// in seconds — what the cold-start numbers are computed from.
|
|
2694
|
+
const phases = outcome.timing?.phases;
|
|
2695
|
+
step.startup = phases && Object.keys(phases).length
|
|
2696
|
+
? Object.fromEntries(Object.entries(phases).map(([k, v]) => [k, secs(v)]))
|
|
2697
|
+
: null;
|
|
2690
2698
|
// Which reservation those seconds held — as numbers, because the bill
|
|
2691
2699
|
// is per core-second and per GiB-second, and what was held must come
|
|
2692
2700
|
// from the record, never from what the config says at billing time.
|
|
@@ -2864,9 +2872,7 @@ stepIndex) {
|
|
|
2864
2872
|
const consultCost = consultTools.drainCost();
|
|
2865
2873
|
const stepCost = repriced(catalog, wireModel, outcome.usage, outcome.costUsd, push, outcome.turnsCostUsd);
|
|
2866
2874
|
step.costUsd = stepCost === null && consultCost === 0 ? null : (stepCost ?? 0) + consultCost;
|
|
2867
|
-
step.tokens = outcome.usage
|
|
2868
|
-
? { input: outcome.usage.inputTokens, output: outcome.usage.outputTokens }
|
|
2869
|
-
: null;
|
|
2875
|
+
step.tokens = tokensOf(outcome.usage);
|
|
2870
2876
|
for (const line of apiTools.drainLog())
|
|
2871
2877
|
push("info", `api: ${line}`);
|
|
2872
2878
|
for (const line of scriptTools.drainLog())
|
|
@@ -4768,7 +4774,9 @@ function lastActivity(run) {
|
|
|
4768
4774
|
export function reconcileRuns(tenant, now = Date.now(), live) {
|
|
4769
4775
|
const closed = [];
|
|
4770
4776
|
for (const workspace of listWorkspaces(tenant)) {
|
|
4771
|
-
|
|
4777
|
+
// Live runs only, read whole — every other status is passed over below,
|
|
4778
|
+
// and parsing the workspace's whole history every sweep was the cost.
|
|
4779
|
+
for (const summary of liveRuns(tenant, workspace.name)) {
|
|
4772
4780
|
if (drivingRuns.has(summary.id))
|
|
4773
4781
|
continue;
|
|
4774
4782
|
// Driven by another process — a second worker pod, on a platform with
|
package/dist/src/secrets.js
CHANGED
|
@@ -218,11 +218,9 @@ export function setSecretSites(tenant, name, sites, workspace) {
|
|
|
218
218
|
export function secretSites(tenant, names, workspace) {
|
|
219
219
|
const out = {};
|
|
220
220
|
const own = workspace ? read(tenant, workspace) : {};
|
|
221
|
-
const source = workspace ? platform.previewSourceOf(tenant, workspace) : null;
|
|
222
|
-
const inherited = source ? read(tenant, source) : {};
|
|
223
221
|
const account = read(tenant);
|
|
224
222
|
for (const name of names) {
|
|
225
|
-
const rec = own[name] ??
|
|
223
|
+
const rec = own[name] ?? account[name];
|
|
226
224
|
if (rec?.sites?.length)
|
|
227
225
|
out[name] = [...rec.sites];
|
|
228
226
|
}
|
|
@@ -298,15 +296,6 @@ export function getSecret(tenant, name, workspace) {
|
|
|
298
296
|
const own = decrypt(tenant, read(tenant, workspace)[name]);
|
|
299
297
|
if (own !== null)
|
|
300
298
|
return { value: own, scope: "workspace" };
|
|
301
|
-
// A preview is a branch of its source workspace, and the branch's
|
|
302
|
-
// agents need the same credentials the source's do. Its own store is
|
|
303
|
-
// still consulted first, so a preview can override one on purpose.
|
|
304
|
-
const source = platform.previewSourceOf(tenant, workspace);
|
|
305
|
-
if (source) {
|
|
306
|
-
const inherited = decrypt(tenant, read(tenant, source)[name]);
|
|
307
|
-
if (inherited !== null)
|
|
308
|
-
return { value: inherited, scope: "workspace" };
|
|
309
|
-
}
|
|
310
299
|
}
|
|
311
300
|
const shared = decrypt(tenant, read(tenant)[name]);
|
|
312
301
|
return shared === null ? null : { value: shared, scope: "account" };
|
package/dist/src/starter.js
CHANGED
|
@@ -373,8 +373,8 @@ Debugging a failed run is usually: \`runs\` to find it, \`runs/<id>\` to read wh
|
|
|
373
373
|
step failed and what it cost, fix the markdown, \`deploy\`, then \`rerun\`.
|
|
374
374
|
|
|
375
375
|
Every route, with request and response shapes, is in \`docs/api.md\` in the
|
|
376
|
-
foldrun repository — including secrets, keys, billing, OAuth, webhooks and
|
|
377
|
-
|
|
376
|
+
foldrun repository — including secrets, keys, billing, OAuth, webhooks and
|
|
377
|
+
restore.
|
|
378
378
|
|
|
379
379
|
## Writing a flow
|
|
380
380
|
|
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
|
};
|
|
@@ -754,15 +759,6 @@ export interface WorkspaceSummary {
|
|
|
754
759
|
flows: number;
|
|
755
760
|
deployedAt: string;
|
|
756
761
|
runCount: number;
|
|
757
|
-
/** Set when this workspace is a branch's preview of another (preview.ts):
|
|
758
|
-
* which one, which branch, which commit. Read from the marker beside the
|
|
759
|
-
* directory here rather than through preview.ts, which imports this file. */
|
|
760
|
-
preview?: {
|
|
761
|
-
source: string;
|
|
762
|
-
branch: string;
|
|
763
|
-
commit: string;
|
|
764
|
-
at: string;
|
|
765
|
-
};
|
|
766
762
|
/** The workspace's picture, as a URL, when the page that lists it looked
|
|
767
763
|
* one up (avatars.ts) — the store itself does not. */
|
|
768
764
|
avatar?: string | null;
|
|
@@ -824,7 +820,7 @@ export interface ChangeMeta {
|
|
|
824
820
|
}
|
|
825
821
|
export declare function writeWorkspaceFile(tenant: string, workspace: string, rel: string, content: string, meta?: ChangeMeta): void;
|
|
826
822
|
/** Tell the platform, and never let its listener fail a write. */
|
|
827
|
-
declare function workspaceChanged(tenant: string, workspace: string, why: "write" | "deploy" | "
|
|
823
|
+
declare function workspaceChanged(tenant: string, workspace: string, why: "write" | "deploy" | "delete"): void;
|
|
828
824
|
export { workspaceChanged as notifyWorkspaceChanged };
|
|
829
825
|
export declare function syncBundleFor(file: string, change?: "Creation" | "Update"): void;
|
|
830
826
|
/**
|
|
@@ -957,10 +953,7 @@ export interface StepAttempt {
|
|
|
957
953
|
* of its own (image slim), before the full go's. */
|
|
958
954
|
status: "completed" | "failed" | "lost";
|
|
959
955
|
costUsd: number | null;
|
|
960
|
-
tokens:
|
|
961
|
-
input: number;
|
|
962
|
-
output: number;
|
|
963
|
-
} | null;
|
|
956
|
+
tokens: TokenUsage | null;
|
|
964
957
|
computeSecs: number | null;
|
|
965
958
|
startedAt: string;
|
|
966
959
|
finishedAt: string;
|
|
@@ -976,6 +969,44 @@ export interface StepAttempt {
|
|
|
976
969
|
* attempt reported the figure — an in-process step rents no sandbox, and
|
|
977
970
|
* billing reads null as zero, not as "unknown, charge something". */
|
|
978
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;
|
|
979
1010
|
export interface StepRecord {
|
|
980
1011
|
agent: string;
|
|
981
1012
|
instruction: string;
|
|
@@ -1144,6 +1175,12 @@ export interface StepRecord {
|
|
|
1144
1175
|
*/
|
|
1145
1176
|
computeSecs?: number | null;
|
|
1146
1177
|
startupSecs?: number | null;
|
|
1178
|
+
/** When each start-up phase ended, in seconds since the executor began
|
|
1179
|
+
* the step — `services`, `applied`, `ready`, `files`, `go`, `shim`,
|
|
1180
|
+
* `system`, `driver`, `imports`, `tools`, `model` (run-container.ts
|
|
1181
|
+
* #STARTUP_PHASES). Only the phases this step went through; null from
|
|
1182
|
+
* an executor that marks none, absent on steps from before 17 Oct 2026. */
|
|
1183
|
+
startup?: Record<string, number> | null;
|
|
1147
1184
|
/** Which reservation class the sandbox held (small | large), and the
|
|
1148
1185
|
* actual limits it held, as numbers. Facts of the step: a price is per
|
|
1149
1186
|
* core-second, so what was reserved must come from the record, never
|
|
@@ -1157,10 +1194,7 @@ export interface StepRecord {
|
|
|
1157
1194
|
/** Token counts behind costUsd, kept so usage reports can say "how many"
|
|
1158
1195
|
* and not only "how much" — the price of a token changes, the count is
|
|
1159
1196
|
* the fact. Absent on steps recorded before this existed. */
|
|
1160
|
-
tokens?:
|
|
1161
|
-
input: number;
|
|
1162
|
-
output: number;
|
|
1163
|
-
} | null;
|
|
1197
|
+
tokens?: TokenUsage | null;
|
|
1164
1198
|
/** What the sandbox actually touched, against what it reserved: CPU
|
|
1165
1199
|
* busy-seconds and peak memory from the pod's own cgroup, bytes on the
|
|
1166
1200
|
* wire from /proc/net/dev. Null per-field where the kernel interface
|
|
@@ -1288,6 +1322,14 @@ export declare function listRunOutputs(tenant: string, workspace: string, runId:
|
|
|
1288
1322
|
export declare function runOutputPath(tenant: string, workspace: string, runId: string, agent: string, rel: string): string | null;
|
|
1289
1323
|
export declare function readRun(tenant: string, workspace: string, runId: string): RunRecord | null;
|
|
1290
1324
|
export declare function writeRun(tenant: string, workspace: string, run: RunRecord): void;
|
|
1325
|
+
/**
|
|
1326
|
+
* The workspace's live runs — queued, running, or parked at a gate — read
|
|
1327
|
+
* whole, newest first. Off the run index: only the live records are opened,
|
|
1328
|
+
* where listRuns parses every record the workspace ever kept. What the
|
|
1329
|
+
* periodic sweeps want (reconcile, the watchdog), and on a box that keeps
|
|
1330
|
+
* ninety days of runs that is a handful of files instead of thousands.
|
|
1331
|
+
*/
|
|
1332
|
+
export declare function liveRuns(tenant: string, workspace: string): RunRecord[];
|
|
1291
1333
|
export interface RunWithWorkspace extends RunRecord {
|
|
1292
1334
|
workspace: string;
|
|
1293
1335
|
}
|
|
@@ -1314,10 +1356,7 @@ export interface RunSummaryStep {
|
|
|
1314
1356
|
group: number;
|
|
1315
1357
|
status: StepRecord["status"];
|
|
1316
1358
|
costUsd: number | null;
|
|
1317
|
-
tokens?:
|
|
1318
|
-
input: number;
|
|
1319
|
-
output: number;
|
|
1320
|
-
} | null;
|
|
1359
|
+
tokens?: TokenUsage | null;
|
|
1321
1360
|
computeSecs?: number | null;
|
|
1322
1361
|
skipReason?: string;
|
|
1323
1362
|
/** The last error event, for a failed step — what runFailure reads. */
|