Skip to content

Document model pricing, the agent's per-session spend caps, and final cancels - #727

Draft
DavidCockerill wants to merge 16 commits into
mainfrom
david/3040-agent-usage-budgets
Draft

DavidCockerill wants to merge 16 commits into
mainfrom
david/3040-agent-usage-budgets

Conversation

@DavidCockerill

@DavidCockerill DavidCockerill commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

agent: carry per-call token usage onto the session and enforce a budget (HarperFast/harper#3040) makes the agent's maxCostUsd real, adds agent.maxSessionTokens, records per-session usage, lets a models.generative entry declare its pricing, makes a cancel final, and validates maxTurns. The reference still calls maxCostUsd "not enforced" with a 5.00 default, and documents none of the rest.

Refs HarperFast/harper#3040. Companion to Cap the agent's spend per session by tokens or priced cost, record its model usage, and make a cancel final (HarperFast/harper#3177).

❓ Your call: requirement. These are user-facing config keys, an operations response field, status and 409 behavior that pollers key on, and a change to what cancel_agent_run does, so the docs are needed whenever the core PR merges. Dated v5.4.0 to match the core milestone.

💡 Solution

❓ Your call: naming. This documents agent.maxSessionTokens; Document agent.maxTokens, and that a cut-off or empty agent reply ends the run error (#712) documents agent.maxTokens as the per-reply output limit from End agent runs cut off by the output-token limit as errors, and add agent.maxTokens (HarperFast/harper#3064). Both keys can coexist; whichever PR lands second rebases the set_agent_config key list.

The api.md and tool-calling.md hunks also re-align their tables, which Prettier does when a wider row is added.

✅ Verification

Every statement was checked against the core branch by review rounds (Codex graded leg, Gemini, Harper domain adjudication); the last round left only nits. npm run format:check passes. The core behavior these pages describe is exercised by the core PR's integrationTests/server/agent-budget.test.ts and its agent unit suites.

🤖 Generated by Claude Code (Anthropic Claude Opus 5.5); posted via @DavidCockerill.

Related PRs: #568 independent, #671 independent, #676 independent, #683 independent, #691 independent, #709 independent, #712 overlaps (documents agent.maxTokens, the per-reply limit, beside agent.maxSessionTokens; the set_agent_config key list and the 5.4 agent notes conflict), #714 independent, #717 overlaps (agent.configScope in the same agent options and set_agent_config text, and the 5.4 agent notes), #720 overlaps (agent.maxToolResultBytes in the same agent options and set_agent_config text, and the 5.4 agent notes), #722 independent, #724 independent, #725 independent, #715 independent, #713 independent, #697 independent, #710 independent, #707 independent
Complexity: medium

Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; blocked=cursor-composer(not-installed); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=8; full=2 @ 5c38284

Review-Attention: skim ~2m (decisions: auto-loop-unpriced-fails-open, failed-calls-uncounted, legacy-sessions-unmeasured, maxcostusd-default-removed, auto-costusd-final-round, cancel-refuses-approved-actions, cancel-finishing-409, maxturns-null-split) @ 5c38284

DavidCockerill and others added 16 commits October 6, 2026 12:47
Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a cap counts

agent.maxTokens is the per-reply output limit (#712). Also: a failed call
is never counted, an unmeasurable cap on a final answer ends completed, invalid caps fail the
agent's start, and auto-mode costUsd covers the final round.

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efused approval does not run later

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the 5.4 release notes

HarperFast/harper#3040 is milestoned v5.4, which is main, so it ships in 5.4.0 rather than a 5.3
patch.

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t stays in the total

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xact about costUsd and capped final answers

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sts until restart, and note streaming is unpriced

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ishes

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… captured at run start

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a mid-turn cancel answers the rest of the turn

Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs HarperFast/harper#3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-727

This preview will update automatically when you push new commits.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the documentation for version 5.4.0, introducing model pricing, per-call cost tracking, and spend caps (maxSessionTokens and maxCostUsd) for the built-in agent. It also details updates to the operations API regarding session status, usage reporting, and cancellation behavior. The review feedback suggests using a VersionBadge to highlight the new costUsd field in the API documentation, and recommends splitting combined sentences in the tool-calling and operations guides to make fallback behaviors more distinct and readable.

Comment thread reference/models/api.md
| `finishReason` | `'stop'` \| `'length'` \| `'tool_calls'` \| `'content_filter'` | Why generation stopped, normalized across backends |
| `toolCalls` | `ToolCall[]` | Tool calls the model requested, when `finishReason` is `'tool_calls'` (each `{ id, name, arguments }`, with `arguments` parsed to an object) |
| `usage` | `TokenUsage` | Token usage reported by the backend (`promptTokens`, `completionTokens`, …), when available |
| `costUsd` | `number` | From v5.4.0, the call's cost from the serving entry's [pricing](./overview#pricing); absent when that entry is unpriced or usage is incomplete. With `toolMode: 'auto'`, the final round's only, like `usage` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When documenting behavior changes to an existing API surface (such as adding the new costUsd field to the GenerateResult response), we should use the <VersionBadge type="changed" version="v5.4.0" /> component to clearly highlight the change.

Suggested change
| `costUsd` | `number` | From v5.4.0, the call's cost from the serving entry's [pricing](./overview#pricing); absent when that entry is unpriced or usage is incomplete. With `toolMode: 'auto'`, the final round's only, like `usage` |
| `costUsd` | `number` | <VersionBadge type="changed" version="v5.4.0" /> The call's cost from the serving entry's [pricing](./overview#pricing); absent when that entry is unpriced or usage is incomplete. With `toolMode: 'auto'`, the final round's only, like `usage` |
References
  1. When documenting behavior changes to an existing API surface (e.g., adding new fields to a response), use .

| `toolHandlers` | `Record<string, ToolHandler>` | — | Dispatch table keyed by tool name. A tool declared in `tools` with no handler here is a configuration error (400) |
| `maxToolIterations` | `number` | `10` | Hard cap on model → tools → model rounds |
| `maxToolTokens` | `number` | — | Cumulative prompt+completion token cap across rounds. Best-effort: requires the backend to report usage; if it does not, Harper warns once and `maxToolIterations` remains the bound. Not supported on `generateStream()` (throws) |
| `maxCostUsd` | `number` | — | From v5.4.0, cumulative USD cap across rounds, from the [pricing](./overview#pricing) of the entry that served each round. If a round is unpriced, Harper warns once and `maxToolIterations` remains the bound; a cap of `0` still stops after the first round. This is the opposite of the built-in agent's `maxCostUsd`, which stops rather than run unpriced. Not supported on `generateStream()` (throws) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback behavior for unpriced rounds is combined with another concept and buried behind a semicolon. To ensure operators can easily find and understand this fallback behavior, please present it in separate, distinct sentences.

Suggested change
| `maxCostUsd` | `number` | — | From v5.4.0, cumulative USD cap across rounds, from the [pricing](./overview#pricing) of the entry that served each round. If a round is unpriced, Harper warns once and `maxToolIterations` remains the bound; a cap of `0` still stops after the first round. This is the opposite of the built-in agent's `maxCostUsd`, which stops rather than run unpriced. Not supported on `generateStream()` (throws) |
| `maxCostUsd` | `number` | — | From v5.4.0, cumulative USD cap across rounds, from the [pricing](./overview#pricing) of the entry that served each round. If a round is unpriced, Harper warns once and `maxToolIterations` remains the bound. A cap of `0` still stops after the first round. This is the opposite of the built-in agent's `maxCostUsd`, which stops rather than run unpriced. Not supported on `generateStream()` (throws) |
References
  1. Ensure critical security warnings, such as unauthorized access risks or fallback behaviors, are presented in separate, distinct sentences rather than being combined with other concepts or buried behind semicolons, so that readers scanning the documentation can easily find them.


Cancels a session's run, clears any followups it scheduled, and marks the session `aborted`. Works on a paused (`awaiting_approval`) or `idle` session as well as an actively running one.

From v5.4.0 the cancel also answers every pending approval, decided or not, with a `cancelled_by_operator` result, in the same write that marks the session `aborted`. An approved action that has not started by then never runs, neither in the cancelled run nor when you next prompt the session, and nothing is left awaiting a decision. One already running finishes, and its result is recorded. A cancel between a turn's tool calls answers the rest of that turn `cancelled_by_operator`. The session stays `aborted`: a run caught mid-step finishes that step without marking the session `completed`, `error` or `awaiting_approval`, a tool call it then asks to have approved is answered `cancelled_by_operator` instead, and it makes no further model call.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback behavior for a run caught mid-step during cancellation is combined with other concepts and buried behind a colon and semicolons. Please present this critical fallback behavior in separate, distinct sentences so that readers scanning the documentation can easily find it.

Suggested change
From v5.4.0 the cancel also answers every pending approval, decided or not, with a `cancelled_by_operator` result, in the same write that marks the session `aborted`. An approved action that has not started by then never runs, neither in the cancelled run nor when you next prompt the session, and nothing is left awaiting a decision. One already running finishes, and its result is recorded. A cancel between a turn's tool calls answers the rest of that turn `cancelled_by_operator`. The session stays `aborted`: a run caught mid-step finishes that step without marking the session `completed`, `error` or `awaiting_approval`, a tool call it then asks to have approved is answered `cancelled_by_operator` instead, and it makes no further model call.
From v5.4.0 the cancel also answers every pending approval, decided or not, with a `cancelled_by_operator` result, in the same write that marks the session `aborted`. An approved action that has not started by then never runs, neither in the cancelled run nor when you next prompt the session, and nothing is left awaiting a decision. One already running finishes, and its result is recorded. A cancel between a turn's tool calls answers the rest of that turn `cancelled_by_operator`. The session stays `aborted`. A run caught mid-step finishes that step without marking the session `completed`, `error`, or `awaiting_approval`. A tool call it then asks to have approved is answered `cancelled_by_operator` instead, and it makes no further model call.
References
  1. Ensure critical security warnings, such as unauthorized access risks or fallback behaviors, are presented in separate, distinct sentences rather than being combined with other concepts or buried behind semicolons, so that readers scanning the documentation can easily find them.

This branch was successfully deployed

1 active deployment
pr-727 — 5c382847 Deployed Oct 12, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant