Skip to content

agent step: model silently ignores templates — validate passes, then the run dies quoting the raw {{...}} as a model name #114

Description

@tylerdavis

An agent step's model field is not rendered against the step's scope, but nothing rejects a template there either. validate passes, the plan runs, and the failure arrives at the agent step itself — with the raw template text quoted back as if it were a model name.

Repro

- id: E6
  tool_name: map
  input:
    over: '{{E5b.items}}'
    concurrency: 6
    do:
      tool_name: agent
      input:
        prompt: '... {{item.name}} ...'      # renders
        model: '{{input.scout_model}}'       # does NOT render
        tools: [user__repo_grep, user__repo_file]
        max_iterations: 18
        output_schema: { ... }

graph plan validateok: true. The run then reaches E6 and dies:

Error: plan failed at step E6 (map): `do` item 0 (agent): LLM call failed:
no model named '{{input.scout_model}}' is configured;
available names: aggregator, opus, sonnet, default, chat, planner, solver, repair, embedder, judge

Why this is worth fixing

The asymmetry is the trap. In a plan, builtin__infer's model is an ordinary tool input and renders like everything else — model: '{{input.model}}' works, and it is exactly how you parameterize a model per call. The agent control step's model looks identical in YAML and silently doesn't. I hit this authoring a review plan that fans a rubric out across several models: the infer step took {{input.model}} fine, so I wrote the same thing on the scout's agent step and had no reason to think it differed.

The failure is deferred past the expensive part. model is a static value known at load time — there is nothing to defer. But because it is only consulted at agent.rs:412, the plan runs its whole prefix first. In my case that was a changed-file listing, seven parallel file fetches, a grouping inference, and a filter — about two minutes and a real inference call — before the step that could never have worked was reached. Every retry pays that again.

The error blames configuration. no model named '{{input.scout_model}}' is configured; available names: … reads as "you forgot a [models.named] entry", which sends you to config.toml instead of to the plan. The braces are the whole clue and they are easy to read past in a long name list.

Current behavior, for reference

This is consistent and deliberate as implemented — only prompt and systemPrompt render:

  • crates/graph-core/src/pipeline/agent.rs:29-30 — "prompt and systemPrompt render against the step's scope when the loop starts, not at parse time."
  • crates/graph-core/src/pipeline/agent.rs:373-377 — those two are the only fields passed through render.
  • crates/graph-core/src/pipeline/agent.rs:412spec.model.as_deref().unwrap_or("chat"), handed straight to chat_named.

docs/plans/agent-step.mdx:42-47 matches: the prompt and system_prompt rows say they render, and the model row says nothing either way. So the docs are accurate by omission — a reader who already suspects the difference can confirm it. The gap is that nothing tells a reader who doesn't suspect it, at the moment they get it wrong.

Two ways to close it

Either render it. model resolves against the same scope as prompt, so {{input.scout_model}} and {{item.model}} work and the field stops being a special case. This is the one that makes plan-level model fan-out expressible for agent steps at all — worth noting map-over-models with a per-item agent model has no workaround today short of duplicating the step.

Or reject it at validation. The whole input is already checked at load time (agent-step.mdx: unknown fields, non-string prompt, bad output_schema, max_iterations under 1, empty tools, template references that point forward or at nothing). Add: a model containing {{ fails validation with something like step E6: `model` is a static field and does not render templates — use a literal model or role name, or set the model on the `chat` role in config. Then the plan never reaches a run it cannot finish.

Rendering is the better outcome if the static-ness isn't load-bearing elsewhere; validation is the smaller change and still removes the deferred failure and the misleading error. Either way the docs row for model should say which it is, the way the prompt and system_prompt rows do.

Found while building a multi-model PR reviewer in another repo; happy to take whichever fix you prefer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions