Decisions
Ask a decision model for typed probabilities, choices and scores without running a full turn.
A decision is a small, fast request with a typed answer: the probability a condition is true, one value from a fixed set, or a position on a scale. Zidane calls the provider's Decisions endpoint and normalizes the answer, so hooks can make cheap routing and policy calls.
| Provider | Endpoint | Default model | Credentials |
|---|---|---|---|
openrouter() | POST /api/alpha/decisions | typesafe/jev-1.13 | OpenRouter API key |
openai() | POST /v1/decisions | gpt-6-luna | Platform API key (sk-…). The endpoint rejects Codex OAuth tokens. |
Ask from a hook#
Every hook context has ctx.decide, bound to the active run. Aborting the run cancels the decision, and decision:* hooks carry the run id. Outside hooks, call agent.decide(...).
const agent = createAgent({
provider: [anthropic(), openrouter()],
hooks: {
'tool:gate': async (ctx) => {
if (ctx.name !== 'shell')
return
const r = await ctx.decide({
input: String(ctx.input.command),
questions: {
destructive: { type: 'predicate', instructions: 'Does this command delete or overwrite data?' },
},
})
if (r.ok && r.answers.destructive.type === 'predicate' && r.answers.destructive.probability > 0.8) {
ctx.block = true
ctx.reason = 'Looks destructive'
}
},
},
})Questions and answers#
Each answer comes back under its question's key. Choice values are typed from the keys: answers.team.choice is 'payments' | 'frontend'.
const r = await agent.decide({
input: { ticket: 'Checkout shows a blank page after Pay' },
questions: {
isBug: { type: 'predicate', instructions: 'Is the customer reporting a defect?', criteria: { true: 'Broken behavior', false: 'A question or request' } },
team: { type: 'choice', instructions: 'Which team owns this?', choices: { payments: 'Checkout and billing', frontend: 'Rendering and layout' } },
urgency: { type: 'score', instructions: 'How urgent is it?', levels: ['Next release', 'This week', { label: 'Now', description: 'Blocking revenue' }] },
},
})| Type | Answer |
|---|---|
predicate | { type: 'predicate', probability }, between 0 and 1 |
choice | { type: 'choice', choice, probabilities?, confidence? } |
score | { type: 'score', score, probabilities?, confidence? }. score is the probability-weighted mean of level indices (from 0). A score question takes 2 to 10 levels (MAX_SCORE_LEVELS): more fails as invalid before any request. |
| any | { type: 'refusal', message? } when the model declines |
input is a string, a JSON object or prompt parts. OpenAI accepts inline base64 images. OpenRouter takes text and JSON only, and returns unsupported for images.
Failures never throw#
decide returns { ok: false, reason, message } on failure, so your hook picks fail-open or fail-closed.
reason | When |
|---|---|
unavailable | Decisions off, no capable provider, missing or rejected credentials, or unknown model. Known up front: nothing is sent. After a 404, zidane skips that model for the agent's lifetime. |
unsupported | The input cannot go to this provider |
timeout | Over timeoutMs (default 5000) |
aborted | The run or your signal was cancelled |
auth | HTTP 401 / 403 |
rate_limited / quota | HTTP 429 or 529 / HTTP 402 |
invalid | Bad questions, a timeoutMs of 0 or less or above 2147483647, or a malformed response |
error | Anything else |
| Call | Does |
|---|---|
ctx.decide.available, agent.decide.available | Returns synchronously whether there is a provider, credentials that allow decisions, and a model not known to be gone |
agent.decide(req, { throw: true }) | Throws a DecisionError instead: reason as above, outcome holds the failure |
Providers throw DecisionHttpError, DecisionUnavailableError, DecisionUnsupportedError, DecisionInvalidResponseError or DecisionTimeoutError. zidane maps them to these reasons.
Configuration#
By default the agent uses the first provider in provider that supports decisions and has credentials for them, with its default model. If none has credentials, it uses the first that supports decisions, which reports why it is unavailable. Override with decisions:
createAgent({
provider: anthropic(),
decisions: { provider: openrouter(), model: 'typesafe/jev-1.13', timeoutMs: 2000 },
})
createAgent({ provider: openrouter(), decisions: false }) // every decision returns `unavailable`In the apps#
In the terminal app, open Settings › Agent › Models. In the desktop app, open Settings › Agent › Helper models.
| Setting | Does |
|---|---|
| Decisions | Turns decisions on. Off by default, because the provider bills each call. |
| Decision model | Picks from the models your signed-in providers can use |
| Test | Sends one small decision with the credentials of the host that runs the sessions. Shows the latency or the failure reason (for example auth: invalid API key). |
Automatic session titles#
Auto-update session titles (autoTitle, next to the Decisions setting) needs decisions on. It titles a session after its first successful run. After every two new prompts, the decision model asks whether the title still fits, and rewrites it if not. The host runs this at run end, so it works the same in the TUI, the desktop app and server sessions.
| Rule | Detail |
|---|---|
| Length | 1 to 5 words while the setting is on, also for Generate title |
| Typed titles | Never replaced. Renames set metadata.titleSource: 'user'. Generated titles set 'auto'. |
| Re-enable | Generate the title once from session details to hand it back to auto-titling |
Titles without titleSource | Treated as yours. A title the desktop app stamped from the first prompt counts as untitled. |
| Main session | Keeps its name |
Auto-titling does not show errors in the chat. The Title line in session details shows the source and the last attempt:
[auto] [checked 5m ago, still fits]
[auto] [check failed 2m ago (unavailable: …)]Time estimates#
Estimate time remaining (runEstimates, under Auto-update session titles, desktop and web apps only) needs decisions on too. It asks how long each running session and subagent has left, and shows a countdown in the sidebar and on subagent headers. See time estimates.
Observability#
| Signal | Detail |
|---|---|
decision:start / decision:end | Fire on every call. decision:end carries ok, reason, durationMs, usage, answers. Usage is not added to run totals. |
| Tracing | One decide <model> span per decision |
| Metrics | gen_ai.decision.duration and gen_ai.decisions. The cost goes into gen_ai.cost_usd. |
| Logging | Warns on failures other than unavailable |
Durable runtime (restateRuntime) | During a run, each decision is a decision <run>:<turn>:decide:0 step, failures included, so replays take the same branch. agent.decide outside a run is not journaled. |
decision:* contexts have no decide, so a decision cannot trigger another.