Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/site/content/docs/cli/orchestration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ orca orchestration worker-start --task <taskId> --worktree new-child --name bill
orca orchestration worker-start --task <taskId> --worktree current --agent claude --model <opaque-model-id> --effort high --json
```

`--model` accepts opaque provider model IDs for Claude, Codex, and Cursor. `--effort` requires `--model` and only applies when that agent/model supports the level. Neither flag can combine with `--terminal` (reuse an existing pane). Overrides apply to that launch only and show under `launch.requested` / `launch.effective` in the start receipt. Federated starts need a worker host that advertises launch-preference support.
`--model` accepts opaque provider model IDs for Claude, Codex, Cursor, Antigravity, and OpenCode. `--effort` requires `--model` and only applies when that agent/model supports the level. Neither flag can combine with `--terminal` (reuse an existing pane). Overrides apply to that launch only and show under `launch.requested` / `launch.effective` in the start receipt. Federated starts need a worker host that advertises launch-preference support.

Wait for completions (process every message in a Delivery, then ack):

Expand Down
2 changes: 1 addition & 1 deletion skill-guides/orchestration/references/coordinator-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ when an older CLI rejects the flag. A nested worker must respect

## Launch preferences

For a fresh Claude, Codex, Cursor, or Antigravity terminal, `--model` accepts an opaque
For a fresh Claude, Codex, Cursor, Antigravity, or OpenCode terminal, `--model` accepts an opaque
provider model ID. Pass it only when the user named a model; otherwise omit it
so the worker inherits the user's configured agent default. Add `--effort` only
when that model supports it:
Expand Down
4 changes: 2 additions & 2 deletions src/cli/bundled-skill-guides.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/cli/specs/orchestration-worker-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const ORCHESTRATION_WORKER_COMMAND_SPECS: CommandSpec[] = [
notes: [
'Current and existing worktrees never rerun setup; a fresh agent terminal is created unless --terminal is explicit.',
'When reusing --terminal, pass --worktree for that terminal; current means the coordinator worktree.',
'--model supports Claude, Codex, Cursor, and Antigravity opaque provider model ids; --effort requires --model. Neither can combine with --terminal.',
'--model supports Claude, Codex, Cursor, Antigravity, and OpenCode opaque provider model ids; --effort requires --model. Neither can combine with --terminal.',
'New worktrees use agent-first creation and default --setup to run. Repository start-immediately runs setup beside the agent; wait-for-setup gates agent readiness and task input.',
'Creation flags (--name, --repo, --base-branch, --display-name, --comment, --setup) are rejected for current/existing worktrees. Use exact --repo on the selected server; project/host convenience routing remains on worktree create.',
"How the worker runs follows the user's own setting for new agent tabs; there is no flag for it and no caller needs to ask. A dispatch the setting cannot apply to still starts, so the placement, agent, and launch options passed here are always the ones honoured.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ describe('orchestration worker launch preferences', () => {
).toEqual({ model: 'gpt-5.6-sol' })
})

it('passes an opaque OpenCode model through the launch catalog', () => {
expect(
resolveWorkerLaunchPreferences({
agent: 'opencode',
model: 'zai-coding-plan/glm-5.3-flash'
})
).toEqual({
preferences: { model: 'zai-coding-plan/glm-5.3-flash' },
receipt: {
requested: {
agent: 'opencode',
model: 'zai-coding-plan/glm-5.3-flash',
effort: null
},
effective: {
agent: 'opencode',
model: 'zai-coding-plan/glm-5.3-flash',
effort: null
}
}
})
})

it.each([
{
model: 'gpt-5.6-sol',
Expand Down Expand Up @@ -138,6 +161,16 @@ describe('orchestration worker launch preferences', () => {
}
})

it('rejects unsupported OpenCode effort instead of silently dropping it', () => {
expect(() =>
resolveWorkerLaunchPreferences({
agent: 'opencode',
model: 'zai-coding-plan/glm-5.3-flash',
effort: 'high'
})
).toThrow('Agent opencode model zai-coding-plan/glm-5.3-flash does not support effort high.')
})

it('rejects effort without a model', () => {
expect(() => resolveWorkerLaunchPreferences({ agent: 'codex', effort: 'high' })).toThrow(
'--effort requires --model'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { AgentLaunchPreferences } from '../../../../../../shared/agent-session-host-authority'
import {
findCatalogModel,
findCatalogOption,
getAgentSessionOptionCatalog
findCatalogOption
} from '../../../../../../shared/agent-session-option-catalog'
import { resolveAgentSessionOptionLaunch } from '../../../../../../shared/agent-session-option-launch'
import {
getAgentSessionOptionLaunchCatalog,
resolveAgentSessionOptionLaunch
} from '../../../../../../shared/agent-session-option-launch'
import { ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY } from '../../../../../../shared/protocol-version'
import type { TuiAgent } from '../../../../../../shared/tui-agent'
import { OrchestrationError } from '../../../../orchestration/orchestration-error'
Expand Down Expand Up @@ -66,7 +68,7 @@ export function resolveWorkerLaunchPreferences(args: {
}
}

const catalog = getAgentSessionOptionCatalog(args.agent)
const catalog = getAgentSessionOptionLaunchCatalog(args.agent)
if (!catalog?.supportsWorkerLaunchPreferences || !catalog.modelApply.launchArgs) {
throw new OrchestrationError(
'invalid_argument',
Expand Down
34 changes: 34 additions & 0 deletions src/shared/agent-session-option-catalog-opencode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { getAgentSessionOptionCatalog } from './agent-session-option-catalog'
import {
removeOverriddenAgentSessionArgs,
resolveAgentSessionOptionLaunch
} from './agent-session-option-launch'

describe('OpenCode launch options', () => {
it('does not expose worker-only launch options as an interactive session catalog', () => {
expect(getAgentSessionOptionCatalog('opencode')).toBeNull()
})

it('maps an opaque model id to the OpenCode --model flag', () => {
expect(
resolveAgentSessionOptionLaunch('opencode', {
model: 'zai-coding-plan/glm-5.3-flash'
})
).toEqual({
args: ['--model', 'zai-coding-plan/glm-5.3-flash'],
appliedValues: { model: 'zai-coding-plan/glm-5.3-flash' }
})
})

it('removes a configured model flag before applying a per-launch model', () => {
expect(
removeOverriddenAgentSessionArgs('opencode', { model: 'zai-coding-plan/glm-5.3-flash' }, [
'--model',
'opencode/global-default',
'--log-level',
'DEBUG'
])
).toEqual(['--log-level', 'DEBUG'])
})
})
16 changes: 16 additions & 0 deletions src/shared/agent-session-option-catalog-opencode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { hasFlag } from './agent-cli-flag-detection'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import type { AgentSessionOptionCatalog } from './agent-session-option-catalog-types'

const MODEL_FLAGS = ['-m', '--model'] as const

// Why: worker-start needs launch serialization without exposing a new Native Chat option surface.
export const OPENCODE_LAUNCH_OPTION_CATALOG: AgentSessionOptionCatalog = {
supportsWorkerLaunchPreferences: true,
models: [],
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, MODEL_FLAGS),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, MODEL_FLAGS)
}
}
12 changes: 10 additions & 2 deletions src/shared/agent-session-option-launch.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import type { AgentType } from './agent-status-types'
import { findCatalogModel, getAgentSessionOptionCatalog } from './agent-session-option-catalog'
import { OPENCODE_LAUNCH_OPTION_CATALOG } from './agent-session-option-catalog-opencode'
import type { AgentSessionOptionCatalog } from './agent-session-option-catalog-types'
import type { SessionOptionValue } from './native-chat-session-options'

export type ResolvedSessionOptionLaunch = {
args: string[]
appliedValues: Record<string, SessionOptionValue>
}

export function getAgentSessionOptionLaunchCatalog(
agent: AgentType
): AgentSessionOptionCatalog | null {
return agent === 'opencode' ? OPENCODE_LAUNCH_OPTION_CATALOG : getAgentSessionOptionCatalog(agent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAgentSessionOptionLaunchCatalog special-cases only 'opencode', so worker-start --agent opencode2 --model … still throws "does not support launch-time model selection" even though opencode2 is the same OpenCode family whose config note says it uses the same flags. Was scoping to opencode deliberate here, or should opencode2 (and possibly mimo-code) route to the same launch catalog? Not a regression — those agents were rejected before this PR too.

}

export function removeOverriddenAgentSessionArgs(
agent: AgentType,
values: Record<string, SessionOptionValue> | null | undefined,
tokens: readonly string[]
): string[] {
const catalog = getAgentSessionOptionCatalog(agent)
const catalog = getAgentSessionOptionLaunchCatalog(agent)
const modelId = typeof values?.model === 'string' ? values.model : null
if (!catalog || !values || !modelId) {
return [...tokens]
Expand All @@ -34,7 +42,7 @@ export function resolveAgentSessionOptionLaunch(
trailingAgentArgs: readonly string[] = [],
includeCatalogDefaults = true
): ResolvedSessionOptionLaunch {
const catalog = getAgentSessionOptionCatalog(agent)
const catalog = getAgentSessionOptionLaunchCatalog(agent)
const modelId = typeof values?.model === 'string' ? values.model : null
if (!catalog || !values || !modelId) {
return { args: [], appliedValues: {} }
Expand Down
18 changes: 18 additions & 0 deletions src/shared/tui-agent-startup-session-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ describe('tui agent startup session options', () => {
expect(plan?.sessionOptions).toEqual({ model: 'gemini-3.1-pro-high', effort: 'high' })
})

it('keeps OpenCode agent arguments while replacing only the per-launch model', () => {
const plan = buildAgentStartupPlan({
agent: 'opencode',
prompt: '',
cmdOverrides: {},
platform: 'linux',
allowEmptyPromptLaunch: true,
sessionOptions: { model: 'zai-coding-plan/glm-5.3-flash' },
sessionOptionsOverrideAgentArgs: true,
agentArgs: '--share --model opencode/global-default'
})
expect(plan?.launchCommand).toBe("opencode '--share' '--model' 'zai-coding-plan/glm-5.3-flash'")
expect(plan?.launchConfig.agentCommand).toBe(
"opencode '--share' '--model' 'opencode/global-default'"
)
expect(plan?.sessionOptions).toEqual({ model: 'zai-coding-plan/glm-5.3-flash' })
})

it('inserts worker preferences before an argument terminator', () => {
const plan = buildAgentStartupPlan({
agent: 'codex',
Expand Down
Loading