@foldrun/core 2.1.0 → 2.3.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/api-tools.d.ts +5 -1
- package/dist/src/api-tools.js +19 -10
- package/dist/src/catalog.d.ts +35 -2
- package/dist/src/catalog.js +39 -12
- package/dist/src/run-container.d.ts +6 -4
- package/dist/src/run-container.js +9 -1
- package/dist/src/runner.js +22 -7
- package/dist/src/script-tools.d.ts +4 -1
- package/dist/src/script-tools.js +9 -3
- package/dist/src/step-exec.d.ts +14 -14
- package/dist/src/step-exec.js +16 -16
- package/dist/src/store.d.ts +17 -39
- package/dist/src/store.js +4 -52
- package/dist/src/tokens.d.ts +38 -0
- package/dist/src/tokens.js +58 -0
- package/dist/src/tool-output.d.ts +29 -0
- package/dist/src/tool-output.js +64 -0
- package/package.json +1 -1
- package/src/api-tools.ts +17 -7
- package/src/catalog.ts +60 -12
- package/src/run-container.ts +15 -3
- package/src/runner.ts +22 -7
- package/src/script-tools.ts +8 -2
- package/src/step-exec.ts +23 -23
- package/src/store.ts +19 -69
- package/src/tokens.ts +75 -0
- package/src/tool-output.ts +82 -0
package/dist/src/api-tools.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import type { ApiSpec } from "./store.ts";
|
|
3
|
+
import { type KeepIn } from "./tool-output.ts";
|
|
3
4
|
export declare function secretsUsedByApi(api: ApiSpec): string[];
|
|
4
5
|
/**
|
|
5
6
|
* A token bucket: `count` tokens, refilled at `perSec`. `take()` resolves
|
|
@@ -43,4 +44,7 @@ export interface ApiToolResult {
|
|
|
43
44
|
export declare function buildApiTools(tenant: string, apis: ApiSpec[], workspace?: string, resolved?: {
|
|
44
45
|
env: Record<string, string | undefined>;
|
|
45
46
|
missing: string[];
|
|
46
|
-
}
|
|
47
|
+
},
|
|
48
|
+
/** Where a body too long to send whole is kept and previewed, when the
|
|
49
|
+
* agent can read it back (tool-output.ts). Without it, the old 20k cut. */
|
|
50
|
+
keepIn?: KeepIn): ApiToolResult;
|
package/dist/src/api-tools.js
CHANGED
|
@@ -16,8 +16,11 @@ import crypto from "node:crypto";
|
|
|
16
16
|
import { EGRESS_ENV, viaEgress } from "./egress.js";
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
19
|
+
import { previewOrKeep, RESULT_KEEP_CHARS } from "./tool-output.js";
|
|
19
20
|
import { resolveSecrets, materializeSecrets } from "./secrets.js";
|
|
20
|
-
|
|
21
|
+
// Collected whole up to this; the model sees a preview and where the rest is
|
|
22
|
+
// kept (tool-output.ts), not a cut with the rest gone.
|
|
23
|
+
const MAX_BODY_CHARS = RESULT_KEEP_CHARS;
|
|
21
24
|
/** MCP tool names are capped; ours also carry the server prefix on the wire. */
|
|
22
25
|
const MAX_TOOL_NAME = 64;
|
|
23
26
|
/** How many typed tools the prompt lists by name before summarising. */
|
|
@@ -92,7 +95,9 @@ export function typedToolName(apiName, opId) {
|
|
|
92
95
|
* the truncation, the log line. Typed and generic tools differ only in how
|
|
93
96
|
* they arrive at a Request.
|
|
94
97
|
*/
|
|
95
|
-
async function performRequest(api, env, bucket, log, req
|
|
98
|
+
async function performRequest(api, env, bucket, log, req,
|
|
99
|
+
/** Where a long body is kept, when the agent can read it back (tool-output.ts). */
|
|
100
|
+
keepIn) {
|
|
96
101
|
const started = Date.now();
|
|
97
102
|
try {
|
|
98
103
|
const waited = bucket ? await bucket.take() : 0;
|
|
@@ -133,9 +138,10 @@ async function performRequest(api, env, bucket, log, req) {
|
|
|
133
138
|
...(api.timeout ? { signal: AbortSignal.timeout(api.timeout * 1000) } : {}),
|
|
134
139
|
});
|
|
135
140
|
const text = await res.text();
|
|
136
|
-
const
|
|
141
|
+
const body = text.length > MAX_BODY_CHARS
|
|
137
142
|
? `${text.slice(0, MAX_BODY_CHARS)}\n…[truncated ${text.length - MAX_BODY_CHARS} chars]`
|
|
138
143
|
: text;
|
|
144
|
+
const truncated = previewOrKeep(body, keepIn, `${api.name}-${req.method.toLowerCase()}`);
|
|
139
145
|
log.push(`${req.method} ${url.pathname}${url.search ? "?…" : ""} → ${res.status} (${Date.now() - started}ms)${waitNote}`);
|
|
140
146
|
return {
|
|
141
147
|
content: [{ type: "text", text: `HTTP ${res.status} ${res.statusText}\n\n${truncated}` }],
|
|
@@ -177,7 +183,7 @@ function argKeys(op) {
|
|
|
177
183
|
}
|
|
178
184
|
return out;
|
|
179
185
|
}
|
|
180
|
-
function typedTool(api, op, env, bucket, log) {
|
|
186
|
+
function typedTool(api, op, env, bucket, log, keepIn) {
|
|
181
187
|
const keys = argKeys(op);
|
|
182
188
|
const shape = {};
|
|
183
189
|
for (const [key, p] of keys)
|
|
@@ -229,10 +235,10 @@ function typedTool(api, op, env, bucket, log) {
|
|
|
229
235
|
query,
|
|
230
236
|
headers,
|
|
231
237
|
body: typeof values.body === "string" ? values.body : undefined,
|
|
232
|
-
});
|
|
238
|
+
}, keepIn);
|
|
233
239
|
});
|
|
234
240
|
}
|
|
235
|
-
function genericTool(api, env, bucket, log) {
|
|
241
|
+
function genericTool(api, env, bucket, log, keepIn) {
|
|
236
242
|
const typed = api.resolvedOperations?.length
|
|
237
243
|
? ` Prefer the typed ${api.name}_* tools; this one is the escape hatch for operations they do not cover.`
|
|
238
244
|
: "";
|
|
@@ -252,12 +258,15 @@ function genericTool(api, env, bucket, log) {
|
|
|
252
258
|
query: args.query ?? {},
|
|
253
259
|
headers: {},
|
|
254
260
|
body: args.body,
|
|
255
|
-
}));
|
|
261
|
+
}, keepIn));
|
|
256
262
|
}
|
|
257
263
|
export function buildApiTools(tenant, apis, workspace,
|
|
258
264
|
// The runner container passes this: secrets were resolved before the specs
|
|
259
265
|
// crossed the boundary, and there is no vault in there to ask.
|
|
260
|
-
resolved
|
|
266
|
+
resolved,
|
|
267
|
+
/** Where a body too long to send whole is kept and previewed, when the
|
|
268
|
+
* agent can read it back (tool-output.ts). Without it, the old 20k cut. */
|
|
269
|
+
keepIn) {
|
|
261
270
|
if (apis.length === 0) {
|
|
262
271
|
return { server: null, toolNames: [], owners: {}, methods: {}, missingSecrets: [], promptLines: [], drainLog: () => [] };
|
|
263
272
|
}
|
|
@@ -302,12 +311,12 @@ resolved) {
|
|
|
302
311
|
}
|
|
303
312
|
const typedNames = [];
|
|
304
313
|
for (const op of ops) {
|
|
305
|
-
tools.push(typedTool(api, op, env, bucket, log));
|
|
314
|
+
tools.push(typedTool(api, op, env, bucket, log, keepIn));
|
|
306
315
|
typedNames.push(typedToolName(api.name, op.id));
|
|
307
316
|
methods[`mcp__foldrun_apis__${typedToolName(api.name, op.id)}`] = String(op.method ?? "").toUpperCase();
|
|
308
317
|
}
|
|
309
318
|
if (withGeneric)
|
|
310
|
-
tools.push(genericTool(api, env, bucket, log));
|
|
319
|
+
tools.push(genericTool(api, env, bucket, log, keepIn));
|
|
311
320
|
const names = [...typedNames, ...(withGeneric ? [`call_${api.name}`] : [])];
|
|
312
321
|
for (const n of names) {
|
|
313
322
|
toolNames.push(`mcp__foldrun_apis__${n}`);
|
package/dist/src/catalog.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export interface CatalogModel {
|
|
|
8
8
|
promptPrice: number | null;
|
|
9
9
|
/** USD per output token. */
|
|
10
10
|
completionPrice: number | null;
|
|
11
|
+
/** USD per input token read from / written to the prompt cache, when the
|
|
12
|
+
* gateway quotes them (OpenRouter's input_cache_read / input_cache_write).
|
|
13
|
+
* Models discount their cache differently — GPT-4o reads at half the
|
|
14
|
+
* input price, Claude at a tenth — so a quoted price beats any multiplier.
|
|
15
|
+
* Absent on catalogues cached before 2.1.1. */
|
|
16
|
+
cacheReadPrice?: number | null;
|
|
17
|
+
cacheWritePrice?: number | null;
|
|
11
18
|
/** Can this model make tool calls? Null when the gateway doesn't say. */
|
|
12
19
|
tools: boolean | null;
|
|
13
20
|
/** Does it take a reasoning-depth parameter? (What `effort:` needs.) */
|
|
@@ -23,6 +30,8 @@ export interface Catalog {
|
|
|
23
30
|
fetchedAt: string;
|
|
24
31
|
models: CatalogModel[];
|
|
25
32
|
}
|
|
33
|
+
/** The gateway's /models payload as entries. Exported for its tests. */
|
|
34
|
+
export declare function parseModels(payload: unknown): CatalogModel[];
|
|
26
35
|
/** The cache as it stands, however old — null only when nothing was ever
|
|
27
36
|
* fetched. Reading never touches the network. */
|
|
28
37
|
export declare function readCatalog(baseUrl: string): Catalog | null;
|
|
@@ -86,9 +95,33 @@ export declare function catalogCost(catalog: Catalog | null, modelId: string, us
|
|
|
86
95
|
cacheReadTokens?: number;
|
|
87
96
|
cacheWriteTokens?: number;
|
|
88
97
|
}): number | null;
|
|
98
|
+
/** What a token costs on one model: input, output, and — when the gateway
|
|
99
|
+
* quotes them — reads from and writes to the prompt cache. */
|
|
100
|
+
export interface TokenPrice {
|
|
101
|
+
input: number;
|
|
102
|
+
output: number;
|
|
103
|
+
cacheRead?: number | null;
|
|
104
|
+
cacheWrite?: number | null;
|
|
105
|
+
}
|
|
106
|
+
/** A catalogue entry's prices, or null when it has no input/output price. */
|
|
107
|
+
export declare function tokenPriceOf(m: CatalogModel | null | undefined): TokenPrice | null;
|
|
108
|
+
/**
|
|
109
|
+
* The cost of a set of counts at a price. `inputTokens` is all input; the
|
|
110
|
+
* cache fields are parts of it. Cached input is priced at the model's own
|
|
111
|
+
* cache prices when the gateway quotes them, and only otherwise at
|
|
112
|
+
* Anthropic's multipliers over the input price. Before the counts were
|
|
113
|
+
* split (18 Oct 2026) every cached token was billed at the full rate.
|
|
114
|
+
*/
|
|
115
|
+
export declare function costOf(usage: {
|
|
116
|
+
inputTokens: number;
|
|
117
|
+
outputTokens: number;
|
|
118
|
+
cacheReadTokens?: number;
|
|
119
|
+
cacheWriteTokens?: number;
|
|
120
|
+
}, p: TokenPrice): number;
|
|
89
121
|
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
90
|
-
* quarter more than fresh input, a read a tenth of it.
|
|
91
|
-
* mid-step ceiling (priceTurn) and
|
|
122
|
+
* quarter more than fresh input, a read a tenth of it. The fallback when a
|
|
123
|
+
* gateway quotes no cache price — for the mid-step ceiling (priceTurn) and
|
|
124
|
+
* the record's repricing (catalogCost) alike. */
|
|
92
125
|
export declare const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
93
126
|
export declare const CACHE_READ_MULTIPLIER = 0.1;
|
|
94
127
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
package/dist/src/catalog.js
CHANGED
|
@@ -29,7 +29,8 @@ const TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
29
29
|
const cacheFile = (baseUrl) => path.join(dataRoot(), "catalog",
|
|
30
30
|
// One file per endpoint, named legibly: catalog/openrouter.ai.json.
|
|
31
31
|
`${new URL(baseUrl).hostname.replace(/[^a-z0-9.-]/gi, "_")}.json`);
|
|
32
|
-
|
|
32
|
+
/** The gateway's /models payload as entries. Exported for its tests. */
|
|
33
|
+
export function parseModels(payload) {
|
|
33
34
|
const data = payload?.data;
|
|
34
35
|
if (!Array.isArray(data))
|
|
35
36
|
return [];
|
|
@@ -44,8 +45,14 @@ function parseModels(payload) {
|
|
|
44
45
|
? m.supported_parameters.map(String)
|
|
45
46
|
: null;
|
|
46
47
|
const pricing = (m.pricing ?? {});
|
|
48
|
+
// Unquoted is unknown, not free: Number(null) and Number("") are 0, and
|
|
49
|
+
// a gateway answering `input_cache_read: null` would have made caching
|
|
50
|
+
// cost nothing. An explicit "0" stays 0.
|
|
47
51
|
const price = (k) => {
|
|
48
|
-
const
|
|
52
|
+
const v = pricing[k];
|
|
53
|
+
if (v == null || (typeof v === "string" && v.trim() === ""))
|
|
54
|
+
return null;
|
|
55
|
+
const n = Number(v);
|
|
49
56
|
return Number.isFinite(n) ? n : null;
|
|
50
57
|
};
|
|
51
58
|
out.push({
|
|
@@ -54,6 +61,8 @@ function parseModels(payload) {
|
|
|
54
61
|
contextLength: Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : null,
|
|
55
62
|
promptPrice: price("prompt"),
|
|
56
63
|
completionPrice: price("completion"),
|
|
64
|
+
cacheReadPrice: price("input_cache_read"),
|
|
65
|
+
cacheWritePrice: price("input_cache_write"),
|
|
57
66
|
tools: supported ? supported.includes("tools") : null,
|
|
58
67
|
reasoning: supported ? supported.includes("reasoning") : null,
|
|
59
68
|
efforts: Array.isArray(m.reasoning?.supported_efforts)
|
|
@@ -200,21 +209,39 @@ export function catalogCost(catalog, modelId, usage) {
|
|
|
200
209
|
const m = findModel(catalog, modelId);
|
|
201
210
|
if (!m || m.promptPrice == null || m.completionPrice == null)
|
|
202
211
|
return null;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
return costOf(usage, tokenPriceOf(m));
|
|
213
|
+
}
|
|
214
|
+
/** A catalogue entry's prices, or null when it has no input/output price. */
|
|
215
|
+
export function tokenPriceOf(m) {
|
|
216
|
+
if (!m || m.promptPrice == null || m.completionPrice == null)
|
|
217
|
+
return null;
|
|
218
|
+
return {
|
|
219
|
+
input: m.promptPrice,
|
|
220
|
+
output: m.completionPrice,
|
|
221
|
+
...(m.cacheReadPrice != null ? { cacheRead: m.cacheReadPrice } : {}),
|
|
222
|
+
...(m.cacheWritePrice != null ? { cacheWrite: m.cacheWritePrice } : {}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The cost of a set of counts at a price. `inputTokens` is all input; the
|
|
227
|
+
* cache fields are parts of it. Cached input is priced at the model's own
|
|
228
|
+
* cache prices when the gateway quotes them, and only otherwise at
|
|
229
|
+
* Anthropic's multipliers over the input price. Before the counts were
|
|
230
|
+
* split (18 Oct 2026) every cached token was billed at the full rate.
|
|
231
|
+
*/
|
|
232
|
+
export function costOf(usage, p) {
|
|
209
233
|
const read = usage.cacheReadTokens ?? 0;
|
|
210
234
|
const written = usage.cacheWriteTokens ?? 0;
|
|
211
235
|
const fresh = Math.max(0, usage.inputTokens - read - written);
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
return (fresh * p.input +
|
|
237
|
+
written * (p.cacheWrite ?? p.input * CACHE_WRITE_MULTIPLIER) +
|
|
238
|
+
read * (p.cacheRead ?? p.input * CACHE_READ_MULTIPLIER) +
|
|
239
|
+
usage.outputTokens * p.output);
|
|
214
240
|
}
|
|
215
241
|
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
216
|
-
* quarter more than fresh input, a read a tenth of it.
|
|
217
|
-
* mid-step ceiling (priceTurn) and
|
|
242
|
+
* quarter more than fresh input, a read a tenth of it. The fallback when a
|
|
243
|
+
* gateway quotes no cache price — for the mid-step ceiling (priceTurn) and
|
|
244
|
+
* the record's repricing (catalogCost) alike. */
|
|
218
245
|
export const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
219
246
|
export const CACHE_READ_MULTIPLIER = 0.1;
|
|
220
247
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
@@ -2,6 +2,7 @@ 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
4
|
import type { EventExtra, ExecUsage } from "./step-exec.ts";
|
|
5
|
+
import type { TokenPrice } from "./catalog.ts";
|
|
5
6
|
import { type RuntimeSpec } from "./runtime.ts";
|
|
6
7
|
import { type NetworkLevel } from "./network.ts";
|
|
7
8
|
import type { ScriptSpec } from "./script-tools.ts";
|
|
@@ -41,10 +42,7 @@ export interface ContainerStepInput {
|
|
|
41
42
|
* step-exec.ts. Values, so the driver can enforce them in the pod. */
|
|
42
43
|
budgetUsd?: number | null;
|
|
43
44
|
budgetNote?: string;
|
|
44
|
-
price?:
|
|
45
|
-
input: number;
|
|
46
|
-
output: number;
|
|
47
|
-
} | null;
|
|
45
|
+
price?: TokenPrice | null;
|
|
48
46
|
verify?: string;
|
|
49
47
|
/** `output: json` — checked inside, where the reply is. */
|
|
50
48
|
output?: "json";
|
|
@@ -147,6 +145,9 @@ export interface ContainerStepOutcome {
|
|
|
147
145
|
data?: unknown;
|
|
148
146
|
costUsd: number | null;
|
|
149
147
|
usage?: ExecUsage | null;
|
|
148
|
+
/** Model turns, and why the last one ended — see ExecOutcome. */
|
|
149
|
+
turns?: number;
|
|
150
|
+
stopReason?: string | null;
|
|
150
151
|
/** Every turn priced at its own model, when sub-agents ran on others —
|
|
151
152
|
* see ExecOutcome.turnsCostUsd. */
|
|
152
153
|
turnsCostUsd?: number;
|
|
@@ -368,6 +369,7 @@ export declare function parseDriverLine(line: string): {
|
|
|
368
369
|
call?: string;
|
|
369
370
|
ms?: number;
|
|
370
371
|
err?: boolean;
|
|
372
|
+
bytes?: number;
|
|
371
373
|
operator?: OperatorEvent;
|
|
372
374
|
subagent?: string;
|
|
373
375
|
check?: boolean;
|
|
@@ -536,13 +536,18 @@ try {
|
|
|
536
536
|
// buildApiTools gets an already-resolved environment. verify: gets the
|
|
537
537
|
// container's own environment, whole: it is the boundary, assembled on
|
|
538
538
|
// the host from an allowlist, and the declared secrets are in it.
|
|
539
|
-
|
|
539
|
+
// A long result is kept in a file only when the agent can read it back.
|
|
540
|
+
// (tool-output.ts canReadBack, inlined: the driver imports core by subpath.)
|
|
541
|
+
const keepIn = { dir: agentDir, readable: () => (input.allowed ?? []).some((t) => t === "Read" || t === "Grep") };
|
|
542
|
+
const api = buildApiTools("", input.apis, undefined, { env, missing: [] }, keepIn);
|
|
540
543
|
const script = buildScriptTools(
|
|
541
544
|
agentDir,
|
|
542
545
|
input.scripts,
|
|
543
546
|
{ ...env, ...runtime.env },
|
|
544
547
|
"/library/scripts",
|
|
545
548
|
runtime.interpreters,
|
|
549
|
+
null,
|
|
550
|
+
keepIn.readable,
|
|
546
551
|
);
|
|
547
552
|
// The translator, when the provider speaks Chat Completions: a loopback
|
|
548
553
|
// server for the length of this step. Its env replaces the endpoint the
|
|
@@ -1049,6 +1054,7 @@ export function parseDriverLine(line) {
|
|
|
1049
1054
|
...(typeof parsed.call === "string" ? { call: parsed.call } : {}),
|
|
1050
1055
|
...(typeof parsed.ms === "number" ? { ms: parsed.ms } : {}),
|
|
1051
1056
|
...(parsed.err === true ? { err: true } : {}),
|
|
1057
|
+
...(typeof parsed.bytes === "number" && parsed.bytes >= 0 ? { bytes: parsed.bytes } : {}),
|
|
1052
1058
|
...(isOperatorEvent(parsed.operator) ? { operator: parsed.operator } : {}),
|
|
1053
1059
|
...(typeof parsed.subagent === "string" && /^[a-z0-9-]{1,64}$/.test(parsed.subagent) ? { subagent: parsed.subagent } : {}),
|
|
1054
1060
|
...(parsed.check === true ? { check: true } : {}),
|
|
@@ -1064,6 +1070,8 @@ export function parseDriverLine(line) {
|
|
|
1064
1070
|
...("data" in parsed ? { data: parsed.data } : {}),
|
|
1065
1071
|
costUsd: typeof parsed.costUsd === "number" ? parsed.costUsd : null,
|
|
1066
1072
|
...(typeof parsed.turnsCostUsd === "number" ? { turnsCostUsd: parsed.turnsCostUsd } : {}),
|
|
1073
|
+
...(typeof parsed.turns === "number" && parsed.turns >= 0 ? { turns: parsed.turns } : {}),
|
|
1074
|
+
...(typeof parsed.stopReason === "string" ? { stopReason: parsed.stopReason.slice(0, 64) } : {}),
|
|
1067
1075
|
...(parseBrowserPod(parsed.browserPod) ? { browserPod: parseBrowserPod(parsed.browserPod) } : {}),
|
|
1068
1076
|
usage: parsed.usage &&
|
|
1069
1077
|
typeof parsed.usage.inputTokens === "number" &&
|
package/dist/src/runner.js
CHANGED
|
@@ -34,7 +34,8 @@ 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
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
|
-
import {
|
|
37
|
+
import { canReadBack } from "./tool-output.js";
|
|
38
|
+
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel, tokenPriceOf } from "./catalog.js";
|
|
38
39
|
import { resolveSecrets, getSecret, materializeSecrets, secretSites } from "./secrets.js";
|
|
39
40
|
import { resolveNetwork, networkLine, proxyEnv, EGRESS_CA_ENV } from "./network.js";
|
|
40
41
|
import { materializeFileSecrets, cleanupFileSecrets } from "./secret-files.js";
|
|
@@ -920,7 +921,12 @@ grantsOnly = false) {
|
|
|
920
921
|
const attached = attachOperations(apis, tenant, path.resolve(agentDir, "..", ".."));
|
|
921
922
|
const apiWarnings = attached.warnings;
|
|
922
923
|
apis.splice(0, apis.length, ...attached.specs);
|
|
923
|
-
|
|
924
|
+
// A long tool result is kept in a file only for an agent that can read it
|
|
925
|
+
// back (tool-output.ts). The grants are settled further down, after the
|
|
926
|
+
// tools are built, so the tools ask when they are called.
|
|
927
|
+
let readsFiles = false;
|
|
928
|
+
const keepIn = { dir: agentDir, readable: () => readsFiles };
|
|
929
|
+
const apiTools = buildApiTools(tenant, apis, workspace, undefined, keepIn);
|
|
924
930
|
if (apiTools.promptLines.length) {
|
|
925
931
|
parts.push(`# APIs you can call\n\n${apiTools.promptLines.join("\n")}`);
|
|
926
932
|
}
|
|
@@ -1254,7 +1260,7 @@ grantsOnly = false) {
|
|
|
1254
1260
|
// (/workspace/agents/<agent>) do not carry the name.
|
|
1255
1261
|
FOLDRUN_WORKSPACE: workspace,
|
|
1256
1262
|
...clockEnv,
|
|
1257
|
-
}, libraryDir(tenant, "scripts"), runtime.interpreters, exec);
|
|
1263
|
+
}, libraryDir(tenant, "scripts"), runtime.interpreters, exec, keepIn.readable);
|
|
1258
1264
|
parts.push(`# Clock\n\nToday is ${clockEnv.FOLDRUN_DATE} (${timezone}). Use this date for anything you ` +
|
|
1259
1265
|
`write or check — it is the workspace's calendar, not the container's clock.`);
|
|
1260
1266
|
if (scriptTools.promptLines.length) {
|
|
@@ -1462,6 +1468,7 @@ grantsOnly = false) {
|
|
|
1462
1468
|
if (denied.includes("install"))
|
|
1463
1469
|
denied.push(INSTALL_TOOL);
|
|
1464
1470
|
const finalAllowed = allowed.filter((t) => !denied.includes(t));
|
|
1471
|
+
readsFiles = canReadBack(finalAllowed);
|
|
1465
1472
|
return {
|
|
1466
1473
|
front,
|
|
1467
1474
|
clockEnv,
|
|
@@ -1621,6 +1628,7 @@ reattach = false) {
|
|
|
1621
1628
|
status,
|
|
1622
1629
|
costUsd: step.costUsd,
|
|
1623
1630
|
tokens: step.tokens ?? null,
|
|
1631
|
+
...(typeof step.turns === "number" ? { turns: step.turns } : {}),
|
|
1624
1632
|
computeSecs: step.computeSecs ?? null,
|
|
1625
1633
|
startedAt: step.podLostTry?.finishedAt ?? startedAt,
|
|
1626
1634
|
finishedAt: step.finishedAt ?? new Date().toISOString(),
|
|
@@ -1634,6 +1642,8 @@ reattach = false) {
|
|
|
1634
1642
|
step.costUsd = totals.costUsd;
|
|
1635
1643
|
step.tokens = totals.tokens;
|
|
1636
1644
|
step.computeSecs = totals.computeSecs;
|
|
1645
|
+
if (totals.turns !== undefined)
|
|
1646
|
+
step.turns = totals.turns;
|
|
1637
1647
|
}
|
|
1638
1648
|
/** A question an agent asked mid-step goes where approvals go (the
|
|
1639
1649
|
* workspace's notify:, `awaiting-approval` events), with the way to answer. */
|
|
@@ -1842,6 +1852,9 @@ stepIndex) {
|
|
|
1842
1852
|
const { front, clockEnv, clock, systemPrompt, allowed, disabled, apiTools, scriptTools, secretEnv, secretScopes, missingSecrets, missingTools, runtime, unknownTools, shadowed, legacyUse, retiredTools, mcpServers, mcpNames, apiSpecs, scriptSpecs, brokenTools, size: agentSize, providerEnv, providerLabel, providerSecrets, providerWarnings, formatWarning, searchChoice, fetchChoice, actionChoices, web, language, region, fallbackEnv, apiWarnings, searchRoots, historyDigest, deskDigest, searchTools, historyTools, deskTools, ask, translator, fallbackTranslator, outward, } = agentContext(agentDir, tenant, tags, { runId, agent: step.agent }, flowTimezone);
|
|
1843
1853
|
disposeRuntime = runtime.dispose;
|
|
1844
1854
|
canActOutward = outward;
|
|
1855
|
+
// Sent again on every turn; with turns and the tools' result sizes, what
|
|
1856
|
+
// a token report reads to say where a step's input went.
|
|
1857
|
+
step.systemPromptChars = systemPrompt.length;
|
|
1845
1858
|
// A retry that moved the step up a class (an evicted or OOM-killed
|
|
1846
1859
|
// attempt) wins over the agent's own `size:` for the attempts after.
|
|
1847
1860
|
const size = step.sizeUp ?? agentSize;
|
|
@@ -2002,10 +2015,7 @@ stepIndex) {
|
|
|
2002
2015
|
const verdict = checkModel(catalog, wireModel, { tools: needsTools });
|
|
2003
2016
|
// Also what a token costs here, so a step can count its own spend
|
|
2004
2017
|
// turn by turn against its ceiling instead of learning at the end.
|
|
2005
|
-
|
|
2006
|
-
if (priced && priced.promptPrice != null && priced.completionPrice != null) {
|
|
2007
|
-
tokenPrice = { input: priced.promptPrice, output: priced.completionPrice };
|
|
2008
|
-
}
|
|
2018
|
+
tokenPrice = tokenPriceOf(findModel(catalog, wireModel));
|
|
2009
2019
|
if (!verdict.ok) {
|
|
2010
2020
|
push("error", verdict.reason);
|
|
2011
2021
|
step.status = "failed";
|
|
@@ -2626,6 +2636,7 @@ stepIndex) {
|
|
|
2626
2636
|
status: "lost",
|
|
2627
2637
|
costUsd: repriced(catalog, wireModel, first.usage ?? null, first.costUsd, push, first.turnsCostUsd),
|
|
2628
2638
|
tokens: tokensOf(first.usage),
|
|
2639
|
+
...(typeof first.turns === "number" ? { turns: first.turns } : {}),
|
|
2629
2640
|
computeSecs: first.timing ? Math.round(first.timing.totalMs) / 1000 : null,
|
|
2630
2641
|
finishedAt: new Date().toISOString(),
|
|
2631
2642
|
error: decision.note.slice(0, 500),
|
|
@@ -2676,6 +2687,8 @@ stepIndex) {
|
|
|
2676
2687
|
step.data = redactData(outcome.data);
|
|
2677
2688
|
step.costUsd = repriced(catalog, wireModel, outcome.usage ?? null, outcome.costUsd, push, outcome.turnsCostUsd);
|
|
2678
2689
|
step.tokens = tokensOf(outcome.usage);
|
|
2690
|
+
step.turns = typeof outcome.turns === "number" ? outcome.turns : null;
|
|
2691
|
+
step.stopReason = outcome.stopReason ?? null;
|
|
2679
2692
|
step.actual = outcome.res ?? null;
|
|
2680
2693
|
if (outcome.res?.peakMemBytes || outcome.res?.rxBytes) {
|
|
2681
2694
|
const mb = (n) => (n === null ? "?" : (n / 1024 / 1024).toFixed(0));
|
|
@@ -2873,6 +2886,8 @@ stepIndex) {
|
|
|
2873
2886
|
const stepCost = repriced(catalog, wireModel, outcome.usage, outcome.costUsd, push, outcome.turnsCostUsd);
|
|
2874
2887
|
step.costUsd = stepCost === null && consultCost === 0 ? null : (stepCost ?? 0) + consultCost;
|
|
2875
2888
|
step.tokens = tokensOf(outcome.usage);
|
|
2889
|
+
step.turns = typeof outcome.turns === "number" ? outcome.turns : null;
|
|
2890
|
+
step.stopReason = outcome.stopReason ?? null;
|
|
2876
2891
|
for (const line of apiTools.drainLog())
|
|
2877
2892
|
push("info", `api: ${line}`);
|
|
2878
2893
|
for (const line of scriptTools.drainLog())
|
|
@@ -88,4 +88,7 @@ export declare function resolveRunPath(agentDir: string, run: string, libraryScr
|
|
|
88
88
|
* have been.
|
|
89
89
|
*/
|
|
90
90
|
export declare function scriptArgShape(args: Record<string, string>): Record<string, z.ZodTypeAny>;
|
|
91
|
-
export declare function buildScriptTools(agentDir: string, scripts: ScriptSpec[], env: Record<string, string>, libraryScripts?: string, interpreters?: Record<string, string>, exec?: ExecutionContext | null
|
|
91
|
+
export declare function buildScriptTools(agentDir: string, scripts: ScriptSpec[], env: Record<string, string>, libraryScripts?: string, interpreters?: Record<string, string>, exec?: ExecutionContext | null,
|
|
92
|
+
/** Whether the agent can read files back (Read or Grep): only then is a
|
|
93
|
+
* long output kept in a file and previewed (tool-output.ts). */
|
|
94
|
+
readable?: () => boolean): ScriptToolResult;
|
package/dist/src/script-tools.js
CHANGED
|
@@ -22,13 +22,16 @@ import { runInContainer } from "./container.js";
|
|
|
22
22
|
import { hostSafeEnv } from "./host-env.js";
|
|
23
23
|
import { workspaceLinkEnv } from "./confine.js";
|
|
24
24
|
import { platform } from "./platform.js";
|
|
25
|
+
import { previewOrKeep, RESULT_KEEP_CHARS } from "./tool-output.js";
|
|
25
26
|
// A script's only clock is the `timeout:` its own tool.md declares. There
|
|
26
27
|
// was a two-minute default and a ten-minute cap here; both were the
|
|
27
28
|
// platform's opinion about how long a person's script may take, and the
|
|
28
29
|
// deploy watcher had to be taught to call its tool four times to outlast the
|
|
29
30
|
// cap. The platform orchestrates; it does not decide that.
|
|
30
31
|
const timeoutFor = (spec) => (spec.timeout ? spec.timeout * 1000 : undefined);
|
|
31
|
-
|
|
32
|
+
// Collected up to this, then previewed and kept (tool-output.ts): the model
|
|
33
|
+
// sees the head and where the rest is, instead of a cut with the rest gone.
|
|
34
|
+
const MAX_OUTPUT = RESULT_KEEP_CHARS;
|
|
32
35
|
export function parseScripts(raw) {
|
|
33
36
|
if (!Array.isArray(raw))
|
|
34
37
|
return [];
|
|
@@ -339,7 +342,10 @@ export function scriptArgShape(args) {
|
|
|
339
342
|
}
|
|
340
343
|
return shape;
|
|
341
344
|
}
|
|
342
|
-
export function buildScriptTools(agentDir, scripts, env, libraryScripts = "", interpreters = {}, exec = null
|
|
345
|
+
export function buildScriptTools(agentDir, scripts, env, libraryScripts = "", interpreters = {}, exec = null,
|
|
346
|
+
/** Whether the agent can read files back (Read or Grep): only then is a
|
|
347
|
+
* long output kept in a file and previewed (tool-output.ts). */
|
|
348
|
+
readable = () => false) {
|
|
343
349
|
if (scripts.length === 0) {
|
|
344
350
|
return { server: null, toolNames: [], promptLines: [], drainLog: () => [] };
|
|
345
351
|
}
|
|
@@ -364,7 +370,7 @@ export function buildScriptTools(agentDir, scripts, env, libraryScripts = "", in
|
|
|
364
370
|
log.push(`${spec.name}(${shown}) → exit ${code ?? "error"} ` +
|
|
365
371
|
`(${Date.now() - started}ms, ${exec?.executor ?? "sandbox"})`);
|
|
366
372
|
return {
|
|
367
|
-
content: [{ type: "text", text: `exit ${code ?? "error"}\n\n${out}` }],
|
|
373
|
+
content: [{ type: "text", text: `exit ${code ?? "error"}\n\n${previewOrKeep(out, { dir: agentDir, readable }, spec.name)}` }],
|
|
368
374
|
isError: code !== 0,
|
|
369
375
|
};
|
|
370
376
|
});
|
package/dist/src/step-exec.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type HookDef, type HookRecord } from "./hooks.ts";
|
|
2
2
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import type { Effort } from "./store.ts";
|
|
4
|
+
import { type TokenPrice } from "./catalog.ts";
|
|
4
5
|
import type { TestEffect } from "./test-mode.ts";
|
|
5
6
|
import type { OperatorEvent } from "./operator.ts";
|
|
6
7
|
import { type SubagentSpec } from "./subagents.ts";
|
|
@@ -47,6 +48,9 @@ export interface ExecOutcome {
|
|
|
47
48
|
* end_turn, max_tokens, stop_sequence, tool_use, pause_turn, refusal or
|
|
48
49
|
* model_context_window_exceeded. Null when no result arrived. */
|
|
49
50
|
stopReason?: string | null;
|
|
51
|
+
/** Model turns the step took (the SDK's num_turns; counted from the
|
|
52
|
+
* turns seen when no closing result arrived). */
|
|
53
|
+
turns?: number;
|
|
50
54
|
/** Set when the step browsed through the account's browser pod from the
|
|
51
55
|
* slim image (ExecOptions.browserPod): its reconnects, and — when the pod
|
|
52
56
|
* was lost — why, and what the step had written by then. */
|
|
@@ -89,10 +93,7 @@ export interface ExecOptions {
|
|
|
89
93
|
/** Price per token for the model, when the catalogue knows it, so spend
|
|
90
94
|
* can be counted turn by turn instead of learned at the end. Without it
|
|
91
95
|
* the step prices its turns from the SDK's own running total. */
|
|
92
|
-
price?:
|
|
93
|
-
input: number;
|
|
94
|
-
output: number;
|
|
95
|
-
} | null;
|
|
96
|
+
price?: TokenPrice | null;
|
|
96
97
|
/** A check the step must pass to count as done: a shell command that must
|
|
97
98
|
* exit 0, or an eval-style assertion (`contains: x`, `not-contains: x`,
|
|
98
99
|
* `matches: re`, `file: path`, `judge: sentence`) — see checkVerify. */
|
|
@@ -168,6 +169,7 @@ export type EventExtra = {
|
|
|
168
169
|
call?: string;
|
|
169
170
|
ms?: number;
|
|
170
171
|
err?: boolean;
|
|
172
|
+
bytes?: number;
|
|
171
173
|
effect?: TestEffect;
|
|
172
174
|
operator?: OperatorEvent;
|
|
173
175
|
subagent?: string;
|
|
@@ -190,21 +192,19 @@ export declare function knownPrice(model: string | undefined): {
|
|
|
190
192
|
/**
|
|
191
193
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
192
194
|
*
|
|
193
|
-
* Cache reads and writes are priced at
|
|
194
|
-
*
|
|
195
|
+
* Cache reads and writes are priced at the model's own cache prices when
|
|
196
|
+
* the catalogue quotes them, else at Anthropic's multipliers (costOf in
|
|
197
|
+
* catalog.ts, the same arithmetic the record is repriced with). Counting
|
|
198
|
+
* both at the full input rate — as this did —
|
|
195
199
|
* makes a long cached prompt read about ten times its price, which on a
|
|
196
200
|
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
197
201
|
* killed for spending money it never spent.
|
|
198
202
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* number exists to stop a runaway step mid-turn, and wants to be close.
|
|
203
|
+
* The run record's own cost is repriced from the catalogue afterwards and
|
|
204
|
+
* stays the authority on the bill. This number exists to stop a runaway
|
|
205
|
+
* step mid-turn, and wants to be close.
|
|
203
206
|
*/
|
|
204
|
-
export declare function priceTurn(u: Record<string, number | undefined>, price:
|
|
205
|
-
input: number;
|
|
206
|
-
output: number;
|
|
207
|
-
} | null): number;
|
|
207
|
+
export declare function priceTurn(u: Record<string, number | undefined>, price: TokenPrice | null): number;
|
|
208
208
|
/**
|
|
209
209
|
* Each step launched together gets an equal share of what is left of the
|
|
210
210
|
* run's budget: the shares sum to the remainder, so a group cannot end
|
package/dist/src/step-exec.js
CHANGED
|
@@ -12,7 +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 {
|
|
15
|
+
import { costOf } from "./catalog.js";
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
17
|
import { checkPaths, checkBash, isFilesystemTool, linkWorkspace, workspaceLinkEnv, resolveAgentPath, isWithin } from "./confine.js";
|
|
18
18
|
import { DELEGATE_TOOLS, subagentGuard, toAgentDefinitions } from "./subagents.js";
|
|
@@ -81,27 +81,20 @@ export function knownPrice(model) {
|
|
|
81
81
|
/**
|
|
82
82
|
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
83
83
|
*
|
|
84
|
-
* Cache reads and writes are priced at
|
|
85
|
-
*
|
|
84
|
+
* Cache reads and writes are priced at the model's own cache prices when
|
|
85
|
+
* the catalogue quotes them, else at Anthropic's multipliers (costOf in
|
|
86
|
+
* catalog.ts, the same arithmetic the record is repriced with). Counting
|
|
87
|
+
* both at the full input rate — as this did —
|
|
86
88
|
* makes a long cached prompt read about ten times its price, which on a
|
|
87
89
|
* ceiling is not "the honest direction to be wrong in", it is a step
|
|
88
90
|
* killed for spending money it never spent.
|
|
89
91
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* number exists to stop a runaway step mid-turn, and wants to be close.
|
|
92
|
+
* The run record's own cost is repriced from the catalogue afterwards and
|
|
93
|
+
* stays the authority on the bill. This number exists to stop a runaway
|
|
94
|
+
* step mid-turn, and wants to be close.
|
|
94
95
|
*/
|
|
95
96
|
export function priceTurn(u, price) {
|
|
96
|
-
|
|
97
|
-
const fresh = u.input_tokens ?? 0;
|
|
98
|
-
const written = u.cache_creation_input_tokens ?? 0;
|
|
99
|
-
const read = u.cache_read_input_tokens ?? 0;
|
|
100
|
-
const outTok = u.output_tokens ?? 0;
|
|
101
|
-
return (fresh * p.input +
|
|
102
|
-
written * p.input * CACHE_WRITE_MULTIPLIER +
|
|
103
|
-
read * p.input * CACHE_READ_MULTIPLIER +
|
|
104
|
-
outTok * p.output);
|
|
97
|
+
return costOf(usageFromSdk(u), price ?? FALLBACK_PRICE);
|
|
105
98
|
}
|
|
106
99
|
/**
|
|
107
100
|
* Each step launched together gets an equal share of what is left of the
|
|
@@ -164,6 +157,7 @@ async function executeStepInner(opts, runQuery) {
|
|
|
164
157
|
let costUsd = null;
|
|
165
158
|
let usage = null;
|
|
166
159
|
let stopReason = null;
|
|
160
|
+
let numTurns = null;
|
|
167
161
|
const texts = [];
|
|
168
162
|
fs.mkdirSync(path.join(agentDir, "outputs"), { recursive: true });
|
|
169
163
|
fs.mkdirSync(path.join(agentDir, "memory"), { recursive: true });
|
|
@@ -622,6 +616,9 @@ async function executeStepInner(opts, runQuery) {
|
|
|
622
616
|
emit("tool", open.name, {
|
|
623
617
|
call: block.tool_use_id,
|
|
624
618
|
ms: Date.now() - open.at,
|
|
619
|
+
// How much the result put into the conversation — sent again on
|
|
620
|
+
// every later turn, so the size is what a token report needs.
|
|
621
|
+
bytes: toolResultText(block.content).length,
|
|
625
622
|
...(block.is_error ? { err: true } : {}),
|
|
626
623
|
...(open.subagent ? { subagent: open.subagent } : {}),
|
|
627
624
|
});
|
|
@@ -649,6 +646,8 @@ async function executeStepInner(opts, runQuery) {
|
|
|
649
646
|
}
|
|
650
647
|
costUsd = "total_cost_usd" in message ? (message.total_cost_usd ?? null) : null;
|
|
651
648
|
stopReason = "stop_reason" in message && typeof message.stop_reason === "string" ? message.stop_reason : null;
|
|
649
|
+
if ("num_turns" in message && typeof message.num_turns === "number")
|
|
650
|
+
numTurns = message.num_turns;
|
|
652
651
|
if ("usage" in message && message.usage) {
|
|
653
652
|
// Cache reads and writes counted beside the input total: with only
|
|
654
653
|
// the total (until 18 Oct 2026) the hit rate could not be read and
|
|
@@ -828,6 +827,7 @@ async function executeStepInner(opts, runQuery) {
|
|
|
828
827
|
status, result, conclusion, ...(opts.output ? { data } : {}), costUsd, usage,
|
|
829
828
|
...(mixedModels() ? { turnsCostUsd: turnsTotal() } : {}),
|
|
830
829
|
stopReason,
|
|
830
|
+
turns: numTurns ?? turns.size,
|
|
831
831
|
...(pod ? { browserPod: podState } : {}),
|
|
832
832
|
};
|
|
833
833
|
}
|