mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
refactor(agent-launch): make the launch-mode decision surface-neutral
`decideWorkerStartMode` was the only shared answer to "structured chat session
or terminal agent?", but it lived in an orchestration-named module and spoke
orchestration's vocabulary, so the other launch surfaces could not call it.
Move the decision to `main/agent-launch/agent-launch-mode` unchanged and leave
`orchestration-worker-start-mode` as the adapter that supplies the noun.
A worker is not a special kind of launch; it is the same launch with a dispatch
attached. Naming the receipt's subject is the only thing orchestration actually
contributed, so that is the only thing the adapter keeps: "worker" in both
sentences, plus the `--terminal` wording, which reads as nonsense anywhere a
`--terminal` flag does not exist. Both are pinned, because they are asserted.
No behavior change. The receipts are byte-identical for every reachable case,
proven by running the new pin against both implementations.
Also pins the wording, which nothing was holding. The existing suites assert
`toContain` fragments ('terminal agent', 'cannot create') and the CLI suite
asserts a receipt handed to it by a mock rather than one this code produced;
all six files stayed green against a deliberately corrupted vocabulary. A
dispatch receipt is the only place a structured-to-terminal downgrade explains
itself, so the whole sentence is the contract, not a fragment of it.
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Which surface a launch gets — a structured chat session or a terminal agent — decided from the
|
||||
* user's own settings and the executing host's answer.
|
||||
*
|
||||
* No caller passes a mode. If the user's default is that a new agent tab opens as a structured
|
||||
* native chat, then every launch is one: an orchestration worker, a mobile create, a CLI create,
|
||||
* a renderer tab. That default is a preference rather than a demand, so a launch it cannot apply
|
||||
* to falls back to a PTY terminal and the receipt says which mode ran and why — a routine launch
|
||||
* must never fail because the user happens to have a chat preference on.
|
||||
*
|
||||
* The settings default and the per-launch feasibility both come from
|
||||
* `shared/structured-native-chat-launch-route`. This module supplies placement facts and formats
|
||||
* the receipt; it does not own a second feasibility policy.
|
||||
*
|
||||
* Callers differ only in what they call the thing being started, so the receipt's noun is
|
||||
* parameterized. Orchestration says "worker" because its receipts are read alongside dispatch
|
||||
* records; every other surface says "chat session" / "terminal agent".
|
||||
*/
|
||||
|
||||
import type { GlobalSettings } from '../../shared/global-settings-types'
|
||||
import { RUNTIME_CAPABILITIES } from '../../shared/protocol-version'
|
||||
import {
|
||||
prefersStructuredNativeChatByDefault,
|
||||
resolveStructuredNativeChatSupport,
|
||||
type NativeChatDefaultSettings,
|
||||
type StructuredNativeChatBlocker
|
||||
} from '../../shared/structured-native-chat-launch-route'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { hasExplicitTuiLaunchCustomization } from '../../shared/tui-agent-launch-customization'
|
||||
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
||||
|
||||
export type AgentLaunchMode = 'structured' | 'terminal'
|
||||
|
||||
export type AgentLaunchModeReason =
|
||||
| 'user_default'
|
||||
| 'remote_execution_host'
|
||||
| 'reused_terminal'
|
||||
| 'agent_without_structured_session'
|
||||
| 'tui_launch_customization'
|
||||
| 'structured_sessions_unavailable'
|
||||
| 'structured_support_unknown'
|
||||
| 'wsl_execution_runtime'
|
||||
| 'codex_on_windows'
|
||||
| 'structured_unsupported_on_host'
|
||||
|
||||
export type AgentLaunchModeReceipt = {
|
||||
/** The mode the launch actually ran in. */
|
||||
mode: AgentLaunchMode
|
||||
/** The user's settings default for a new agent tab. */
|
||||
preferred: AgentLaunchMode
|
||||
reason: AgentLaunchModeReason
|
||||
/** One sentence, always present, so a fallback is never silent. */
|
||||
detail: string
|
||||
}
|
||||
|
||||
/** What this caller calls the thing it is starting, so one decision serves every surface without
|
||||
* a receipt reading "worker" on a phone. */
|
||||
export type AgentLaunchModeVocabulary = {
|
||||
/** e.g. 'a structured chat session worker' */
|
||||
structured: string
|
||||
/** e.g. 'a terminal agent worker' */
|
||||
terminal: string
|
||||
/** Per-reason wording a surface states differently. Orchestration names the `--terminal` flag
|
||||
* in its reused-terminal detail, which would be meaningless in a phone's receipt. */
|
||||
detailOverrides?: Partial<Record<Exclude<AgentLaunchModeReason, 'user_default'>, string>>
|
||||
}
|
||||
|
||||
export const DEFAULT_LAUNCH_VOCABULARY: AgentLaunchModeVocabulary = {
|
||||
structured: 'a structured chat session',
|
||||
terminal: 'a terminal agent'
|
||||
}
|
||||
|
||||
export type AgentLaunchModeSettings = Partial<
|
||||
NativeChatDefaultSettings &
|
||||
Pick<GlobalSettings, 'agentCmdOverrides' | 'agentDefaultArgs' | 'agentDefaultEnv'>
|
||||
>
|
||||
|
||||
/** The placement facts the decision reads. `worktree`, `model` and `effort` are deliberately not
|
||||
* here: a structured launch honours all three, and a placement flag must never imply a mode. */
|
||||
export type AgentLaunchModePlacement = {
|
||||
agent?: string
|
||||
/** A connected execution server; absent means local. */
|
||||
on?: string
|
||||
/** An existing terminal being reused. */
|
||||
terminal?: string
|
||||
}
|
||||
|
||||
const DOWNGRADE_DETAIL: Record<Exclude<AgentLaunchModeReason, 'user_default'>, string> = {
|
||||
remote_execution_host: 'this launch runs on a remote execution host',
|
||||
reused_terminal: 'it reuses a running terminal agent',
|
||||
agent_without_structured_session: 'this agent has no structured session',
|
||||
tui_launch_customization:
|
||||
'this agent has a custom launch command, arguments or environment that only a terminal applies',
|
||||
structured_sessions_unavailable: 'this runtime does not support structured agent sessions',
|
||||
structured_support_unknown: 'the execution host has not established structured session support',
|
||||
wsl_execution_runtime: 'this workspace runs under WSL',
|
||||
codex_on_windows: 'Codex has no structured session on Windows',
|
||||
structured_unsupported_on_host: 'the execution host cannot create one here'
|
||||
}
|
||||
|
||||
const BLOCKER_REASON: Record<
|
||||
StructuredNativeChatBlocker,
|
||||
Exclude<AgentLaunchModeReason, 'user_default'>
|
||||
> = {
|
||||
'reused-terminal': 'reused_terminal',
|
||||
'agent-without-structured-session': 'agent_without_structured_session',
|
||||
'draft-prompt': 'structured_unsupported_on_host',
|
||||
'floating-workspace': 'structured_unsupported_on_host',
|
||||
'tui-launch-customization': 'tui_launch_customization',
|
||||
'remote-execution-host': 'remote_execution_host',
|
||||
'project-runtime': 'wsl_execution_runtime',
|
||||
'runtime-capability': 'structured_sessions_unavailable',
|
||||
'runtime-capability-unknown': 'structured_support_unknown'
|
||||
}
|
||||
|
||||
/** The host's own create-support verdict (`agentSession.createSupport`) in this vocabulary. */
|
||||
const HOST_SUPPORT_REASON: Record<
|
||||
'agent' | 'remote' | 'wsl',
|
||||
Exclude<AgentLaunchModeReason, 'user_default'>
|
||||
> = {
|
||||
agent: 'structured_unsupported_on_host',
|
||||
remote: 'remote_execution_host',
|
||||
wsl: 'wsl_execution_runtime'
|
||||
}
|
||||
|
||||
/**
|
||||
* First half of the decision: the user's default, plus every feasibility fact knowable before a
|
||||
* workspace is resolved.
|
||||
*/
|
||||
export function decideAgentLaunchMode(args: {
|
||||
placement: AgentLaunchModePlacement
|
||||
settings: AgentLaunchModeSettings | null | undefined
|
||||
vocabulary?: AgentLaunchModeVocabulary
|
||||
}): AgentLaunchModeReceipt {
|
||||
const { placement, settings } = args
|
||||
const vocabulary = args.vocabulary ?? DEFAULT_LAUNCH_VOCABULARY
|
||||
if (!prefersStructuredNativeChatByDefault(settings)) {
|
||||
return {
|
||||
mode: 'terminal',
|
||||
preferred: 'terminal',
|
||||
reason: 'user_default',
|
||||
detail: `Started ${vocabulary.terminal}, the default for new agent tabs in your settings.`
|
||||
}
|
||||
}
|
||||
const agent = placement.agent as TuiAgent
|
||||
const support = resolveStructuredNativeChatSupport({
|
||||
agent,
|
||||
executionHostId: placement.on ? `runtime:${placement.on}` : 'local',
|
||||
reusesTerminal: Boolean(placement.terminal),
|
||||
hostCapabilities: RUNTIME_CAPABILITIES,
|
||||
// A resolved managed worktree or folder workspace is never a floating terminal. WSL is left to
|
||||
// the executing host's own create-support probe, which reads the resolved workspace rather
|
||||
// than guessing from a client-side project runtime.
|
||||
requiresTuiLaunchCustomization: hasExplicitTuiLaunchCustomization(settings, agent)
|
||||
})
|
||||
if (!support.supported) {
|
||||
return downgraded(BLOCKER_REASON[support.blocker], vocabulary)
|
||||
}
|
||||
return {
|
||||
mode: 'structured',
|
||||
preferred: 'structured',
|
||||
reason: 'user_default',
|
||||
detail: `Started ${vocabulary.structured}, the default for new agent tabs in your settings.`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Second half, once the workspace is resolved: the host that will run the agent answers whether it
|
||||
* can create a structured session there at all. Asked before anything is created, so a refusal
|
||||
* becomes a terminal agent rather than a failed launch.
|
||||
*/
|
||||
export async function resolveAgentLaunchModeOnHost(
|
||||
runtime: Pick<OrcaRuntimeService, 'getStructuredAgentSessionCreateSupport'>,
|
||||
receipt: AgentLaunchModeReceipt,
|
||||
worktreeId: string | undefined,
|
||||
agent: TuiAgent | undefined,
|
||||
vocabulary: AgentLaunchModeVocabulary = DEFAULT_LAUNCH_VOCABULARY
|
||||
): Promise<AgentLaunchModeReceipt> {
|
||||
if (receipt.mode !== 'structured' || !worktreeId) {
|
||||
return receipt
|
||||
}
|
||||
return downgradeAgentLaunchModeForHost(
|
||||
receipt,
|
||||
await readStructuredCreateSupport(runtime, worktreeId, agent),
|
||||
vocabulary
|
||||
)
|
||||
}
|
||||
|
||||
/** A host that cannot answer has not proved it can create one, so the launch stays a PTY agent. */
|
||||
async function readStructuredCreateSupport(
|
||||
runtime: Pick<OrcaRuntimeService, 'getStructuredAgentSessionCreateSupport'>,
|
||||
worktreeId: string,
|
||||
agent: TuiAgent | undefined
|
||||
): Promise<{ supported: boolean; reason?: 'agent' | 'remote' | 'wsl' } | null> {
|
||||
if (agent !== 'claude' && agent !== 'codex') {
|
||||
return { supported: false, reason: 'agent' }
|
||||
}
|
||||
try {
|
||||
return await runtime.getStructuredAgentSessionCreateSupport(`id:${worktreeId}`, agent)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the executing host's `agentSession.createSupport` answer, which is the authority on WSL,
|
||||
* remoteness and the Windows process-start-time gate for the resolved workspace.
|
||||
*/
|
||||
export function downgradeAgentLaunchModeForHost(
|
||||
receipt: AgentLaunchModeReceipt,
|
||||
support: { supported: boolean; reason?: 'agent' | 'remote' | 'wsl' } | null,
|
||||
vocabulary: AgentLaunchModeVocabulary = DEFAULT_LAUNCH_VOCABULARY
|
||||
): AgentLaunchModeReceipt {
|
||||
if (receipt.mode !== 'structured' || support?.supported) {
|
||||
return receipt
|
||||
}
|
||||
if (support === null) {
|
||||
return downgraded(BLOCKER_REASON['runtime-capability-unknown'], vocabulary)
|
||||
}
|
||||
return downgraded(
|
||||
support.reason ? HOST_SUPPORT_REASON[support.reason] : 'structured_unsupported_on_host',
|
||||
vocabulary
|
||||
)
|
||||
}
|
||||
|
||||
function downgraded(
|
||||
reason: Exclude<AgentLaunchModeReason, 'user_default'>,
|
||||
vocabulary: AgentLaunchModeVocabulary
|
||||
): AgentLaunchModeReceipt {
|
||||
const why = vocabulary.detailOverrides?.[reason] ?? DOWNGRADE_DETAIL[reason]
|
||||
return {
|
||||
mode: 'terminal',
|
||||
preferred: 'structured',
|
||||
reason,
|
||||
detail: `Your default is a structured chat session, but ${why}; started ${vocabulary.terminal} instead.`
|
||||
}
|
||||
}
|
||||
|
||||
/** The store can be missing on a runtime that never opened one; that reads as no preference. */
|
||||
export function readAgentLaunchModeSettings(
|
||||
runtime: Pick<OrcaRuntimeService, 'getClientSettings'>
|
||||
): AgentLaunchModeSettings | null {
|
||||
try {
|
||||
return runtime.getClientSettings()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,41 @@
|
||||
/**
|
||||
* Which kind of worker `orchestration.workerStart` starts, decided from the user's own settings.
|
||||
* `orchestration.workerStart`'s view of the shared launch-mode decision.
|
||||
*
|
||||
* There is no `--structured` flag: if the user's default is that a new agent tab opens as a
|
||||
* structured native chat, an orchestration worker is one too. That default is a preference, not a
|
||||
* demand, so a dispatch it cannot apply to falls back to an ordinary PTY terminal worker and the
|
||||
* receipt says which mode ran and why — a routine `worker-start` must never fail because the user
|
||||
* happens to have a chat preference on.
|
||||
*
|
||||
* The settings default and the per-launch feasibility both come from
|
||||
* `shared/structured-native-chat-launch-route`, the same module the renderer's
|
||||
* `resolveAgentLaunchRoute` uses. This adapter supplies placement facts and formats the receipt;
|
||||
* it does not own a second feasibility policy.
|
||||
* The decision itself lives in `main/agent-launch/agent-launch-mode`, which every launch surface
|
||||
* shares — a worker is not a special kind of launch, it is the same launch with a dispatch
|
||||
* attached. All this module contributes is the noun orchestration puts in its receipts ("worker")
|
||||
* and the `--terminal` wording, so a dispatch receipt reads the way it always has.
|
||||
*/
|
||||
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import { RUNTIME_CAPABILITIES } from '../../../../shared/protocol-version'
|
||||
import {
|
||||
prefersStructuredNativeChatByDefault,
|
||||
resolveStructuredNativeChatSupport,
|
||||
type NativeChatDefaultSettings,
|
||||
type StructuredNativeChatBlocker
|
||||
} from '../../../../shared/structured-native-chat-launch-route'
|
||||
decideAgentLaunchMode,
|
||||
downgradeAgentLaunchModeForHost,
|
||||
readAgentLaunchModeSettings,
|
||||
resolveAgentLaunchModeOnHost,
|
||||
type AgentLaunchMode,
|
||||
type AgentLaunchModeReason,
|
||||
type AgentLaunchModeReceipt,
|
||||
type AgentLaunchModeSettings,
|
||||
type AgentLaunchModeVocabulary
|
||||
} from '../../../agent-launch/agent-launch-mode'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { hasExplicitTuiLaunchCustomization } from '../../../../shared/tui-agent-launch-customization'
|
||||
import type { OrcaRuntimeService } from '../../orca-runtime'
|
||||
|
||||
export type WorkerStartMode = 'structured' | 'terminal'
|
||||
export type WorkerStartMode = AgentLaunchMode
|
||||
export type WorkerStartModeReason = AgentLaunchModeReason
|
||||
export type WorkerStartModeReceipt = AgentLaunchModeReceipt
|
||||
|
||||
export type WorkerStartModeReason =
|
||||
| 'user_default'
|
||||
| 'remote_execution_host'
|
||||
| 'reused_terminal'
|
||||
| 'agent_without_structured_session'
|
||||
| 'tui_launch_customization'
|
||||
| 'structured_sessions_unavailable'
|
||||
| 'structured_support_unknown'
|
||||
| 'wsl_execution_runtime'
|
||||
| 'codex_on_windows'
|
||||
| 'structured_unsupported_on_host'
|
||||
|
||||
export type WorkerStartModeReceipt = {
|
||||
/** The mode the worker actually started in. */
|
||||
mode: WorkerStartMode
|
||||
/** The user's settings default for a new agent tab. */
|
||||
preferred: WorkerStartMode
|
||||
reason: WorkerStartModeReason
|
||||
/** One sentence, always present, so a fallback is never silent. */
|
||||
detail: string
|
||||
/** Orchestration's receipts are read next to dispatch records, so they name the worker and the
|
||||
* flag that reused a terminal. Pinned here because the exact strings are asserted. */
|
||||
export const WORKER_START_VOCABULARY: AgentLaunchModeVocabulary = {
|
||||
structured: 'a structured chat session worker',
|
||||
terminal: 'a terminal agent worker',
|
||||
detailOverrides: {
|
||||
remote_execution_host: 'this worker runs on a remote execution host',
|
||||
reused_terminal: '--terminal reuses a running terminal agent'
|
||||
}
|
||||
}
|
||||
|
||||
type WorkerStartModeSettings = Partial<
|
||||
NativeChatDefaultSettings &
|
||||
Pick<GlobalSettings, 'agentCmdOverrides' | 'agentDefaultArgs' | 'agentDefaultEnv'>
|
||||
>
|
||||
|
||||
/** The placement options that exist only on `worker-start`. `worktree`, `model` and `effort` are
|
||||
* listed but no longer read: a structured worker honours all three, and naming them here keeps
|
||||
* the set of options this decision has considered visible. */
|
||||
@@ -66,153 +48,35 @@ type WorkerStartModePlacement = {
|
||||
effort?: string
|
||||
}
|
||||
|
||||
const DOWNGRADE_DETAIL: Record<Exclude<WorkerStartModeReason, 'user_default'>, string> = {
|
||||
remote_execution_host: 'this worker runs on a remote execution host',
|
||||
reused_terminal: '--terminal reuses a running terminal agent',
|
||||
agent_without_structured_session: 'this agent has no structured session',
|
||||
tui_launch_customization:
|
||||
'this agent has a custom launch command, arguments or environment that only a terminal applies',
|
||||
structured_sessions_unavailable: 'this runtime does not support structured agent sessions',
|
||||
structured_support_unknown: 'the execution host has not established structured session support',
|
||||
wsl_execution_runtime: 'this workspace runs under WSL',
|
||||
codex_on_windows: 'Codex has no structured session on Windows',
|
||||
structured_unsupported_on_host: 'the execution host cannot create one here'
|
||||
}
|
||||
|
||||
const BLOCKER_REASON: Record<
|
||||
StructuredNativeChatBlocker,
|
||||
Exclude<WorkerStartModeReason, 'user_default'>
|
||||
> = {
|
||||
'reused-terminal': 'reused_terminal',
|
||||
'agent-without-structured-session': 'agent_without_structured_session',
|
||||
'draft-prompt': 'structured_unsupported_on_host',
|
||||
'floating-workspace': 'structured_unsupported_on_host',
|
||||
'tui-launch-customization': 'tui_launch_customization',
|
||||
'remote-execution-host': 'remote_execution_host',
|
||||
'project-runtime': 'wsl_execution_runtime',
|
||||
'runtime-capability': 'structured_sessions_unavailable',
|
||||
'runtime-capability-unknown': 'structured_support_unknown'
|
||||
}
|
||||
|
||||
/** The host's own create-support verdict (`agentSession.createSupport`) in this vocabulary. */
|
||||
const HOST_SUPPORT_REASON: Record<
|
||||
'agent' | 'remote' | 'wsl',
|
||||
Exclude<WorkerStartModeReason, 'user_default'>
|
||||
> = {
|
||||
agent: 'structured_unsupported_on_host',
|
||||
remote: 'remote_execution_host',
|
||||
wsl: 'wsl_execution_runtime'
|
||||
}
|
||||
|
||||
export function decideWorkerStartMode(args: {
|
||||
params: WorkerStartModePlacement
|
||||
settings: WorkerStartModeSettings | null | undefined
|
||||
settings: AgentLaunchModeSettings | null | undefined
|
||||
}): WorkerStartModeReceipt {
|
||||
const { params, settings } = args
|
||||
if (!prefersStructuredNativeChatByDefault(settings)) {
|
||||
return {
|
||||
mode: 'terminal',
|
||||
preferred: 'terminal',
|
||||
reason: 'user_default',
|
||||
detail: 'Started a terminal agent worker, the default for new agent tabs in your settings.'
|
||||
}
|
||||
}
|
||||
const agent = params.agent as TuiAgent
|
||||
const support = resolveStructuredNativeChatSupport({
|
||||
agent,
|
||||
executionHostId: params.on ? `runtime:${params.on}` : 'local',
|
||||
reusesTerminal: Boolean(params.terminal),
|
||||
hostCapabilities: RUNTIME_CAPABILITIES,
|
||||
// Orchestration resolves a managed worktree or folder workspace; a floating terminal is never
|
||||
// a worker placement. WSL is left to the executing host's own create-support probe, which
|
||||
// reads the resolved workspace rather than guessing from a client-side project runtime.
|
||||
requiresTuiLaunchCustomization: hasExplicitTuiLaunchCustomization(settings, agent)
|
||||
return decideAgentLaunchMode({
|
||||
placement: args.params,
|
||||
settings: args.settings,
|
||||
vocabulary: WORKER_START_VOCABULARY
|
||||
})
|
||||
if (!support.supported) {
|
||||
return downgraded(BLOCKER_REASON[support.blocker])
|
||||
}
|
||||
return {
|
||||
mode: 'structured',
|
||||
preferred: 'structured',
|
||||
reason: 'user_default',
|
||||
detail:
|
||||
'Started a structured chat session worker, the default for new agent tabs in your settings.'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Second half of the decision, once the worktree is resolved: the host that will run the worker
|
||||
* answers whether it can create a structured session there at all. Asked before anything is
|
||||
* created, so a refusal becomes a terminal worker rather than a failed start.
|
||||
*/
|
||||
export async function resolveWorkerStartModeOnHost(
|
||||
runtime: Pick<OrcaRuntimeService, 'getStructuredAgentSessionCreateSupport'>,
|
||||
mode: WorkerStartModeReceipt,
|
||||
worktreeId: string | undefined,
|
||||
agent: TuiAgent | undefined
|
||||
): Promise<WorkerStartModeReceipt> {
|
||||
if (mode.mode !== 'structured' || !worktreeId) {
|
||||
return mode
|
||||
}
|
||||
return downgradeWorkerStartModeForHost(
|
||||
mode,
|
||||
await readStructuredCreateSupport(runtime, worktreeId, agent)
|
||||
)
|
||||
return resolveAgentLaunchModeOnHost(runtime, mode, worktreeId, agent, WORKER_START_VOCABULARY)
|
||||
}
|
||||
|
||||
/** A host that cannot answer has not proved it can create one, so the worker stays a PTY agent. */
|
||||
async function readStructuredCreateSupport(
|
||||
runtime: Pick<OrcaRuntimeService, 'getStructuredAgentSessionCreateSupport'>,
|
||||
worktreeId: string,
|
||||
agent: TuiAgent | undefined
|
||||
): Promise<{ supported: boolean; reason?: 'agent' | 'remote' | 'wsl' } | null> {
|
||||
if (agent !== 'claude' && agent !== 'codex') {
|
||||
return { supported: false, reason: 'agent' }
|
||||
}
|
||||
try {
|
||||
return await runtime.getStructuredAgentSessionCreateSupport(`id:${worktreeId}`, agent)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the executing host's `agentSession.createSupport` answer, which is the authority on WSL,
|
||||
* remoteness and the Windows process-start-time gate for the resolved workspace.
|
||||
*/
|
||||
export function downgradeWorkerStartModeForHost(
|
||||
receipt: WorkerStartModeReceipt,
|
||||
support: { supported: boolean; reason?: 'agent' | 'remote' | 'wsl' } | null
|
||||
): WorkerStartModeReceipt {
|
||||
if (receipt.mode !== 'structured' || support?.supported) {
|
||||
return receipt
|
||||
}
|
||||
if (support === null) {
|
||||
return downgraded(BLOCKER_REASON['runtime-capability-unknown'])
|
||||
}
|
||||
return downgraded(
|
||||
support.reason ? HOST_SUPPORT_REASON[support.reason] : 'structured_unsupported_on_host'
|
||||
)
|
||||
return downgradeAgentLaunchModeForHost(receipt, support, WORKER_START_VOCABULARY)
|
||||
}
|
||||
|
||||
function downgraded(
|
||||
reason: Exclude<WorkerStartModeReason, 'user_default'>
|
||||
): WorkerStartModeReceipt {
|
||||
return {
|
||||
mode: 'terminal',
|
||||
preferred: 'structured',
|
||||
reason,
|
||||
detail: `Your default is a structured chat session, but ${DOWNGRADE_DETAIL[reason]}; started a terminal agent worker instead.`
|
||||
}
|
||||
}
|
||||
|
||||
/** The store can be missing on a runtime that never opened one; that reads as no preference. */
|
||||
export function readWorkerStartModeSettings(
|
||||
runtime: Pick<OrcaRuntimeService, 'getClientSettings'>
|
||||
): WorkerStartModeSettings | null {
|
||||
try {
|
||||
return runtime.getClientSettings()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
): AgentLaunchModeSettings | null {
|
||||
return readAgentLaunchModeSettings(runtime)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* The exact sentences `orchestration.workerStart` puts in its mode receipt.
|
||||
*
|
||||
* These were unpinned when the decision moved to `main/agent-launch/agent-launch-mode`: the
|
||||
* existing suites assert `toContain` fragments ('terminal agent', 'cannot create'), and the CLI
|
||||
* suite asserts a receipt handed to it by a mock rather than one this code produced. Every one of
|
||||
* them stayed green against a deliberately corrupted vocabulary, so nothing was actually holding
|
||||
* the wording. A dispatch receipt is the only place a structured→terminal downgrade explains
|
||||
* itself, so the whole sentence is the contract, not a fragment of it.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
decideWorkerStartMode,
|
||||
downgradeWorkerStartModeForHost,
|
||||
type WorkerStartModeReceipt
|
||||
} from './orchestration-worker-start-mode'
|
||||
|
||||
const STRUCTURED_PREFERENCE = {
|
||||
experimentalNativeChat: true,
|
||||
experimentalStructuredNativeChat: true,
|
||||
openAgentTabsInChatByDefault: true
|
||||
} as const
|
||||
|
||||
function structuredReceipt(): WorkerStartModeReceipt {
|
||||
const receipt = decideWorkerStartMode({
|
||||
params: { agent: 'claude' },
|
||||
settings: STRUCTURED_PREFERENCE
|
||||
})
|
||||
expect(receipt.mode).toBe('structured')
|
||||
return receipt
|
||||
}
|
||||
|
||||
function downgradeSentence(why: string): string {
|
||||
return `Your default is a structured chat session, but ${why}; started a terminal agent worker instead.`
|
||||
}
|
||||
|
||||
describe('worker-start mode receipt wording', () => {
|
||||
it('states the settings default when the user has no structured preference', () => {
|
||||
expect(decideWorkerStartMode({ params: { agent: 'claude' }, settings: null })).toEqual({
|
||||
mode: 'terminal',
|
||||
preferred: 'terminal',
|
||||
reason: 'user_default',
|
||||
detail: 'Started a terminal agent worker, the default for new agent tabs in your settings.'
|
||||
})
|
||||
})
|
||||
|
||||
it('states the settings default when the launch is structured', () => {
|
||||
expect(structuredReceipt()).toEqual({
|
||||
mode: 'structured',
|
||||
preferred: 'structured',
|
||||
reason: 'user_default',
|
||||
detail:
|
||||
'Started a structured chat session worker, the default for new agent tabs in your settings.'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'remote execution host',
|
||||
{ agent: 'claude', on: 'server-1' },
|
||||
'remote_execution_host',
|
||||
'this worker runs on a remote execution host'
|
||||
],
|
||||
[
|
||||
'reused terminal',
|
||||
{ agent: 'claude', terminal: 'term_1' },
|
||||
'reused_terminal',
|
||||
'--terminal reuses a running terminal agent'
|
||||
],
|
||||
[
|
||||
'agent with no structured session',
|
||||
{ agent: 'grok' },
|
||||
'agent_without_structured_session',
|
||||
'this agent has no structured session'
|
||||
]
|
||||
])('names the %s downgrade in full', (_label, params, reason, why) => {
|
||||
expect(decideWorkerStartMode({ params, settings: STRUCTURED_PREFERENCE })).toEqual({
|
||||
mode: 'terminal',
|
||||
preferred: 'structured',
|
||||
reason,
|
||||
detail: downgradeSentence(why)
|
||||
})
|
||||
})
|
||||
|
||||
it('names a custom TUI launch as the downgrade', () => {
|
||||
expect(
|
||||
decideWorkerStartMode({
|
||||
params: { agent: 'claude' },
|
||||
settings: { ...STRUCTURED_PREFERENCE, agentDefaultArgs: { claude: '--custom' } }
|
||||
})
|
||||
).toEqual({
|
||||
mode: 'terminal',
|
||||
preferred: 'structured',
|
||||
reason: 'tui_launch_customization',
|
||||
detail: downgradeSentence(
|
||||
'this agent has a custom launch command, arguments or environment that only a terminal applies'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'an unanswered host',
|
||||
null,
|
||||
'structured_support_unknown',
|
||||
'the execution host has not established structured session support'
|
||||
],
|
||||
[
|
||||
'a host refusal with no reason',
|
||||
{ supported: false },
|
||||
'structured_unsupported_on_host',
|
||||
'the execution host cannot create one here'
|
||||
],
|
||||
[
|
||||
'a WSL workspace',
|
||||
{ supported: false, reason: 'wsl' as const },
|
||||
'wsl_execution_runtime',
|
||||
'this workspace runs under WSL'
|
||||
],
|
||||
[
|
||||
'a remote workspace',
|
||||
{ supported: false, reason: 'remote' as const },
|
||||
'remote_execution_host',
|
||||
'this worker runs on a remote execution host'
|
||||
]
|
||||
])('names %s in full', (_label, support, reason, why) => {
|
||||
expect(downgradeWorkerStartModeForHost(structuredReceipt(), support)).toEqual({
|
||||
mode: 'terminal',
|
||||
preferred: 'structured',
|
||||
reason,
|
||||
detail: downgradeSentence(why)
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves a settled terminal receipt untouched', () => {
|
||||
const terminal = decideWorkerStartMode({ params: { agent: 'claude' }, settings: null })
|
||||
expect(downgradeWorkerStartModeForHost(terminal, null)).toEqual(terminal)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user