foldrun 2.1.0 → 2.2.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/docs/api.md CHANGED
@@ -206,7 +206,7 @@ paying for the steps around it.
206
206
  | `/api/workspaces/<ws>/notify/deliveries` | GET | the outbound webhook log: every `notify:` url delivery from this workspace, newest first — `{ deliveries, total, limit, offset, endpoint }`. A delivery is `{ id, event, status, destination, attempts, attemptLog, lastStatusCode, lastError, nextAttemptAt, createdAt, updatedAt, body }`: `id` is the `X-Foldrun-Delivery` header; `status` is `pending` (a retry is due at `nextAttemptAt`), `delivered`, `failed` (gave up after six attempts) or `skipped` (the endpoint was switched off); each attempt is `{ n, at, statusCode, durationMs, response (first 500 characters), error, manual? }`. `body` is what was sent, except that a gate's `approveUrl`/`rejectUrl` (top level and in `links`) read `[redacted]` — they decide without a login, and this log is readable by every role; the receiver still gets them, on a redelivery too. `?status=`, `?event=`, `?limit=` (50, at most 200), `?offset=`. `endpoint` is `{ failingSince, lastSuccessAt, lastFailureAt, disabledAt, disabledReason }` or null. Kept 30 days |
207
207
  | `/api/workspaces/<ws>/notify/deliveries/<id>/redeliver` | POST | send one delivery again now — the same id and body, signed afresh, to the URL `notify:` names today. `{ ok, attempt, delivery }`, always `200` when the delivery exists (whether the receiver took it is the answer); `404` when it does not. An accepted redelivery switches a disabled endpoint back on. Needs the permission that runs |
208
208
  | `/api/workspaces/<ws>/notify/test` | POST | sends one real notification to whatever this workspace's `notify:` resolves to, and answers `{ ok, destination, detail }` — including the provider's own words when it failed, which are what name the problem. Always `200`: the request was handled, and the result is the answer. Needs the permission that runs, because it sends real mail |
209
- | `/api/workspaces/<ws>/observe?days=` | GET | the observability report the Observe page renders — per-agent failures, retries, tokens and `cacheHitRate`, per-tool calls/errors/latency, per-flow duration, day series, recent failures, and `startup` — one row per start-up phase in order, `{ phase, took, at }`, each `{ n, p50, p95, max }` in seconds (`took` is the phase's own share, `at` when it ended counted from the step's start; percentiles need five samples). `days` 1–365, default 30 (`since=`, its older name, still works — deprecated) |
209
+ | `/api/workspaces/<ws>/observe?days=` | GET | the observability report the Observe page renders — per-agent failures, retries, tokens, `cacheHitRate`, `turns` and `systemPromptChars` (each `{ n, p50, p95, max }`), per-tool calls/errors/latency and `resultSize` (characters per result, same shape), `resultChars` (their sum) and `heavyCalls` (results over 10,000 characters), per-flow duration, day series, recent failures, and `startup` — one row per start-up phase in order, `{ phase, took, at }`, each `{ n, p50, p95, max }` in seconds (`took` is the phase's own share, `at` when it ended counted from the step's start; percentiles need five samples). `days` 1–365, default 30 (`since=`, its older name, still works — deprecated) |
210
210
  | `/api/workspaces/<ws>/history?path=&id=&limit=` | GET | every change to the workspace, newest first — `path` narrows to one file, `id` returns one revision with its before/after and a line diff, `limit` 1–500, default 100. A deploy's revision id is its commit |
211
211
  | `/api/workspaces/<ws>/restore` | POST | put the workspace's source back as it was at a point in its history. `to`: a revision id (`r-…`), the commit a deploy named (or its prefix), an ISO date/time (the last revision at or before it) or an age (`90m`, `12h`, `3d`, `2w`) — every revision since 17 Oct 2026 holds a snapshot of the whole source. `{ to, dryRun: true }` answers `{ to: { id, sha, at, by, message }, from, added, updated, removed, files: [{ path, change, diff }], blockedBy, noop, issues, warnings }` and writes nothing; `issues` are the deploy checks that would refuse that tree, `warnings` the ones it would deploy with. The restore needs `confirm` set to the workspace's name (400 without) and takes the dry run's `expectRemoved`; it deploys that revision's snapshot as a new revision (undo: restore to `from`; `to.sha` is the same id as `to.id`, kept from when revisions were git commits), keeps runs, `state/`, storage, secrets and agent-written memory, and is refused 409 while runs are in flight or when it would remove something not in `expectRemoved`. `workspace:write`; audited as `workspace.restored` |
