Start orchestration workers as native chats for every agent that has native chat (#26776)

* fix(orchestration): start native-chat workers for every registered native-chat agent

* chore(orchestration): correct the restored worker agent fallback comment
This commit is contained in:
Brennan Benson
2026-10-09 13:06:40 -07:00
committed by GitHub
parent 8defd660c7
commit dfffb302f8
9 changed files with 68 additions and 61 deletions
+1 -4
View File
@@ -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 } : {}),
@@ -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)
}))
}
@@ -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<Record<string, string>>
@@ -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
})
}
@@ -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 },
@@ -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<Awaited<ReturnType<typeof createStructuredWorkerSession>>> {
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,
@@ -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()
@@ -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 = {
@@ -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