Skip to content

feat(workflow): let a prompt step choose its model (#1177) - #1192

Merged
jeff-r2026 merged 13 commits into
Tencent:mainfrom
SaulMoro:feat/1177-step-model
Oct 18, 2026
Merged

jeff-r2026 merged 13 commits into
Tencent:mainfrom
SaulMoro:feat/1177-step-model

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #1177.
Closes #1193.

A prompt step can set model, and a workflow file can set defaults.model. Each takes an alias from models/aliases.yaml or a model name. The lease carries both as written, and the local driver resolves them with the member's aliases for the step's tool.

A workflow step with agent: on OpenCode now runs as its agent on OpenCode 1 and 2 (#1193). Before, it hung on both, and once started it would not have run as the definition.

 pull                                   parse.ts
+  model on prompt and prompt_file steps, defaults.model in the header
+  a template, a space, or model on a command or workflow step: diagnostic, not cached
 next                                   engine.ts
   leaseData
+    model           the step's, as written
+    defaultModel    prompt steps only: the file's defaults.model, else the one in effect
 promptReport                           local-driver.ts
+  chooseStepModel                      step-model.ts
+    step model > definition's model > defaults.model > tool default
+    resolveModelValue(value, tool)     aliases.ts, split out of resolveAgentModel
+    no model for the tool: model_unavailable, the tool does not start
+    on OpenCode, the definition's model and variant are passed too
   adapter.run                          local-tool-adapter.ts
+    resolve the tool, then ask that executable its OpenCode version, cached per path
+    start every tool with stdin ignored
-    claude -p … --output-format stream-json --verbose --agent A
+    claude -p … --output-format stream-json --verbose --agent A --model M --effort E
-    codex exec --json --model <definition's model> …
+    codex exec --json --model M -c model_reasoning_effort=E …
-    opencode run … --agent A
+    opencode run <instructions + prompt> --model M --variant E     OpenCode 1, mode: subagent file
+    opencode run … --agent A --model M --variant E                 OpenCode 1, other modes
+    opencode run … --agent A --model M#E                           OpenCode 2
+    result.applied: how the launch handed over the definition
   observeLaunch                        agent-observation.ts
-    model.requested   the pull record's model (Claude), the --model of Codex
+    model.requested   the model on argv: the step's or defaults.model's, else the definition's on Codex and OpenCode,
+                      else the pull record's for the deployed file
+    model.alias       the alias the step or defaults.model named, else the pull record's for the definition's model
+    effort.requested  the effort on argv
-    definition.applied  definitionHandoff(tool) before launch: main (Claude), prompt (Codex), none (OpenCode)
+    definition.applied  from the adapter's result, for every tool: OpenCode's depends on the major it probed

On Claude and Codex, a step whose model comes from its definition, or from nothing, launches with the same arguments as before.

Calls the issues left open:

  • Templates. The issue rejects a value containing ${{. Templates here are {{ … }}, and ${{ … }} in prompt text stays literal. Pull rejects any {{ in model, which covers both.
  • Claude under --agent. Claude Code 2.1.301 sends the --model and --effort it is given over the agent file's, and never sends the file's effort: under --agent. I read the request bodies through a local ANTHROPIC_BASE_URL: an agent file with effort: max and --effort low sent low; with no --effort it sent the model's default, as a file with no effort does. So Claude needs no model_unavailable for agent with model, and an alias with no effort runs at Claude's default effort.
  • OpenCode effort. OpenCode 1 takes --variant. OpenCode 2 has none, rejects it (Unrecognized flag: --variant), and reads the effort from --model provider/model#variant. The adapter resolves the executable it will launch and asks that path for its version (opencodeMajorVersion(path), the probe pull and doctor already use, now cached per path). Then it picks the form.
  • OpenCode agents. [bug] A workflow step on OpenCode does not run as its agent #1193 has the measurements: request bodies read through a local server posing as the Anthropic API. In short:
    • OpenCode 2 reads neither the model nor the variant from the agent file. OpenCode 1 reads both, but only for an agent it starts. So the launch passes the definition's own model and variant on the command line, unless the step's model replaces them.
    • OpenCode 1 does not start a mode: subagent agent under opencode run --agent, and pull writes every OpenCode agent that way. So on OpenCode 1 a subagent gets its instructions in the prompt, as Codex does, with no --agent. A warning names the file settings that then do not apply, such as permission.
    • Pull keeps deploying subagents. mode: all would put every team agent into the primary agent cycle of every member's interactive OpenCode.
  • A step on its agent's own model. OpenCode 1 on --agent applies the file's variant to a launch on the agent's own model that passes none. So that step, with no effort, fails with model_unavailable before launch. Only the adapter knows the launched version, so the check runs there. OpenCode 2 never applies the file's variant, and the OpenCode 1 prompt route does not read it.
  • stdin. OpenCode 1 and 2 wait for the end of a piped stdin before they start, and the adapter used to leave stdin an open pipe. Every tool now starts with stdin ignored.
  • A member's ~ or default, and teamai models switch. Each gives the tool no model, so each fails with model_unavailable and a message of its own.
  • Docs. The guide has no step keys table, so the rules are a paragraph after the step keys, in both languages.

Evidence

  • Before (main, real CLI): teamai pull refuses the workflow.

    ⚠ [workflows] workflows/review.yaml:7: field "steps[0].model": workflows/review.yaml uses `model`, which this teamai does not know. Update teamai.
    ⚠ [workflows] workflows/review.yaml has no runnable version
    

    After, criterion 9 (real CLI, Claude 2.1.301, a build of b0a73d5). The deployed reviewer.md has model: haiku. models/aliases.yaml maps strong to claude: { model: sonnet, effort: low }. Step review has agent: reviewer and model: strong. teamai pull, then teamai workflow start review --driver local. The launch:

    claude -p <prompt> --output-format stream-json --verbose --agent reviewer --model sonnet --effort low
    

    The init event has "model":"claude-sonnet-5-5". The review entry on teamai-reports:

    {
      "step": "review",
      "pass": 1,
      "agent": {
        "v": 1,
        "definition": { "name": "reviewer", "applied": "main" },
        "tool": { "name": "claude", "version": "2.1.301" },
        "model": { "requested": "sonnet", "reported": "claude-sonnet-5-5", "alias": "strong" },
        "effort": { "requested": "low" },
        "team": { "revision": "094af633eff84f054c6854c1957310f04f4f1f24", "localChanges": false },
        "project": { "commit": "094af633eff84f054c6854c1957310f04f4f1f24", "dirty": false },
        "teamai": "0.22.0",
        "driver": "local",
        "fingerprint": "635e56a38a3612488545dac0aab01f1d9c6e5e31944b46e10773fa6071a5bd9e"
      },
      "tool": "claude",
      "model": "claude-sonnet-5-5",
      "outcome": "success",
      "tokens": { "input": 2, "output": 81, "cachedInput": 0 }
    }

    The result event's modelUsage keys claude-haiku-5-5 and claude-sonnet-5-5, which is why the reported model comes from init.

  • Criterion 10, real CLI with Claude (d376feb). The workflow sets defaults: { model: strong }. Step plain has no agent, and step review runs reviewer, whose file has model: haiku. The launches:

    plain:   claude -p <prompt> --output-format stream-json --verbose --model sonnet --effort low
    review:  claude -p <prompt> --output-format stream-json --verbose --agent reviewer
    

    The entries on teamai-reports:

    plain:   model claude-sonnet-5-5   agent.model { requested: sonnet, reported: claude-sonnet-5-5, alias: strong }   agent.effort { requested: low }
    review:  model claude-haiku-5-5    agent.model { requested: haiku, reported: claude-haiku-5-5 }
    

    review runs on its file's model, so it carries no strong, although defaults.model is set for it.

  • [bug] A workflow step on OpenCode does not run as its agent #1193, real CLI on OpenCode 1.18.23 and 2.0.30. The team agent reviewer has model: strong, an alias to opencode: { model: anthropic/claude-sonnet-4-5, effort: high }, and tool_extras.opencode.permission. A marker is in its instructions. The deployed .opencode/agents/reviewer.md has mode: subagent, model: anthropic/claude-sonnet-4-5, variant: high, and permission. The step has agent: reviewer and no model. OpenCode's anthropic provider points at a local server that logs each request body. Its config default model is anthropic/claude-haiku-4-5.

    Build OpenCode Launch OpenCode ran Request with the message
    3226a7d 1.18.23 --agent reviewer until killed at 90 s none
    3226a7d 2.0.30 --agent reviewer until killed at 90 s none
    d09263d 1.18.23 instructions in the prompt, --model anthropic/claude-sonnet-4-5 --variant high 19 s sonnet-4-5, thinking 16000, agent prompt
    d09263d 2.0.30 --agent reviewer --model anthropic/claude-sonnet-4-5#high 6 s sonnet-4-5, thinking 16000, agent prompt

    On OpenCode 1 the driver warned: Agent "reviewer" runs on opencode 1 from the prompt, since opencode run does not start a subagent there, so it does not apply permission from the deployed agent file. The step is not blocked. The steps then fail with tool_failed, because the server answers each request with an error. On its first run, OpenCode 1 installed node_modules and package*.json into the fresh .opencode/. It also wrote a .gitignore there that ignores them and itself, so the read-only check did not trip.

    With a definition that sets variant: high and no model, OpenCode 2.0.30 on d09263d launched --agent reviewer and sent the config default, haiku-4-5, with the agent prompt. The driver warned: Agent "reviewer" sets a variant with no model, and opencode 2 takes a variant only with a model, so it does not apply variant from the deployed agent file. The step is not blocked.

  • OpenCode version of the launched path, real CLI (OpenCode 2.0.30, no login). The process PATH finds OpenCode 1.18.23 first, and the login shell the launch resolves through finds OpenCode 2.0.30. A step with model: mid (opencode: { model: anthropic/claude-sonnet-4-5, effort: low }):

    3c133704  version asked of  1.18.23 (process PATH)
              launched          2.0.30: run --format json <prompt> --model anthropic/claude-sonnet-4-5 --variant low
              OpenCode          Unrecognized flag: --variant in command opencode run
    4ed7fca8  version asked of  2.0.30 (the launched path)
              launched          2.0.30: run --format json <prompt> --model anthropic/claude-sonnet-4-5#low
              OpenCode          accepted the arguments
    
  • Tests. Criteria 1 and 2 in workflow-parse.test.ts and workflow-cache.test.ts. Criteria 3 to 8 and 10, and the OpenCode launches for both majors, in workflow-local-driver.test.ts. They run through the real adapter and an executable that answers --version, reads stdin to its end, and writes its argv to a file. The leases through the scope chain in workflow-engine.test.ts, and a conformance scenario for model and defaultModel.

    The run record fields: workflow-local-driver.test.ts ("the agent a local pass ran as") covers the alias of defaults.model only for a step it chose, and an OpenCode pass with the handoff its launch used. The per-major OpenCode test asserts applied from the real adapter.

    On d376feb, npx vitest run passes 465 files and 10985 tests. npm run test:e2e passes 28 tests on the e2e files with a fake tool or the OpenCode version probe: workflow-prompt, workflow-conformance, workflow-records-reports-branch-1083, and opencode-versions-915. npx tsc --noEmit and npm run lint exit 0. The OpenCode runs above are on d09263d, before the merge, which changed no OpenCode argv. The criterion 9 run is on b0a73d5. The criterion 10 run, repeated on d376feb, published the same entries.

  • Not covered locally. A real Codex run, an OpenCode step that completes against a real provider, and the gitlab and github providers. Left to CI.

Known limits:

  • On OpenCode 1, the prompt route does not apply a subagent file's other settings, such as permission or temperature. The warning names them.
  • An OpenCode definition with a variant and no model gets no variant on the command line, since OpenCode 2 takes one only inside --model. OpenCode 2 and the OpenCode 1 prompt route then warn about it.
  • A failed OpenCode version probe counts as version 1 for that path for the rest of the process, as it did before. An OpenCode 2 behind such a probe then rejects the --variant it gets, a definition's included, and the step fails with tool_failed.
  • A model or alias value that starts with - reaches the tool's argv as its own token, after --model. OpenCode would read it as a flag. Only the team repo and the member's own aliases file set these values.
  • When a member's local aliases turn a definition's alias off for the tool (~ or default), the deployed agent file has no model, so defaults.model applies to that step, as for any definition that sets no model for the tool.
  • A Claude agent file with model: inherit counts as a definition that sets a model, so defaults.model does not apply to it.
  • On Codex, the warning about agent settings Codex ignores still lists the agent file's model_reasoning_effort when the step's effort replaces it.

With #1175

#1175 also edits promptReport and the Claude and Codex launch arguments. Whichever merges second resolves the overlap.

Merge Danger

Door: two-way.

Workflows that use neither key lease and launch as before on Claude and Codex. An OpenCode step with an agent now passes the definition's model and variant, and on OpenCode 1 a subagent runs from the prompt. Before, those steps hung. An older teamai refuses a workflow file that uses model or defaults at pull, as it does any key it does not know.

Blast Radius: workflows.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Probe the resolved OpenCode executable — src/workflow/local-tool-adapter.ts:269. The version probe runs before resolveTool and can inspect a different installation. If the process PATH finds OpenCode 1 while the login-shell resolver finds OpenCode 2, a step selecting an alias with effort receives --variant, which OpenCode 2 rejects. This makes a supported prompt step fail rather than run. Resolve the executable first, then probe and cache its version by path.

Testing documentation is sufficient: the description records representative real-CLI runs on this branch, including Claude model/default selection. The explicitly deferred provider and agent coverage is not blocking. No PR code was run, built, or installed.

…unches

The version probe ran `opencode --version` from the process PATH, while
the launch resolves opencode through a login shell first. When the two
found different installs, the step got the other version's effort form
and OpenCode rejected it. Resolve the executable first and probe that
path, cached per path.
…when the agent is not a subagent

opencode run --agent runs the default agent in place of a subagent, so
a subagent's variant never comes back. teamai writes every OpenCode
agent as a subagent unless tool_extras.opencode sets mode, so the
refusal fired on launches that run as the step asks.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Fixed in 4ed7fca. run() resolves the tool first, then asks that executable for its version, with the probe cached per path. The PR body has the real run where PATH and the login shell find different OpenCode versions: 3c13370 sent OpenCode 2 --variant, and 4ed7fca sends #low.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Restrict the agent-variant rejection to OpenCode 1 — src/workflow/step-model.ts:93. On OpenCode 2, a primary agent with model: anthropic/claude-sonnet and variant: low, plus a step explicitly selecting that model without effort, fails with model_unavailable before launch. The description’s measurements establish that OpenCode 2 ignores the agent’s variant, so this supported step should run. Gate this rejection on the resolved executable’s version.

The earlier executable-probe finding is resolved: the adapter resolves the executable first and caches its version by path.

Testing documentation is sufficient; representative real-CLI runs are recorded, and additional coverage is explicitly deferred. No PR code was run, built, or installed.

OpenCode 2 reads neither the model nor the variant from the agent file,
and OpenCode 1 does not start a subagent under opencode run --agent,
which is how pull deploys every OpenCode agent. The launch now passes
the definition's model and variant on argv, and on OpenCode 1 hands a
subagent its instructions in the prompt, warning about the file
settings that then do not apply. OpenCode 2 warns about a variant it
cannot take, one set with no model.

The own-model refusal moves into the adapter, which knows the launched
version: only OpenCode 1 on --agent brings the file's variant back.

Every tool now starts with stdin ignored. OpenCode 1 and 2 wait for the
end of a piped stdin, so every OpenCode step hung.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Fixed in d09263d. The check moved from chooseStepModel into the adapter, which knows the major of the executable it launches. It now fails a step only on OpenCode 1, and only when the agent runs through --agent, meaning its file's mode is not subagent. On OpenCode 2 the same step launches --agent reviewer --model anthropic/claude-sonnet, since OpenCode 2 does not read the file's variant. Both cases are in workflow-local-driver.test.ts ("fails an OpenCode 1 step on its primary agent's own model with no effort when the agent sets a variant").

@github-actions

Copy link
Copy Markdown

No new findings in the specified PR diff.

Both earlier findings are resolved:

  • The adapter resolves the OpenCode executable before probing its version and caches the result by path.
  • The agent-variant rejection applies only to OpenCode 1’s --agent route, not OpenCode 2 or the subagent prompt route.

Testing documentation is sufficient: the description records representative real-CLI runs, including runs on the current head. Explicitly deferred provider and agent coverage is not blocking.

No PR code was run, built, or installed.

Resolve the overlap with Tencent#1184. The observation now records the model and
effort the launch passes on argv. The alias comes from chooseStepModel when
the step or defaults.model chose the model, and from the pull record when the
definition did. The adapter reports the OpenCode handoff it used.
@github-actions

Copy link
Copy Markdown

No new findings in the specified PR diff.

Both earlier findings are resolved:

  • OpenCode’s executable is resolved before its version is probed, with results cached by path.
  • The agent-variant rejection applies only to OpenCode 1’s --agent route, not OpenCode 2 or the subagent prompt route.

Testing documentation is sufficient: the description records representative real-CLI verification. Older tested commits are a note, not blocking; explicitly deferred provider and agent coverage is also not blocking.

No PR code was run, built, or installed.

@github-actions

Copy link
Copy Markdown

No new findings in the specified PR diff.

Both earlier findings are resolved:

  • The adapter resolves OpenCode’s executable before probing its version and caches the result by path.
  • The agent-variant rejection applies only to OpenCode 1’s --agent route, not OpenCode 2 or the subagent prompt route.

Testing documentation is sufficient: the description records representative real-CLI verification, including Claude default-model selection on the current head. Older OpenCode test records and explicitly deferred provider/agent coverage are not blocking.

No PR code was run, built, or installed.

@jeff-r2026
jeff-r2026 merged commit ce4f656 into Tencent:main Oct 18, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] A workflow step on OpenCode does not run as its agent [feat] Let a workflow choose the model of its prompt steps

2 participants