212
212
 
@@ -443,7 +443,7 @@ signed-in person, never by a key — mints an ordinary API key at their role.
443
443
  | `/api/cli/login/<id>` | GET | `{ status }` — `pending`, `denied`, `expired`, or `approved` with `token`, `account`, `email`, `role`. The key is returned once; the request is gone after |
444
444
  | `/api/cli/authorize` | GET, POST | `?code=` describes the request (machine, expiry). POST `{ code, decision }` from a session approves (mints the key, labelled `cli · <hostname>`) or denies — `decision` is `approve` or `deny`, and absent approves; any other word is a `400`. Every role may sign its own CLI in; an owner's key is an admin's |
445
445
  | `/api/me` | GET | who is calling: `account`, `owner`, `role`, `workspaces` (null: all), and the `actor` — a person's email, or a key's id, label, prefix and who minted it |
446
- | `/api/me/onboarding` | GET, PATCH | the account's getting-started steps, each derived from what is really there — `{ steps: [{ id, title, why, href, cli?, done }], done, total, complete, next, dismissed, dismissedAt }`. Steps: `workspace`, `agent` (an agent written or deployed after the workspace was made), `run`, `success` (a completed run), `secret`, `trigger` (a schedule or webhook), `teammate` (a second member), `billing` (hosted, not fully waived: a saved card or a plan), `cli` (any API key or CLI login). PATCH `{ dismissed: true or false }` hides or brings back the dashboard card for the signed-in person (kept in their prefs); a key reads `dismissed: false` and cannot PATCH |
446
+ | `/api/me/onboarding` | GET, PATCH | the account's getting-started steps, each derived from what is really there — `{ steps: [{ id, title, why, href, cli?, done }], done, total, complete, next, dismissed, dismissedAt }`. Steps: `model` (first; only for an account that must bring its own key — done once it has one, or a `provider:` block in the account's AGENTS.md), `workspace`, `agent` (an agent written or deployed after the workspace was made), `run`, `success` (a completed run), `secret`, `trigger` (a schedule or webhook), `teammate` (a second member), `billing` (hosted, not fully waived: a saved card or a plan), `cli` (any API key or CLI login). PATCH `{ dismissed: true or false }` hides or brings back the dashboard card for the signed-in person (kept in their prefs); a key reads `dismissed: false` and cannot PATCH |
447
447
  | `/api/me/notifications` | GET, PATCH | what mail you get. GET `{ email, categories: [{ category, label, description, required, why?, perWorkspace, enabled, workspaces: { <ws>: bool } }] }` for `security`, `invites`, `billing`, `account` (required — always sent) and `run-alerts`, `approvals` (per workspace), `low-balance`, `product-updates`. PATCH `{ category, enabled, workspace? }` or `{ preferences: [ … ] }`; answers the same as GET. Turning a required category off is a `409` with the reason; `workspace` on an account-wide category is a `400`; `enabled: true` account-wide clears that category's per-workspace offs. A key acts for the person who minted it |
