You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
agents.resume without agentOptions yields agents whose turns fail with "prompt variable {{model}} has no value"
#10208
agents.resume without agentOptions yields agents whose turns all fail with prompt variable "{{model}}" has no value
Version: 0.2.1-rc.2 (desktop)
Repro
Persist any session (so it survives the process).
In a fresh process, resume it without options: agents.resume({ resumeSessionId }) — omitting agentOptions entirely (they are optional at the call site).
Send any message / run any turn on the resumed agent.
Observed: every turn fails immediately with prompt variable "{{model}}" has no value (turn/end reason kind: "error"). The session is effectively bricked until a model selection is made through the UI.
Root cause: the agent loop binds the prompt variables from the live agent's options — ctx.systemPrompt.variable("model", (context) => context.agent?.options.model) and the matching provider variable (dsh-agent-loop). A resume that omits agentOptions leaves agent.options.model unset, so every prompt assembly that references {{model}} throws.
Impact: any programmatic resume path (headless runners, startup-recovery plugins, automation) that omits agentOptions produces an unusable agent with an error that names a template variable, not the missing binding. We hit this live: a startup-recovery plugin resumed interrupted goal sessions and every turn errored until a model was picked in the GUI.
Suggested fixes (either suffices):
When agentOptions is omitted on resume, fall back to the session's last recorded model/selection event, or the configured default model — mirroring what the interactive resume path effectively does.
Or reject agents.resume up front with a precise error ("agentOptions/model required for resume") instead of failing every later turn at prompt assembly.
Context: this surfaced while verifying dsh-session-auto-resume (showcase post); the plugin now binds the default model explicitly and logs a loud resume-no-model line when it cannot.
Confirmed at source on 687f282e86 (the 0.2.1-rc.2 release commit), including the two lines you named. One correction matters more than the confirmation, though: the fallback you want already exists in the tree, in a form that suggests it should be reused rather than reimplemented — and filling in agentOptions.model alone would fix the error while making the prompt lie. Details below.
Confirmed
Claim
Where
the loop binds both variables from live options
packages/core/agent-loop/src/index.ts:389-390 — ctx.systemPrompt.variable('provider', c => c.agent?.options.provider) and the same for model
the error is the "registered but valueless" arm
packages/core/system-prompt/src/index.ts:371
agentOptions is genuinely optional
packages/core/agent/src/index.ts:131; AgentOptions.model?: string at runtime-types.ts:28
the loop passes an empty object when omitted
packages/core/agent-loop/src/index.ts:777 and :912 — options.agentOptions ?? {}
it fails at assembly, not at resume
interpolate() runs while rendering sections; nothing checks the pairing before then
There are two suppliers of {{model}}, and the one that always answers is installed by the caller
Beyond the global binding above, installModelSelection (packages/core/agent/src/model-selection.ts:81) registers a system-prompt/assemble waterfall listener that rewrites the variables afternext():
So your resumed agent fails because none of the three entry points that can install a selection ever ran: selection.current is undefined, the override declines, and the static options.model — also undefined — is what interpolation sees.
The interactive path works not because it sets agentOptions but because packages/api/session-controller/src/agent.ts:296-310 resolves a getter with three rungs, in this order:
projectionState.pending — a selection the user made but has not yet committed;
agent.session.requestHeader() — the last logged request header, which is your "session's last recorded model" and is more reliable than scanning for a model/selection event, because the projection's lastUsed is also fed from request/header (model-selection-projection.ts:44-53), so sessions that never appended an explicit selection still have a usable route;
defaultModel.currentSelection() — the deployment default you asked for.
Rung 2 carries a subtlety any second implementation would have to reproduce verbatim (:306-310): an effort the adapter defaulted is not a conversation choice, so it is dropped when loggedHeader.adapterDefaults?.reasoningEffort === true. That asymmetry is exactly the kind of rule that rots when copied.
Why filling agentOptions.model is the wrong shape of fix
installModelSelection owns two surfaces at once: the assembly override above, and an agent/request listener that overwrites provider/model on the outgoing call config (and deliberately drops an inherited effort so the selected model's own default applies). If a fix binds only the prompt variable, the prompt tells the model it is X while the request routes to whatever the adapter defaults to — a worse failure than the current one, because it is silent. Any fix here should install a selection, so both surfaces move together. That is also why packages/core/agent/src/model-selection.ts looks like the right home for rungs 1-3 as a shared resolver: it already exists, and session-controller, acp, and bundle/headless all import from it — bundle/headless/src/index.ts:334-342 is the template (resolve the default, then install the selection in setup).
Your suggestion 2 would break a legal configuration
Rejecting agents.resume without a model up front is not safe: ResumeAgentOptions.setup is documented as the pre-publication hook that receives agentCtx, and a caller that installs its own selection there — passing no agentOptions at all — is a correct, working configuration. It never touches the prompt variable's valueless arm. An up-front rejection would reject that caller. The same reasoning is why the obligation does not belong in dsh-agent at all: the registry cannot reach agentDefaultModel, so any fallback inside it would be a second owner of a decision three callers already make.
The failure is conditional, which is what a test should pin
{{model}} is fatal only when some assembled text references it. Every shipped bundle writes it into personaPrefix (bundle/{web-app,sdk-app,acp-app,headless}/cordis.patch.yml, plus the three web presets), so under any stock profile your omission is fatal on the first turn — but a deployment whose persona drops the variable runs fine with an unset model and routes through the adapter default. Worth a test each way, since the first looks like "resume is broken" and the second looks like "omission is supported".
If you want it as a plugin rather than a core patch
Then it is mountable, and cleanly: AgentRegistry.resume deliberately routes through Reflect.apply(target.resume, receiver, [ownerCtx, options]) (packages/core/agent/src/index.ts:412-417), which is a wrap seam, not an accidental indirection. Wrap it; when the caller omitted a model, resolve via the three rungs and compose the caller's options.setup with your own that calls installModelSelection(agentCtx, resolved) — the documented pre-publication hook, no core edit, correct on both surfaces. The one thing I would not do is install a guessed value: a selection that disagrees with the route the adapter will take makes the persona prefix assert a false identity, and that is harder to notice than the error you are fixing today.
Adjacent, and different from the sibling fault: an asset we ship for unknown prompt variable would not help here, precisely because your case takes the :371 arm where the name is registered and the provider answered undefined — the registry can name the owner and currently does not. Adding the owning scope to that one message (:368 already lists registered names; :371 lists nothing) would have said "registered globally by agent-loop, resolved undefined" instead of pointing at a section.
Two questions: does your recovery plugin need reasoningEffort restored as well — the request listener clears any inherited effort when the selection has none, so a model-only restore can change behaviour versus the pre-interruption turns; and do you see the same omission on the ACP path, where AcpModelControl is constructed with initial: ModelSelection | undefined (packages/acp/acp/src/model-control.ts:46) and can therefore start with current undefined too?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
agents.resumewithoutagentOptionsyields agents whose turns all fail withprompt variable "{{model}}" has no valueVersion: 0.2.1-rc.2 (desktop)
Repro
agents.resume({ resumeSessionId })— omittingagentOptionsentirely (they are optional at the call site).Observed: every turn fails immediately with
prompt variable "{{model}}" has no value(turn/end reasonkind: "error"). The session is effectively bricked until a model selection is made through the UI.Root cause: the agent loop binds the prompt variables from the live agent's options —
ctx.systemPrompt.variable("model", (context) => context.agent?.options.model)and the matchingprovidervariable (dsh-agent-loop). A resume that omitsagentOptionsleavesagent.options.modelunset, so every prompt assembly that references{{model}}throws.Impact: any programmatic resume path (headless runners, startup-recovery plugins, automation) that omits
agentOptionsproduces an unusable agent with an error that names a template variable, not the missing binding. We hit this live: a startup-recovery plugin resumed interrupted goal sessions and every turn errored until a model was picked in the GUI.Suggested fixes (either suffices):
agentOptionsis omitted on resume, fall back to the session's last recordedmodel/selectionevent, or the configured default model — mirroring what the interactive resume path effectively does.agents.resumeup front with a precise error ("agentOptions/model required for resume") instead of failing every later turn at prompt assembly.Context: this surfaced while verifying dsh-session-auto-resume (showcase post); the plugin now binds the default model explicitly and logs a loud
resume-no-modelline when it cannot.All reactions