mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user