448
448
  | `/api/me/preferences` | GET, PATCH | your own display preferences: `{ theme }` — `system` (the default: follow the computer's light or dark setting), `light` or `dark`. PATCH `{ theme }` answers the same; anything else is a `400`. Kept in your prefs, so it follows you to every browser; the dashboard applies it on the server, before the first paint. A key acts for the person who minted it; a key minted by nobody is a `400` |
449
449
  | `/api/unsubscribe?token=` | POST | one-click unsubscribe (RFC 8058) — no login, the signed token is the credential. It is the `List-Unsubscribe` URL on every optional mail, beside `List-Unsubscribe-Post: List-Unsubscribe=One-Click`; a mail client POSTs `List-Unsubscribe=One-Click` to it. Turns off the one category (for the one workspace) the token was minted for, for its address. The token may also come as a form field (`token`, and `from=page` to be sent back 303 to the `/unsubscribe` confirmation page) or JSON. `200 { ok, category, workspace }`; `400` no token, `401` forged, `410` expired (180 days; `FOLDRUN_UNSUBSCRIBE_TTL_DAYS`). Never on a GET — `/unsubscribe?token=` is the page that asks first |
package/docs/cli.md CHANGED
@@ -77,7 +77,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
77
77
  | `foldrun logs [run-id]` | recent runs in one workspace, or one run's whole event trail. `--local` reads this machine's store even when signed in; `foldrun runs --local` is the same command and prints the same thing |
78
78
  | `foldrun runs` | what has run lately across the whole account: when, workspace, flow, status, steps done, how long, what it cost and the run's own one-line summary. A completed run shows its verdict beside the status — `GOOD`, `BAD`, `QUIET`, or `BLOCKED` in red with a ⛔ mark. `--status failed`, `--verdict blocked`, `--since 24h`, `--to <workspace>`, `--limit <n>`. `--local` prints exactly what `logs --local` prints — this machine's runs, one workspace |
79
79
  | `foldrun runs rm <run-id>` | erase a run from history: its record and its archived outputs. A run still in flight is stopped first. The ledger line stays — money is not deleted because a trace was. Asks first; `--yes` with no terminal |
80
- | `foldrun report <run-id>` | one run, whole, without opening the dashboard: the header, then every step with its group, agent, attempts, the runner image it ran on (`slim · browser pod` when it browsed through the account's pod from slim), cost, how much of its input the prompt cache served (`90% cached`; nothing for a step recorded before 18 Oct 2026), duration, what happened to the browser pod under it (reconnects, `browser pod lost; re-ran on full` with its tries — `slim · lost` then `full` — or why it was not re-run), whether its `verify:` held and the first line of what it said — and for a failed step, the error and the last events before it. Then what it wrote, what it published and what is still waiting on a person. `--json` prints the run record instead |
80
+ | `foldrun report <run-id>` | one run, whole, without opening the dashboard: the header, then every step with its group, agent, attempts, the runner image it ran on (`slim · browser pod` when it browsed through the account's pod from slim), cost, how much of its input the prompt cache served (`90% cached`; nothing for a step recorded before 18 Oct 2026), how many model turns it took, duration, what happened to the browser pod under it (reconnects, `browser pod lost; re-ran on full` with its tries — `slim · lost` then `full` — or why it was not re-run), whether its `verify:` held and the first line of what it said — and for a failed step, the error and the last events before it. Then what it wrote, what it published and what is still waiting on a person. `--json` prints the run record instead |
81
81
  | `foldrun report <run-id> get <agent>/<path>` | download one file the run archived — whatever that agent left in `outputs/`, copied under the run when it ended. Writes it beside you under its own name; `--file <path>` names somewhere else (`-` for stdout), `--force` overwrites |
82
82
  | `foldrun report <run-id> live` | the run page's live browser view: which agents' browsers have posted a frame, and when; `--agent <name>` saves the newest frame as a JPEG (`--file <path>`, `-` for stdout) and prints the page's address, the engine and whether the browser has closed (`GET …/runs/<id>/live`) |
83
83
  | `foldrun approvals` | every gate waiting on a person, anywhere in the account: which workspace, flow and run, which step and agent, the question, how long it has been waiting and which files under `storage/` the gate previews — and, first, every question an agent is asking mid-step, with its choices. `--to <workspace>` narrows it |
@@ -114,7 +114,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
114
114
  | `foldrun logout` | forget this machine's key, and revoke it if `login` made it |
115
115
  | `foldrun whoami` | who you are on the platform: account, role, which workspaces |
116
116
  | `foldrun team` | who is in the account (`GET /api/team`): each member's email, role (the owner marked), the workspaces they may open — a scoped key sees only the ones it shares — and when they joined. `--json`. Read-only on purpose: inviting, changing a role, removing someone and transferring the account are a signed-in person's, in Settings → Team, and the platform refuses them from a key |
117
- | `foldrun onboarding` | the account's getting-started steps — workspace, agent, a run, a run that completes, a secret, a schedule or webhook, a teammate, billing where the plan asks for it, the CLI — each worked out from what is really there, with the next one first and where to do it (`--json`). The dashboard card is hidden per person from the dashboard |
117
+ | `foldrun onboarding` | the account's getting-started steps — the model key (where the account brings its own), workspace, agent, a run, a run that completes, a secret, a schedule or webhook, a teammate, billing where the plan asks for it, the CLI — each worked out from what is really there, with the next one first and where to do it (`--json`). The dashboard card is hidden per person from the dashboard |
118
118
  | `foldrun preferences` | your own settings on the platform — today the theme (`system`, `light`, `dark`), kept with you on every browser; `preferences set theme dark` changes it (`/api/me/preferences`; a key acts for whoever minted it) |
119
119
  | `foldrun api spec` | the platform's OpenAPI 3.1 document, from `GET /api/openapi.json` (no key needed) — to stdout, or `--out <file>`. Every other command sends `Foldrun-Version` (the API version this CLI was built for), an `Idempotency-Key` on each write (reused on its own retry), and waits out one `429` for `Retry-After` before saying so. See [Using the API](api-usage) |
120
120
  | `foldrun api version` | Settings → API: the API version this account's requests get when they send no `Foldrun-Version`, whether it is pinned, every version still served, the rate limits, and the last 30 days: requests by version, and any deprecated route the account still calls — by which key or person, and when it stops answering. `api version pin <YYYY-MM-DD>` pins it, `unpin` goes back to current (`PATCH /api/account/api`; an admin's) |
@@ -127,7 +127,7 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
127
127
  | `foldrun schedule tick` | Settings' **Run scheduler now**: fire whatever is due this minute and print which flows went (`POST /api/schedule`). They were due anyway; this only makes them go now |
128
128
  | `foldrun rerun <run-id>` | the same flow again, from a step: `--from <n>` as the flow file numbers them, or `--agent <name>` for the first step that agent runs. Earlier steps are recorded as skipped. This is the debug loop from the terminal: fix the file, `deploy`, `rerun --from` the step that failed. `--wait` follows the new run like `invoke --wait`; `--to <workspace>` from anywhere. With no run id and a filter (`--status failed`, `--since 24h`, `--until 2h`, `--flow <name>`) it reruns every match from `--from <n>` (default 1), listing and asking first as a bulk `stop` does; `--dry-run` lists only |
129
129
  | `foldrun triggers` | why nothing ran. One row per flow that fired in the window: its trigger, how many fires became runs, when it last ran, and every reason for the difference with a count — a duplicate delivery, a throttled or debounced burst, a fire the platform slept through, an `overlap: skip`, a flow switched off by `disable_after:`. A switched-off flow is a non-zero exit; one successful run clears it. `--since <days>` (default 7), `--to <workspace>` |
130
- | `foldrun observe` | where the account fails, retries and spends: one row per workspace — runs, failed and what share, steps, cost — then the recent failures in their own words. `--to <workspace>` prints that workspace in full, as the Observe page does: per flow (runs, failed, p50/p95 duration, cost), per agent (failed, retried, skipped, p95, cache hit rate), per tool (calls, errors, p95 latency, which agents use it), and start-up — each phase of a step's first seconds, its own share and when it was done, p50/p95. `--since <days>` (default 30), `--json` for the document |
130
+ | `foldrun observe` | where the account fails, retries and spends: one row per workspace — runs, failed and what share, steps, cost — then the recent failures in their own words. `--to <workspace>` prints that workspace in full, as the Observe page does: per flow (runs, failed, p50/p95 duration, cost), per agent (failed, retried, skipped, p95, turns, cache hit rate), per tool (calls, errors, p95 latency, result size p50/p95 and how many results were over 10k characters, which agents use it), and start-up — each phase of a step's first seconds, its own share and when it was done, p50/p95. `--since <days>` (default 30), `--json` for the document |
131
131
  | `foldrun usage` | what the account consumed — runs, steps, tokens (with the share the prompt cache served), compute, storage, per workspace — and what it was charged over `--days <n>` (default 56) from the ledger, per workspace with the change against the period before. `--json` for both documents |
132
132
  | `foldrun billing` | the account's balance, whether billing is on at all, what the platform is currently waiving, and the recent ledger entries with what each was for — a run names its workspace, flow and run id; an adjustment carries its own note. `--limit <n>` shows more. `statement [YYYY-MM]` is a month of the ledger (this month by default) — `--csv` for the file an accountant wants, to stdout or `--file <path>`. `wallet` is the burn, the runway and auto top-up; `wallet set auto-top-up --threshold <usd> --amount <usd>` turns it on (a refill of 5–500), `set auto-top-up off` turns it off, `set email <address>` is where receipts go. `details` is who invoices are made out to; `details set --name "…" --abn … --address "line1, city, state, postcode, AU" --email …` changes it, merged with what is there. `portal` prints a link to Stripe's billing portal. Changing the wallet, the details, and the portal are the account owner's; anyone else is told so |
133
133
  | `foldrun billing plans` | the Plans page: every plan with its monthly fee, credits, runs at once and workspaces, which one the account is on and this cycle's credits left (`GET /api/billing/plans`). `billing plan <id>` chooses one — a first plan answers with Stripe's checkout page, a change on a live plan is prorated at Stripe at once (asks first; `--yes`); `plan cancel` stops it at the end of the cycle, `plan resume` undoes that until then (`POST`/`DELETE /api/billing/subscribe`) |
@@ -118,14 +118,17 @@ written into the workspace, so a workspace is always safe to commit.
118
118
 
119
119
  ## The checklist on the dashboard
120
120
 
121
- A new account's home page shows a **Getting started** card: create a
121
+ A new account's home page shows a **Getting started** card: add your model
122
+ key (on a hosted platform, where the account brings its own), create a
122
123
  workspace, write or deploy an agent, run a flow, get a run to finish, add a
123
124
  secret, schedule a flow or give it a webhook, invite a teammate, set up
124
125
  billing (where the plan asks for it) and install the CLI — "4 of 8", with the
125
126
  next step first and a link to where to do it. Each step is worked out from
126
127
  the account as it is (a run on record, a secret in the vault, a key minted
127
128
  for the CLI), so it ticks itself whichever way you did the thing; there is
128
- nothing to mark. **Dismiss** hides it for you alone; the line at the foot of
129
+ nothing to mark. While an account has no model key every run is refused, so
130
+ every dashboard page also carries a banner saying so, with a link to
131
+ Settings → Model provider (`foldrun account model set anthropic --key …`). **Dismiss** hides it for you alone; the line at the foot of
129
132
  the page brings it back, and it goes away by itself once every step is done.
130
133
 
131
134
  ```sh
package/docs/providers.md CHANGED
@@ -18,7 +18,10 @@ and the API show only its last four characters.
18
18
  own model credential is for the operator's accounts
19
19
  (`FOLDRUN_PLATFORM_KEY_ACCOUNTS`); a step in any other account with no key
20
20
  of its own is refused before it starts — "no model key: …" on the run — and
21
- an eval's judge or "draft with AI" says the same. A **Claude login token**
21
+ an eval's judge or "draft with AI" says the same. Until it has one, every
22
+ dashboard page says so in a banner, and **Add your model key** is the first
23
+ step of [getting started](getting-started.md#the-checklist-on-the-dashboard).
24
+ A `provider:` block in the account's AGENTS.md counts as its key. A **Claude login token**
22
25
  (`sk-ant-oat…`, from `claude setup-token`) is refused as a customer's key:
23
26
  Anthropic's terms do not let a third-party product run its customers on
24
27
  claude.ai logins. Use an API key from the provider.
@@ -35,7 +38,10 @@ checked against each provider's own documentation on 2026-09-02 and again on
35
38
  endpoints (Chat Completions) and `responses` endpoints (OpenAI's newer
36
39
  Responses API) are reached through the runtime's own translator, a loopback
37
40
  server inside the run sandbox that lives for one step — the same server,
38
- a second pair of mappings. No preset defaults to `responses`; a block asks
41
+ a second pair of mappings. An eval's `judge:` and "draft with AI" go through
42
+ the same translator, started for that one call, so they work on any key a
43
+ run works on. The dashboard's assistant does not: it needs an
44
+ `anthropic`-format provider or the platform's models. No preset defaults to `responses`; a block asks
39
45
  for it by name (`name: openai` + `format: responses`). **Verified**
40
46
  means a tool loop was actually driven through the endpoint from this
41
47
  runtime. A name without it is **documented, not proven**: the address, wire
package/docs/runs.md CHANGED
@@ -15,12 +15,13 @@ Each run holds its steps in group order. A step carries:
15
15
  | `result` | everything the agent wrote, every turn joined |
16
16
  | `conclusion` | the final turn alone — what it concluded |
17
17
  | `data` | the parsed value, for a step with `output: json` |
18
- | `events` | tool calls, script runs with their timing, verify verdicts, refusals |
18
+ | `events` | tool calls, script runs with their timing, verify verdicts, refusals. A tool call is a pair of events, the call and its completion; the completion carries `ms` and, from 18 Oct 2026, `bytes` — the result's length in characters, which every later turn sends again |
19
+ | `turns`, `systemPromptChars`, `stopReason` | model turns, summed across attempts; the system prompt's length in characters (it rides on every turn); why the last turn ended. With the events' `bytes`, these say where a step's input tokens went. From 18 Oct 2026 |
19
20
  | `costUsd`, `tokens`, `computeSecs`, `startupSecs` | what it cost and how long the sandbox lived — **summed across its attempts**. `tokens` is `{ input, output, cacheRead, cacheWrite }`: `input` is all input, and `cacheRead` and `cacheWrite` the part of it the [prompt cache](budgets-and-billing#prompt-caching) served and stored. Steps recorded before 18 Oct 2026 have only `input` and `output` |
20
21
  | `startup` | where the step's first seconds went: when each start-up phase ended, in seconds since the step started — `services` (the account's search/browser pods up), `room` (room on the cluster to start — see [Budgets and billing](budgets-and-billing)), `applied` (sandbox created), `ready`, `files` (workspace copied in), `go` (step released), `shim`, `system` (system packages restored), `driver`, `imports`, `tools` (runtime and tools built) and `model` (the model's first text or tool call). Only the phases the step went through; `null` for a step that ran in-process, absent before 17 Oct 2026. The last attempt's |
21
22
  | `image` | which runner image the step's sandbox ran (isolated runs on a cluster): `variant` `full` or `slim`, `why`, and `pod: true` when a browsing step ran slim and browsed through the account's browser pod ([slim steps and a lost pod](tools#slim-steps-and-a-lost-pod)) |
22
23
  | `browserPod` | on a step that browsed through the pod from slim: `reconnects` (tries), `reconnected` (how many got through: the call went on after it) and `closedAgain` (how many reached the pod only for the call to drop again at once, when any did); when the pod was lost, `lost` (`cause` `lost` or `needs-full`, and `detail`) and `writes` (what the step had written by then, worded); then `fallback: "browser pod lost; re-ran on full"` when it had only read and ran again on the full image, or `failure` — `browser pod died after the step had written <what>` — when it had written and was not re-run |
23
- | `tries` | one row per attempt, oldest first (and a `status: "lost"` row, `image: "slim"`, before the full re-run when the browser pod was lost under a slim step that had only read — see [tools](tools#slim-steps-and-a-lost-pod)): `n` (the try, numbered in the order they ran), `attempt` (the step's attempt it belongs to, only when that is not `n` — the full re-run after a lost slim try 1 is `n: 2, attempt: 1`), `status`, `costUsd`, `tokens`, `computeSecs`, `startedAt`, `finishedAt`, the attempt's `image` and its `browserPod` line when it had one, and the attempt's last `error` when it failed. A `retry: 2` step that failed twice and then passed has three rows, and its `costUsd` is all three — the run's spend, the flow's `budget:` and the bill read the step's figure, so the figure is the total. A retry is handed what is left of the budget after the failed attempt spent, and nothing left means no retry, said on the record |
24
+ | `tries` | one row per attempt, oldest first (and a `status: "lost"` row, `image: "slim"`, before the full re-run when the browser pod was lost under a slim step that had only read — see [tools](tools#slim-steps-and-a-lost-pod)): `n` (the try, numbered in the order they ran), `attempt` (the step's attempt it belongs to, only when that is not `n` — the full re-run after a lost slim try 1 is `n: 2, attempt: 1`), `status`, `costUsd`, `tokens`, `turns`, `computeSecs`, `startedAt`, `finishedAt`, the attempt's `image` and its `browserPod` line when it had one, and the attempt's last `error` when it failed. A `retry: 2` step that failed twice and then passed has three rows, and its `costUsd` is all three — the run's spend, the flow's `budget:` and the bill read the step's figure, so the figure is the total. A retry is handed what is left of the budget after the failed attempt spent, and nothing left means no retry, said on the record |
24
25
  | `item` | for a fan-out instance, which item this one handled |
25
26
 
26
27
  The run's `summary` is the **first meaningful line of the last step's
package/docs/tools.md CHANGED
@@ -1720,6 +1720,32 @@ real is in the sandbox for it. The gallery's `web action=search` does, because i
1720
1720
  needs no secret at all; the `web action=browse` does not, because a cookie has to be
1721
1721
  seeded into a real browser. See [Secrets](secrets#where-the-value-actually-is).
1722
1722
 
1723
+ ## Long results
1724
+
1725
+ A tool's result stays in the step's conversation and is sent to the model
1726
+ again on every later turn, so a long one is paid for many times over. A
1727
+ script or HTTP tool's result longer than 12,000 characters (about 3,000
1728
+ tokens) is therefore kept whole in a file under the agent's
1729
+ `.cache/results/`, and the model gets its first 12,000 characters, cut at a
1730
+ line break, and the file's path:
1731
+
1732
+ ```text
1733
+ …(41800 more characters not shown — the whole result, 53800 characters, is in `.cache/results/crm-get-mgx1k2-a1b2c3.txt`. Read it with offset/limit, or Grep it, if you need what is past this point.)
1734
+ ```
1735
+
1736
+ This applies to an agent that can read the file back — one granted Read or
1737
+ Grep (the `read` group, or `web`, which brings Read). An agent granted only
1738
+ `apis:` or `scripts:` has no tool to open it, so it gets the whole result up
1739
+ to 20,000 characters, as before.
1740
+
1741
+ Nothing is lost: until 18 Oct 2026 these results were cut at 20,000
1742
+ characters and the rest dropped. The file lives for the step — `.cache/` is
1743
+ never copied back from a run's sandbox and never deployed. The built-in
1744
+ tools (Read, Grep, Bash, WebFetch) keep the limits Claude Code gives them,
1745
+ and the web tool's own `max_output=` still decides how much of a page it
1746
+ returns. The Observe page's **Result size** column says which tools return
1747
+ the most, so a tool can be made to return less.
1748
+
1723
1749
  ## Limits — how many calls a step may make
1724
1750
 
1725
1751
  `limits:` on an agent caps how many times one step may call a tool. A call
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldrun",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Agents are just folders. Write them, check them, run them — on your machine.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  "node": ">=22"
12
12
  },
13
13
  "dependencies": {
14
- "@foldrun/core": "^2.2.0"
14
+ "@foldrun/core": "^2.3.0"
15
15
  },
16
16
  "files": [
17
17
  "bin",
package/src/commands.mjs CHANGED
@@ -6405,6 +6405,7 @@ async function reportCmd(runId, flags, layout) {
6405
6405
  startupLabel(step),
6406
6406
  step.costUsd ? `$${step.costUsd.toFixed(4)}` : null,
6407
6407
  cachedLabel(cachedShare(step.tokens)),
6408
+ typeof step.turns === "number" ? `${step.turns} turn${step.turns === 1 ? "" : "s"}` : null,
6408
6409
  step.startedAt && step.finishedAt
6409
6410
  ? humanDuration(Date.parse(step.finishedAt) - Date.parse(step.startedAt))
6410
6411
  : null,
@@ -8271,6 +8272,15 @@ const secs = (n) => (n == null ? "—" : humanDuration(n * 1000));
8271
8272
  const fine = (n) => (n == null ? "—" : n < 1 ? `${Math.round(n * 1000)}ms` : `${n.toFixed(2)}s`);
8272
8273
  const pct = (a, b) => (b ? `${Math.round((a / b) * 100)}%` : "—");
8273
8274
 
8275
+ /** Characters as a reader sizes them: 52k, 1.2M. */
8276
+ const kchars = (v) => (v >= 1_000_000 ? `${(v / 1_000_000).toFixed(1)}M` : v >= 1000 ? `${Math.round(v / 1000)}k` : String(v));
8277
+ /** p50/p95, or the worst case when there are too few samples, as the Observe page shows it. */
8278
+ const spreadOf = (s, unit) => {
8279
+ if (!s || !s.n) return "—";
8280
+ if (s.p50 == null) return `≤${unit(s.max)} (n=${s.n})`;
8281
+ return `${unit(s.p50)}/${unit(s.p95)}`;
8282
+ };
8283
+
8274
8284
  /** One workspace's observability report, as the Observe page reads it. */
8275
8285
  function printObservation(o) {
8276
8286
  console.log(`\n ${c.bold(o.workspace)} ${c.dim(`last ${o.sinceDays} days`)}`);
@@ -8283,8 +8293,8 @@ function printObservation(o) {
8283
8293
  for (const r of rows) console.log(` ${r.map((x, i) => (i === 0 ? c.bold(pad(x, w[i])) : pad(x, w[i]))).join(" ")}`);
8284
8294
  };
8285
8295
  table("flows", ["flow", "runs", "failed", "p50", "p95", "cost"], (o.flows ?? []).map((f) => [f.flow, f.runs, f.failed, secs(f.seconds?.p50), secs(f.seconds?.p95), `$${f.costUsd.toFixed(2)}`]));
8286
- table("agents", ["agent", "steps", "failed", "retried", "skipped", "p95", "cached", "cost"], (o.agents ?? []).map((a) => [a.agent, a.steps, a.failed, a.retried, a.skipped, secs(a.seconds?.p95), a.cacheHitRate == null ? "—" : `${Math.round(a.cacheHitRate * 100)}%`, `$${a.costUsd.toFixed(2)}`]));
8287
- table("tools", ["tool", "calls", "errors", "p95", "used by"], (o.tools ?? []).map((t) => [t.name, t.calls, t.errors, t.ms?.p95 == null ? "—" : `${Math.round(t.ms.p95)}ms`, (t.agents ?? []).join(", ")]));
8296
+ table("agents", ["agent", "steps", "failed", "retried", "skipped", "p95", "turns", "cached", "cost"], (o.agents ?? []).map((a) => [a.agent, a.steps, a.failed, a.retried, a.skipped, secs(a.seconds?.p95), spreadOf(a.turns, String), a.cacheHitRate == null ? "—" : `${Math.round(a.cacheHitRate * 100)}%`, `$${a.costUsd.toFixed(2)}`]));
8297
+ table("tools", ["tool", "calls", "errors", "p95", "result size", "heavy", "used by"], (o.tools ?? []).map((t) => [t.name, t.calls, t.errors, t.ms?.p95 == null ? "—" : `${Math.round(t.ms.p95)}ms`, spreadOf(t.resultSize, kchars), t.heavyCalls || "—", (t.agents ?? []).join(", ")]));
8288
8298
  // Where a step's first seconds go, phase by phase (FOL-29): each phase's
8289
8299
  // own share, and when it was done counted from the step's start.
8290
8300
  table("start-up", ["start-up", "n", "took p50", "p95", "done by p50", "p95"], (o.startup ?? []).map((p) => [p.phase, p.took?.n ?? 0, fine(p.took?.p50), fine(p.took?.p95), fine(p.at?.p50), fine(p.at?.p95)]));