Skip to content

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.

ProviderEndpointDefault modelCredentials
openrouter()POST /api/alpha/decisionstypesafe/jev-1.13OpenRouter API key
openai()POST /v1/decisionsgpt-6-lunaPlatform 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(...).

TypeScript
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'.

TypeScript
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' }] },
  },
})
TypeAnswer
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.

reasonWhen
unavailableDecisions 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.
unsupportedThe input cannot go to this provider
timeoutOver timeoutMs (default 5000)
abortedThe run or your signal was cancelled
authHTTP 401 / 403
rate_limited / quotaHTTP 429 or 529 / HTTP 402
invalidBad questions, a timeoutMs of 0 or less or above 2147483647, or a malformed response
errorAnything else
CallDoes
ctx.decide.available, agent.decide.availableReturns 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:

TypeScript
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.

SettingDoes
DecisionsTurns decisions on. Off by default, because the provider bills each call.
Decision modelPicks from the models your signed-in providers can use
TestSends 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.

RuleDetail
Length1 to 5 words while the setting is on, also for Generate title
Typed titlesNever replaced. Renames set metadata.titleSource: 'user'. Generated titles set 'auto'.
Re-enableGenerate the title once from session details to hand it back to auto-titling
Titles without titleSourceTreated as yours. A title the desktop app stamped from the first prompt counts as untitled.
Main sessionKeeps its name

Auto-titling does not show errors in the chat. The Title line in session details shows the source and the last attempt:

Text
[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#

SignalDetail
decision:start / decision:endFire on every call. decision:end carries ok, reason, durationMs, usage, answers. Usage is not added to run totals.
TracingOne decide <model> span per decision
Metricsgen_ai.decision.duration and gen_ai.decisions. The cost goes into gen_ai.cost_usd.
LoggingWarns 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.

Reference#