diff --git a/src/main/agent-launch/agent-launch-mode.ts b/src/main/agent-launch/agent-launch-mode.ts index 3ef3c373ba3..874f5695656 100644 --- a/src/main/agent-launch/agent-launch-mode.ts +++ b/src/main/agent-launch/agent-launch-mode.ts @@ -133,9 +133,6 @@ export function decideAgentLaunchMode(args: { /** Host-internal: the caller's contract is a terminal handle, so the chat default cannot apply. */ terminalOnly?: boolean vocabulary?: AgentLaunchModeVocabulary - /** Registered agents (beyond Claude and Codex) this surface can open as structured; defaults to - * every agent this host registers. */ - registeredStructuredAgents?: readonly string[] }): AgentLaunchModeReceipt { const { placement, settings } = args const vocabulary = args.vocabulary ?? DEFAULT_LAUNCH_VOCABULARY @@ -160,7 +157,7 @@ export function decideAgentLaunchMode(args: { reusesTerminal: Boolean(placement.terminal), hostCapabilities: RUNTIME_CAPABILITIES, // This host is the one that will run the agent, so its own registrations answer. - hostStructuredAgents: args.registeredStructuredAgents ?? REGISTERED_STRUCTURED_AGENTS, + hostStructuredAgents: REGISTERED_STRUCTURED_AGENTS, // The host resolves the floating workspace to its configured directory; create-support still // answers for the resolved workspace, including whether it uses WSL. ...(placement.workspaceKind ? { workspaceKind: placement.workspaceKind } : {}), diff --git a/src/main/runtime/orchestration/structured-worker-group-addressing.ts b/src/main/runtime/orchestration/structured-worker-group-addressing.ts index 66cf8322e91..e739c16f4b0 100644 --- a/src/main/runtime/orchestration/structured-worker-group-addressing.ts +++ b/src/main/runtime/orchestration/structured-worker-group-addressing.ts @@ -15,7 +15,6 @@ * the smaller shape instead, and nothing here has to invent a `worktreePath` or a `branch`. */ -import type { TuiAgent } from '../../../shared/tui-agent' import type { TerminalAgent } from '../../../shared/terminal-agent' import { holdStructuredWorker, @@ -65,7 +64,7 @@ export function listAddressableStructuredWorkers( .map((identity) => ({ handle: identity.handle, worktreeId: identity.worktreeId, - agentIdentity: structuredWorkerAgent(identity) as TuiAgent + agentIdentity: structuredWorkerAgent(identity) })) } diff --git a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts index 3fc96c595b7..68120a5ad35 100644 --- a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts +++ b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.ts @@ -20,6 +20,7 @@ import type { } from '../../../../shared/agent-session-journal-types' import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host' import { getStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { OrcaRuntimeService } from '../../orca-runtime' import { OrchestrationError } from '../../orchestration/orchestration-error' import { mintAgentSessionOperationId } from '../../orchestration/structured-pointer-operation-id' @@ -79,7 +80,7 @@ export function releaseStructuredWorkerSession( export async function createStructuredWorkerSession(args: { runtime: OrcaRuntimeService worktreeId: string - agent: 'claude' | 'codex' + agent: TuiAgent dispatchId: string /** The dispatch's own `--model`/`--effort`, already narrowed to the seedable string subset. */ options?: Readonly> diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-mode.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-mode.ts index fcdaade4702..9cd9f51e0ac 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-mode.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-mode.ts @@ -55,10 +55,7 @@ export function decideWorkerStartMode(args: { return decideAgentLaunchMode({ placement: args.params, settings: args.settings, - vocabulary: WORKER_START_VOCABULARY, - // The structured worker factory creates Claude and Codex only; any other agent (Grok included) - // starts as a terminal worker until structured workers take registered agents. - registeredStructuredAgents: [] + vocabulary: WORKER_START_VOCABULARY }) } diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts index 9d24f3dad1e..4398be07343 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts @@ -1,56 +1,66 @@ -// An orchestration worker for a registered agent (Grok) opens as a terminal worker, as before Grok -// had a structured chat: the structured worker factory creates Claude and Codex only, so a -// structured verdict for Grok would fail the start. Other launches still open Grok as a chat. +// Every agent the host registers for native chat starts as a native-chat worker, the same as a +// chat opened any other way; an agent with no native chat still starts in a terminal. import { describe, expect, it } from 'vitest' import { decideAgentLaunchMode } from '../../../agent-launch/agent-launch-mode' +import { isTuiAgent } from '../../../../shared/tui-agent-config' +import { STRUCTURED_AGENT_RUNTIME_REGISTRATIONS } from '../../structured-agent-runtime-registrations' import { decideWorkerStartMode, resolveWorkerStartModeOnHost } from './orchestration-worker-start-mode' -import { createStructuredWorkerSessionForWorktree } from './orchestration/worker/worker-topology' const STRUCTURED_PREFERENCE = { experimentalNativeChat: true } as const -describe('a Grok worker and the structured-chat setting', () => { - it('starts as a terminal worker while the setting is on; Claude starts structured', async () => { - const grok = decideWorkerStartMode({ - params: { agent: 'grok' }, +const REGISTERED_AGENTS = STRUCTURED_AGENT_RUNTIME_REGISTRATIONS.map( + ({ definition }) => definition.agent +).filter(isTuiAgent) + +describe('a worker for a registered native-chat agent', () => { + it('covers every registered agent, the ACP and Pi agents included', () => { + expect(REGISTERED_AGENTS).toHaveLength(STRUCTURED_AGENT_RUNTIME_REGISTRATIONS.length) + expect(REGISTERED_AGENTS).toEqual(expect.arrayContaining(['grok', 'opencode', 'omp', 'pi'])) + }) + + it.each(REGISTERED_AGENTS)('starts %s structured, as any other launch does', async (agent) => { + const mode = decideWorkerStartMode({ params: { agent }, settings: STRUCTURED_PREFERENCE }) + expect(mode).toMatchObject({ mode: 'structured', reason: 'user_default' }) + expect( + decideAgentLaunchMode({ placement: { agent }, settings: STRUCTURED_PREFERENCE }) + ).toMatchObject({ mode: 'structured' }) + const host = { getStructuredAgentSessionCreateSupport: async () => ({ supported: true }) } + expect(await resolveWorkerStartModeOnHost(host, mode, 'wt-1', agent)).toMatchObject({ + mode: 'structured' + }) + }) + + it('still starts a terminal worker when the host refuses that agent here', async () => { + const mode = decideWorkerStartMode({ + params: { agent: 'opencode' }, settings: STRUCTURED_PREFERENCE }) - expect(grok).toMatchObject({ mode: 'terminal', reason: 'agent_without_structured_session' }) - const host = { getStructuredAgentSessionCreateSupport: async () => ({ supported: true }) } - expect(await resolveWorkerStartModeOnHost(host, grok, 'wt-1', 'grok')).toMatchObject({ - mode: 'terminal' - }) - expect( - decideWorkerStartMode({ params: { agent: 'claude' }, settings: STRUCTURED_PREFERENCE }) - ).toMatchObject({ mode: 'structured', reason: 'user_default' }) - }) - - it('still opens Grok as a structured chat for other launches', () => { - expect( - decideAgentLaunchMode({ placement: { agent: 'grok' }, settings: STRUCTURED_PREFERENCE }) - ).toMatchObject({ mode: 'structured' }) - }) - - it('would fail a structured Grok worker: the factory creates Claude and Codex only', async () => { - await expect( - createStructuredWorkerSessionForWorktree({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the factory refuses the agent before it reads the runtime. - runtime: {} as never, - worktreeId: 'wt-1', - agent: 'grok', - dispatchId: 'd-1', - effects: [] + const host = { + getStructuredAgentSessionCreateSupport: async () => ({ + supported: false, + reason: 'agent' as const }) - ).rejects.toThrow(/Structured workers support claude and codex/) + } + expect(await resolveWorkerStartModeOnHost(host, mode, 'wt-1', 'opencode')).toMatchObject({ + mode: 'terminal', + reason: 'structured_unsupported_on_host' + }) }) - it('starts as a terminal agent while the setting is off, as Claude does', () => { - for (const agent of ['grok', 'claude']) { + it('starts an agent with no native chat as a terminal worker', () => { + expect( + decideWorkerStartMode({ params: { agent: 'gemini' }, settings: STRUCTURED_PREFERENCE }) + ).toMatchObject({ mode: 'terminal', reason: 'agent_without_structured_session' }) + }) + + it('starts a terminal agent while the setting is off, as Claude does', () => { + for (const agent of ['opencode', 'claude']) { expect( decideWorkerStartMode({ params: { agent }, diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts index 3e86dea3e28..a11eb4b4c00 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts @@ -3,7 +3,6 @@ import { narrowStructuredLaunchSeedOptions } from '../../../../../../shared/nati import type { TuiAgent } from '../../../../../../shared/tui-agent' import type { OrcaRuntimeService } from '../../../../orca-runtime' import type { OrchestrationDb } from '../../../../orchestration/db' -import { OrchestrationError } from '../../../../orchestration/orchestration-error' import { createStructuredWorkerSession } from '../../orchestration-structured-worker-session' export type WorkerEffect = { @@ -102,12 +101,6 @@ export async function createStructuredWorkerSessionForWorktree(args: { launchPreferences?: AgentLaunchPreferences effects: WorkerEffect[] }): Promise>> { - if (args.agent !== 'claude' && args.agent !== 'codex') { - throw new OrchestrationError( - 'agent_unconfigured', - `Structured workers support claude and codex; ${args.agent} has no structured session.` - ) - } const options = narrowStructuredLaunchSeedOptions(args.launchPreferences) const created = await createStructuredWorkerSession({ runtime: args.runtime, diff --git a/src/main/runtime/structured-worker-authority.test.ts b/src/main/runtime/structured-worker-authority.test.ts index f286d4b668a..aa510ba76e6 100644 --- a/src/main/runtime/structured-worker-authority.test.ts +++ b/src/main/runtime/structured-worker-authority.test.ts @@ -17,7 +17,7 @@ const { const SESSION_ID = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' -function installRecordProvider(provider: 'claude' | 'codex' | null): void { +function installRecordProvider(provider: string | null): void { hostRef.current = { deps: { store: { @@ -71,6 +71,14 @@ describe('structuredWorkerAgent', () => { expect(structuredWorkerAgent(identity)).toBe('codex') }) + it.each(['opencode', 'grok', 'omp', 'pi'])( + 'reads a rehydrated %s worker as itself, not as Claude', + (provider) => { + installRecordProvider(provider) + expect(structuredWorkerAgent(rehydratedIdentity())).toBe(provider) + } + ) + it('keeps the provider this process registered, without consulting the record', () => { installRecordProvider('claude') const handle = mintStructuredWorkerHandle() diff --git a/src/main/runtime/structured-worker-authority.ts b/src/main/runtime/structured-worker-authority.ts index 2c2e4703d4e..0916ca11295 100644 --- a/src/main/runtime/structured-worker-authority.ts +++ b/src/main/runtime/structured-worker-authority.ts @@ -32,7 +32,8 @@ import { structuredWorkerAddressable } from './structured-worker-custody' import { AGENT_SESSION_FOUNDING_FENCE } from './agent-session-record-founding' -import { isAgentSessionHandleProvider } from '../../shared/agent-session-provider-handle' +import { isTuiAgent } from '../../shared/tui-agent-config' +import type { TuiAgent } from '../../shared/tui-agent' import { isStructuredWorkerHandle, structuredWorkerIdentities, @@ -218,17 +219,17 @@ export function resolveStructuredWorkerAuthority( * The registry carries it only for a session THIS process started; a rehydrated entry has null, * because the durable worker-terminal row does not record a provider. The durable agent-session * record does, and it is the only source that survives a restart — defaulting instead would - * relabel every restarted Codex worker as Claude, permanently, because the startup release + * relabel every restarted non-Claude worker as Claude, permanently, because the startup release * reconciler stamps the frozen journal archive with whatever it is told here. */ -export function structuredWorkerAgent(identity: StructuredWorkerIdentity): 'claude' | 'codex' { +export function structuredWorkerAgent(identity: StructuredWorkerIdentity): TuiAgent { if (identity.agent) { return identity.agent } - // Workers are Claude or Codex sessions only: dispatch refuses any other agent. const running = structuredWorkerSession(identity) const provider = running.kind === 'unverifiable' ? undefined : running.record.provider - return isAgentSessionHandleProvider(provider) ? provider : 'claude' + // An unreadable record or a provider this build doesn't know falls back to Claude. + return isTuiAgent(provider) ? provider : 'claude' } export type StructuredWorkerObservation = { diff --git a/src/main/runtime/structured-worker-identity.ts b/src/main/runtime/structured-worker-identity.ts index 2ee60c28b5f..e1899f4fdce 100644 --- a/src/main/runtime/structured-worker-identity.ts +++ b/src/main/runtime/structured-worker-identity.ts @@ -26,6 +26,7 @@ import { } from '../../shared/structured-agent-session-projection' import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../shared/stable-pane-id' import { isOrcaSessionId, type OrcaSessionId } from '../../shared/orca-session-address' +import type { TuiAgent } from '../../shared/tui-agent' import { STRUCTURED_WORKER_HANDLE_PREFIX, isStructuredWorkerHandle @@ -50,7 +51,7 @@ export type StructuredWorkerIdentity = { */ sessionId: string /** Null when the entry was rehydrated from the durable row, which does not carry the provider. */ - agent: 'claude' | 'codex' | null + agent: TuiAgent | null paneKey: string processIncarnation: string worktreeId: string