Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3d8605f
Document model pricing and the agent's per-session spend caps
DavidCockerill Oct 6, 2026
7df48c5
Rename the session token cap to maxSessionTokens, and say which calls…
DavidCockerill Oct 6, 2026
d435808
Say how to leave a session whose cap cannot be measured, and that a r…
DavidCockerill Oct 6, 2026
30cb0c7
Date model pricing and the agent spend caps to 5.4.0, and add them to…
DavidCockerill Oct 6, 2026
22705b6
Say an unmeasured call keeps the session total unmeasured, not that i…
DavidCockerill Oct 6, 2026
093e0c9
Keep the http_fetch section badge at v5.3.2, and make the 5.4 notes e…
DavidCockerill Oct 6, 2026
5f602d6
Say that cancel_agent_run answers every pending approval
DavidCockerill Oct 6, 2026
0122412
Contrast the two cost caps, say clearing a cap by set_agent_config la…
DavidCockerill Oct 6, 2026
089f328
Say that an approved action already running when a cancel arrives fin…
DavidCockerill Oct 6, 2026
b1ff812
Merge remote-tracking branch 'origin/main' into david/3040-agent-usag…
DavidCockerill Oct 12, 2026
4a72a5e
Say maxTurns is validated, and that a cancelled session stays aborted
DavidCockerill Oct 12, 2026
ee7415c
Add agent cancellation and maxTurns validation to the 5.4 release notes
DavidCockerill Oct 12, 2026
e65e021
Say a cancelled run makes no further model call, and that maxTurns is…
DavidCockerill Oct 12, 2026
c00c685
Say agent_prompt answers 409 while a cancelled run finishes, and that…
DavidCockerill Oct 12, 2026
ca925cf
Say how long the 409 after a cancel can last
DavidCockerill Oct 12, 2026
5c38284
Say a prompt that races a cancel is recorded but not run
DavidCockerill Oct 12, 2026
632c145
Merge origin/main, and say a rejected reply counts toward the agent's…
DavidCockerill Oct 19, 2026
ffd6fe6
Reconcile the 5.4 notes on rejected replies, and tell clients not to …
DavidCockerill Oct 19, 2026
1d173b7
Say an approved action runs at most once, and how an interrupted one …
DavidCockerill Oct 19, 2026
526f5ba
Say what a cancel does to an approved action that had already started
DavidCockerill Oct 19, 2026
c61c758
Say that a tool call cut off by a stop is answered interrupted, not o…
DavidCockerill Oct 19, 2026
78648e6
Say a cut-off call is answered interrupted only once it started, and …
DavidCockerill Oct 19, 2026
0986d00
Document agent_prompt's 409 codes, runInFlight, the auto loop stoppin…
DavidCockerill Oct 19, 2026
3d7e8a6
Say runInFlight is per node, which agent_prompt 409s carry a code, an…
DavidCockerill Oct 19, 2026
569af46
Say an auto-loop cap of 0 makes no model call
DavidCockerill Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions reference/configuration/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,10 +426,11 @@ agent:
- `enabled` β€” Enable the agent component; _Default_: `false`
- `provider` β€” Recorded on the session but **not yet used to route the model call** β€” only `model` reaches the provider. Set the provider through the [`models`](../models/overview.md#configuration) configuration instead
- `model` β€” Model id override, passed through to the model call; _Default_: the [`models`](../models/overview.md#configuration) generative default
- `maxTurns` β€” Maximum tool-call iterations in a single run; _Default_: `50`
- `maxTurns` β€” Maximum tool-call iterations in a single run; a positive integer, validated from v5.4.0. A bare `maxTurns:` with no value fails the agent's start like any other invalid value; _Default_: `50`
- `maxTokens` <VersionBadge version="v5.4.0" /> β€” Output-token cap sent with every model request the agent makes. A reply cut off at this cap ends the run with status `error`, not `completed`, and `lastError` says it was cut off; raise the cap if replies keep hitting it. A cut-off reply can also mean the session's transcript filled the model's context window, which a higher cap does not fix; start a new session instead. Keep it within the model's own output limit: most providers reject a larger value on every request, and the run then ends `error` with the provider's message, so lower it for a model with a small output limit (some older models allow 4096 or 8192). Before v5.4.0 the agent sent no cap: Anthropic models, called directly or through Bedrock, then used 4096, and the other backends sent none; _Default_: `16384`
- `maxToolResultBytes` <VersionBadge version="v5.4.0" /> β€” Largest tool result, in bytes of JSON, the agent adds to its conversation. A larger result is cut to this size, ending with a note that gives the original size and tells the model how to ask for less. The file-reading tools return pages of at most half this size, measured as JSON, so a page arrives whole unless the file's path alone takes up most of the cap. Lower it for a model with a small context window. It must be an integer from `1024` to `1048576`; at startup an invalid value is logged and the default is used. See [When a conversation outgrows the model's context window](../operations-api/operations.md#when-a-conversation-outgrows-the-models-context-window); _Default_: `65536`
- `maxCostUsd` β€” Intended per-run cost ceiling. **Not enforced** β€” it is a stored setting only, and nothing checks spend against it; _Default_: `5.00`
- `maxSessionTokens` <VersionBadge version="v5.4.0" /> β€” Per-session cap on prompt plus completion tokens, summed over every model call in the session that returns, a reply the agent rejects as cut off or empty included; a positive integer. See [Spend caps](#agent-spend-caps); _Default_: none
- `maxCostUsd` <VersionBadge type="changed" version="v5.4.0" /> β€” Per-session cap in USD, priced from the [`pricing`](../models/overview.md#pricing) on the `models.generative` entry that served each call; a number of `0` or more, and `0` stops every session before its first call. See [Spend caps](#agent-spend-caps). Before v5.4.0 this setting was stored but never enforced, and defaulted to `5.00`; _Default_: none
- `autoApprove` β€” Run without per-action approval gates; _Default_: `false`
- `allowDestructive` β€” Include the tools marked destructive in the agent's toolset: `write_file`, the inspector's code-evaluation tools, and any operations tool carrying MCP's [`destructiveHint`](../mcp/tool-metadata.md) (`drop_table`, `delete`, `restart`, `set_configuration`, ...). When `false` they are removed entirely rather than gated. That hint comes from a curated set in core which does not cover every damaging operation, so this is not a complete safety boundary on its own β€” see [Agent operations](../operations-api/operations.md#agent); _Default_: `false`
- `user` β€” Harper user the agent's **operations** tools run as; the filesystem, HTTP, schedule, and inspector tools always run at process privilege regardless. If it cannot be resolved and it is not the default, the agent fails closed and runs with no operations tools; _Default_: `hdb_agent`, which falls back to a `super_user` bootstrap identity
Expand All @@ -438,7 +439,7 @@ agent:
- `httpFetch` <VersionBadge version="v5.4.0" /> β€” Whether the agent has its `http_fetch` tool, and which hosts it may reach: `true`, `false`, or `{ allow: [...] }`. Read at startup only. See [Restricting `http_fetch`](#restricting-http_fetch); _Default_: `true`
- `systemPromptAppend` β€” Operator text appended to the agent's system prompt

`enabled`, `provider`, `model`, `maxTurns`, `maxTokens`, `maxToolResultBytes`, `maxCostUsd`, `autoApprove`, `allowDestructive`, and `systemPromptAppend` can also be changed at runtime with [`set_agent_config`](../operations-api/operations.md#set_agent_config), which applies in memory only. `enabled` is the exception worth knowing: it cannot switch the agent on, because with the agent disabled at startup no agent operation is registered at all. `httpFetch`, `componentsScope`, and `configScope` are read at startup only, and `set_agent_config` rejects them.
`enabled`, `provider`, `model`, `maxTurns`, `maxTokens`, `maxToolResultBytes`, `maxSessionTokens`, `maxCostUsd`, `autoApprove`, `allowDestructive`, and `systemPromptAppend` can also be changed at runtime with [`set_agent_config`](../operations-api/operations.md#set_agent_config), which applies in memory only. `enabled` is the exception worth knowing: it cannot switch the agent on, because with the agent disabled at startup no agent operation is registered at all. `httpFetch`, `componentsScope`, and `configScope` are read at startup only, and `set_agent_config` rejects them.

### Filesystem scopes

Expand Down Expand Up @@ -472,6 +473,25 @@ A key with neither a key file name nor PEM armor, such as a `.p12` bundle or raw

The config file can still hold secrets that are not PEM keys, such as a model `apiKey` or storage credentials, and the agent reads it. Setting `configScope` to `rootPath` also restores read access to the raw database files and backups.

### Agent spend caps

<VersionBadge version="v5.4.0" />

`maxSessionTokens` and `maxCostUsd` bound a whole session, across every run in it. Approving a paused action or sending a follow-up prompt starts a new run, but does not reset a cap the way it resets `maxTurns`. Each assistant message records the usage of the model call that produced it, and [`get_agent_session`](../operations-api/operations.md#get_agent_session) reports the session's totals as `usage`.

A cap is checked when a run starts, before it executes an approved action, and after every model call:

- **At or over a cap**, the session makes no further model call and ends `error`, with `lastError` naming the cap, for example `agent.maxSessionTokens=200000 reached: this session used 201450 tokens`. Tool calls the model had just requested are answered with a `budget_exceeded` result rather than run, so the transcript stays valid if you continue the session. A final answer that crosses the cap is still delivered: the run ends `completed`, with `lastError` naming the cap.
- **A cap that cannot be measured stops the session too.** `maxCostUsd` needs every call priced, and `maxSessionTokens` needs every call's token counts. A model with no `pricing`, or a provider that reports no usage, stops the session after its first call rather than letting it run uncapped: it ends `error`, or `completed` if that call was the final answer, with `lastError` saying why either way. The unmeasured call stays on the session, so the session's total stays unmeasured and later runs stop before making any call, and raising the cap or adding `pricing` does not clear it: start a new session, or clear the cap. `set_agent_config` with `null` clears it until the next restart; to clear it for good, remove the key from the configuration file. A session created before v5.4.0, whose earlier calls recorded no usage, is in the same state.
- **Pending approvals are closed too.** A run stopped before it executes an approved action answers that action, and any approval still awaiting a decision, with `budget_exceeded`, so nothing is left pending. The action does not run later, even once the cap is raised; the model sees the refusal and can ask again.
- **A capped session stays capped.** Raise the cap with `set_agent_config` or start a new session.

A cap is a threshold checked after each call, not a guarantee: the call that reaches it can overshoot it. Every call that returns is counted, including a reply the agent rejects as cut off or empty: that reply stays out of the transcript, but its usage is kept on the session. A call that fails, for example on a timeout or a provider error and whether or not another model then serves it, is not counted, even if the provider billed it. The caps bound one session only: there is no cross-session or per-user budget, so a caller allowed to run `agent_prompt` can keep spending by starting new sessions.

A cap, or a `maxTurns`, that is not a usable number fails the agent's start: Harper logs the error and registers no agent operation. Through `set_agent_config` the same value is rejected with a 400. A bare `maxSessionTokens:` or `maxCostUsd:` with no value leaves that cap unset, as `null` does through `set_agent_config`; a bare `maxTurns:` fails, because `maxTurns` cannot be unset.

If you upgrade with `maxCostUsd` already set in your configuration, it starts being enforced, and needs `pricing` on the generative entry the agent uses.

### Restricting `http_fetch`

<VersionBadge version="v5.4.0" />
Expand Down
17 changes: 9 additions & 8 deletions reference/models/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,22 @@ Additional options apply only when `toolMode: 'auto'`; they are documented in [T

### GenerateResult

| Field | Type | Description |
| -------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `content` | `string` | The generated text |
| `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 |
| `trace` | `ToolTraceEntry[]` | Per-tool-invocation trace; only populated by the `toolMode: 'auto'` loop β€” see [Tool Calling](./tool-calling) |
| Field | Type | Description |
| -------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content` | `string` | The generated text |
| `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` |
Comment thread
DavidCockerill marked this conversation as resolved.
Resolved
| `trace` | `ToolTraceEntry[]` | Per-tool-invocation trace; only populated by the `toolMode: 'auto'` loop β€” see [Tool Calling](./tool-calling) |

## generateStream()

```typescript
models.generateStream(input: GenerateInput, options?: GenerateOpts): AsyncIterable<GenerateChunk>
```

Identical to `generate()` but yields the completion incrementally:
Identical to `generate()` but yields the completion incrementally. Streamed calls are not [priced](./overview#pricing), so no chunk carries `costUsd`:

```javascript
let text = '';
Expand Down
1 change: 1 addition & 0 deletions reference/models/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ All backends support these common fields:
| `model` | Provider-side model identifier (e.g. `gpt-4o`) used when a call does not pass its own `model` option |
| `requestTimeoutMs` | Per-request timeout in milliseconds; composed with any caller-provided `AbortSignal` |
| `revision` | From v5.3.1, a non-secret label for the deployment behind the entry, for [calibration](./calibration#when-the-deployment-changes-but-the-configuration-does-not); change it when the same settings reach a different model |
| `pricing` | From v5.4.0, `generative` entries only: `{ inputPerMtok, outputPerMtok }`, what you pay in USD per million prompt and completion tokens. See [Pricing](./overview#pricing) |

## Ollama

Expand Down
24 changes: 24 additions & 0 deletions reference/models/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,30 @@ The logical name `default` is used when application code does not pass an explic

See [Backends](./backends) for the full set of configuration fields supported by each backend.

### Pricing

<VersionBadge version="v5.4.0" />

A `generative` entry can declare what you pay for its model, so Harper can put a dollar figure on each call. Harper ships no prices of its own: the rates are yours to keep current.

```yaml
models:
generative:
default:
backend: openai
apiKey: ${OPENAI_API_KEY}
model: gpt-4o
pricing:
inputPerMtok: 2.5 # USD per million prompt tokens
outputPerMtok: 10 # USD per million completion tokens
```

Each `generate()` call is priced at the rates of the entry that served it, and the result carries `costUsd`. With `toolMode: 'auto'`, the result's `costUsd` covers the final round only, like its `usage`. A call that fell back to another entry in a [fallback group](./routing#fallback-groups) is priced at that entry's rates, not the requested one's. A call has no `costUsd` when the entry that served it declares no `pricing`, or when the provider did not report both a prompt and a completion token count.

The two rates are the only cost inputs Harper uses. Discounts for cached prompt tokens, long-context surcharges and provisioned capacity are not, so set rates that match what an average call costs you, or rates that overestimate it if the figure backs a cap. Streaming calls (`generateStream()`) are not priced.

The price is read by [`maxCostUsd`](./tool-calling#options) on `toolMode: 'auto'` calls and by the [built-in agent's `maxCostUsd`](../configuration/options#agent-spend-caps). `pricing` is accepted only on `generative` entries; on an `embedding` or `decision` entry it fails configuration validation. A price change applies on config reload, except on an entry whose `backend` is a module rather than a built-in, which takes it on restart like any other change to that entry. It does not affect [calibration](./calibration).

### Credentials

String values in model entries support environment-variable indirection with `${VAR_NAME}` syntax, resolved at startup. Use this for API keys rather than placing the literal key in the configuration file β€” Harper logs a warning at startup when a credential field contains a literal value. If the referenced environment variable is unset, the placeholder is left as-is; for credential fields the backend rejects the unresolved placeholder at startup, while other fields (such as `host` or `model`) carry the literal placeholder into requests β€” surfacing as per-request failures rather than a startup error. Indirection applies to string-typed fields only; numeric fields such as `requestTimeoutMs` must be literal values.
Expand Down
Loading
Loading