mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* refactor(agent-launch): redefine the agent.launch contract
`agent.launch` has no clients yet, so the contract is redefined in place
rather than versioned.
- params require `operation.id`, pinned to the shipped operation-id mint so
the host can read the embedded timestamp back. No caller-supplied
fingerprint: the host derives its own.
- the result carries `disposition` ('created' | 'replayed', the same
vocabulary `RuntimeCreateAgentSessionResult` already uses) and a single
top-level `warning` instead of one on the terminal arm only.
- the prompt receipt becomes an outcome enum, so a receipt can under-claim
instead of reporting a bare `delivered: false`.
- the dead `customization` field is deleted, and the mode-reason union and
receipt are declared once in shared with main re-exporting.
- `clientMutationId` joins the reserved create fields, with a test pinning
the list to the create schema in both directions.
Contract only; no behaviour change and no ledger wiring.
* docs(agent-launch): stop calling the stripped set "agent fields"
`clientMutationId` joined AGENT_LAUNCH_RESERVED_CREATE_FIELDS, so three
comments describing the stripped set as agent fields now teach the wrong
model — including a SAFETY rationale, where a reader is trusting it most.
The rationale's claim is unchanged and still sound: deleting keys from a
parsed object leaves the rest the parsed shape.
* refactor(agent-launch): make the attempt id the launch's only idempotency key
Review follow-ups on the contract redefinition.
`operation: { id }` becomes a flat `clientOperationId`, spelled the way
`terminal.createAgentSession` and the structured mutation envelope already
spell the same concept, and admitted by the shipped
`parseAgentSessionOperationTimestamp` rather than a second copy of its
pattern — so `agent-session-host-authority` keeps the regex private.
The handler now dedupes on that id instead of the create payload's
`clientMutationId`. That field is optional, so keying on it left any launch
that omitted one with no idempotency at all, while the required attempt id
did nothing. Reserving `clientMutationId` is still right, but for the reason
the comments now give: `createManagedWorktree` never reads it, so a copy left
in the forwarded payload is inert while still reading as a guarantee. The
previous rationale — that it was a second live dedupe key — was not true.
`messageId` moves onto the prompt receipt's `journaled` arm so a producer
cannot report the text as committed without saying where, and `rpcCallerKey`
picks up the `terminal.create` call site it was lifted from instead of
shipping with no callers.
* docs(agent-launch): record why disposition is two-valued only for now
The ledger admits attempts whose outcome was never recorded, and neither
`created` nor `replayed` can say "I cannot tell you" — a caller handed
`created` for an unresolved attempt starts a second agent. Noted at the type
rather than in review, so whoever wires the ledger reads it where they edit.
* fix(agent-launch): keep contract within implemented guarantees
250 lines
9.3 KiB
TypeScript
250 lines
9.3 KiB
TypeScript
/**
|
|
* The executor's ordering contract, which is the defect this module exists to remove.
|
|
*
|
|
* The old shape created a new worktree agent-first, so its startup terminal WAS the agent and the
|
|
* structured branch below it could not be reached for any new worktree. The assertions that matter
|
|
* here are therefore about *order and arguments*, not just the returned mode: a structured launch
|
|
* must create the worktree with `startupAgent: undefined`, and it must ask the host only after the
|
|
* workspace exists.
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
AgentLaunchStructuredSessionRefusedError,
|
|
executeAgentLaunch,
|
|
type AgentLaunchExecution
|
|
} from './agent-launch-executor'
|
|
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
|
|
|
|
const STRUCTURED_PREFERENCE = {
|
|
experimentalNativeChat: true,
|
|
experimentalStructuredNativeChat: true,
|
|
openAgentTabsInChatByDefault: true
|
|
}
|
|
|
|
function harness(options: {
|
|
settings?: Record<string, unknown> | null
|
|
createSupport?: { supported: boolean; reason?: 'agent' | 'remote' | 'wsl' }
|
|
createSupportThrows?: boolean
|
|
structuredCreateError?: Error
|
|
}) {
|
|
const calls: string[] = []
|
|
const createWorktree = vi.fn(
|
|
async (args: { create: Record<string, unknown>; startupAgent: string | undefined }) => {
|
|
calls.push(`createWorktree(startupAgent=${String(args.startupAgent)})`)
|
|
return {
|
|
worktreeId: 'wt-new',
|
|
startupTerminalHandle: args.startupAgent ? 'term_agent_first' : undefined
|
|
}
|
|
}
|
|
)
|
|
const getStructuredAgentSessionCreateSupport = vi.fn(async () => {
|
|
calls.push('createSupport')
|
|
if (options.createSupportThrows) {
|
|
throw new Error('host unreachable')
|
|
}
|
|
return options.createSupport ?? { supported: true }
|
|
})
|
|
const createStructuredSession = vi.fn(async () => {
|
|
calls.push('createStructuredSession')
|
|
if (options.structuredCreateError) {
|
|
throw options.structuredCreateError
|
|
}
|
|
return { sessionId: 'sess-1', handle: 'handle_structured' }
|
|
})
|
|
const createTerminalAgent = vi.fn(async () => {
|
|
calls.push('createTerminalAgent')
|
|
return { handle: 'term_1' }
|
|
})
|
|
const runtime = {
|
|
getClientSettings: () =>
|
|
options.settings === undefined ? STRUCTURED_PREFERENCE : options.settings,
|
|
getStructuredAgentSessionCreateSupport
|
|
}
|
|
return {
|
|
calls,
|
|
createWorktree,
|
|
createStructuredSession,
|
|
createTerminalAgent,
|
|
run: (intent: AgentLaunchIntent) =>
|
|
executeAgentLaunch({
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements only the two runtime methods the executor reaches, and each test asserts the calls made, so an omitted method throws rather than reading a wrong value.
|
|
runtime: runtime as unknown as AgentLaunchExecution['runtime'],
|
|
intent,
|
|
surfaces: { createStructuredSession, createTerminalAgent },
|
|
workspaces: { createWorktree }
|
|
})
|
|
}
|
|
}
|
|
|
|
const CREATE_INTENT: AgentLaunchIntent = {
|
|
agent: 'claude',
|
|
target: { kind: 'create-worktree', create: { repo: 'id:repo-1', name: 'task' } }
|
|
}
|
|
|
|
describe('a structured launch that creates its own worktree', () => {
|
|
it('creates the worktree with no startup agent, then asks the host, then opens a session', async () => {
|
|
const h = harness({})
|
|
const result = await h.run(CREATE_INTENT)
|
|
|
|
// The whole defect in one assertion: the worktree must not be created agent-first.
|
|
expect(h.calls).toEqual([
|
|
'createWorktree(startupAgent=undefined)',
|
|
'createSupport',
|
|
'createStructuredSession'
|
|
])
|
|
expect(result.outcome).toEqual({
|
|
kind: 'structured',
|
|
sessionId: 'sess-1',
|
|
handle: 'handle_structured'
|
|
})
|
|
expect(result.worktreeId).toBe('wt-new')
|
|
expect(result.receipt.mode).toBe('structured')
|
|
})
|
|
|
|
it('asks the host only after the workspace exists, never before', async () => {
|
|
const h = harness({})
|
|
await h.run(CREATE_INTENT)
|
|
expect(h.calls.indexOf('createSupport')).toBeGreaterThan(
|
|
h.calls.indexOf('createWorktree(startupAgent=undefined)')
|
|
)
|
|
})
|
|
|
|
it('falls back to a terminal in the worktree it just created when the host refuses', async () => {
|
|
const h = harness({ createSupport: { supported: false, reason: 'wsl' } })
|
|
const result = await h.run(CREATE_INTENT)
|
|
|
|
expect(h.calls).toEqual([
|
|
'createWorktree(startupAgent=undefined)',
|
|
'createSupport',
|
|
'createTerminalAgent'
|
|
])
|
|
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_1' })
|
|
// Not a failed launch, and the workspace is the one just created.
|
|
expect(result.worktreeId).toBe('wt-new')
|
|
expect(result.receipt).toMatchObject({ mode: 'terminal', reason: 'wsl_execution_runtime' })
|
|
})
|
|
|
|
it('falls back to a terminal when the host cannot be reached at all', async () => {
|
|
const h = harness({ createSupportThrows: true })
|
|
const result = await h.run(CREATE_INTENT)
|
|
expect(result.outcome.kind).toBe('terminal')
|
|
expect(result.receipt).toMatchObject({ reason: 'structured_support_unknown' })
|
|
})
|
|
|
|
it('falls back only for a definitive structured refusal after the worktree exists', async () => {
|
|
const h = harness({
|
|
structuredCreateError: new AgentLaunchStructuredSessionRefusedError(
|
|
'structured_agent_session_unsupported',
|
|
'unsupported'
|
|
)
|
|
})
|
|
const result = await h.run(CREATE_INTENT)
|
|
|
|
expect(h.calls).toEqual([
|
|
'createWorktree(startupAgent=undefined)',
|
|
'createSupport',
|
|
'createStructuredSession',
|
|
'createTerminalAgent'
|
|
])
|
|
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_1' })
|
|
expect(result.receipt).toMatchObject({
|
|
mode: 'terminal',
|
|
reason: 'structured_unsupported_on_host'
|
|
})
|
|
})
|
|
|
|
it('does not create a duplicate terminal when structured creation is unknown', async () => {
|
|
const h = harness({
|
|
structuredCreateError: new AgentLaunchStructuredSessionRefusedError(
|
|
'agent_session_operation_unknown',
|
|
'unknown'
|
|
)
|
|
})
|
|
|
|
await expect(h.run(CREATE_INTENT)).rejects.toThrow('unknown')
|
|
expect(h.calls).toEqual([
|
|
'createWorktree(startupAgent=undefined)',
|
|
'createSupport',
|
|
'createStructuredSession'
|
|
])
|
|
})
|
|
|
|
it('strips a stale startupAgent out of a migrated create payload', async () => {
|
|
const h = harness({})
|
|
await h.run({
|
|
agent: 'claude',
|
|
target: {
|
|
kind: 'create-worktree',
|
|
// Exactly what mobile sends `worktree.create` today.
|
|
create: { repo: 'id:repo-1', name: 'task', startupAgent: 'claude', startupDraft: 'url' }
|
|
}
|
|
})
|
|
const passed = h.createWorktree.mock.calls[0]?.[0]
|
|
expect(passed?.create).not.toHaveProperty('startupAgent')
|
|
expect(passed?.create).not.toHaveProperty('startupDraft')
|
|
expect(passed?.create).toMatchObject({ repo: 'id:repo-1', name: 'task' })
|
|
})
|
|
})
|
|
|
|
describe('a launch the user did not ask to be structured', () => {
|
|
it('creates the worktree agent-first and never asks the host', async () => {
|
|
const h = harness({ settings: null })
|
|
const result = await h.run(CREATE_INTENT)
|
|
|
|
// Agent-first is preserved for PTY launches: it is what sequences the agent's startup command
|
|
// behind the setup runner, so the wait-for-setup gate comes for free.
|
|
expect(h.calls).toEqual(['createWorktree(startupAgent=claude)'])
|
|
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_agent_first' })
|
|
expect(result.receipt).toMatchObject({ mode: 'terminal', reason: 'user_default' })
|
|
})
|
|
})
|
|
|
|
describe('a launch into a workspace that already exists', () => {
|
|
it('opens a session without creating anything', async () => {
|
|
const h = harness({})
|
|
const result = await h.run({ agent: 'codex', target: { kind: 'existing', worktree: 'wt-7' } })
|
|
expect(h.calls).toEqual(['createSupport', 'createStructuredSession'])
|
|
expect(h.createWorktree).not.toHaveBeenCalled()
|
|
expect(result.worktreeId).toBe('wt-7')
|
|
})
|
|
|
|
it('reuses a running terminal without creating or asking', async () => {
|
|
const h = harness({})
|
|
const result = await h.run({
|
|
agent: 'claude',
|
|
target: { kind: 'existing', worktree: 'wt-7' },
|
|
reuseTerminal: { handle: 'term_live' }
|
|
})
|
|
expect(h.calls).toEqual([])
|
|
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_live' })
|
|
expect(result.receipt).toMatchObject({ mode: 'terminal', reason: 'reused_terminal' })
|
|
})
|
|
})
|
|
|
|
describe('an agent with no structured session', () => {
|
|
it('stays a terminal without asking the host', async () => {
|
|
const h = harness({})
|
|
const result = await h.run({ agent: 'grok', target: { kind: 'existing', worktree: 'wt-7' } })
|
|
expect(h.calls).toEqual(['createTerminalAgent'])
|
|
expect(result.receipt).toMatchObject({ reason: 'agent_without_structured_session' })
|
|
})
|
|
})
|
|
|
|
describe('the prompt receipt', () => {
|
|
it('reports a requested prompt as not delivered rather than omitting it', async () => {
|
|
const h = harness({})
|
|
const result = await h.run({
|
|
...CREATE_INTENT,
|
|
prompt: { text: 'do the thing', delivery: 'draft' }
|
|
})
|
|
// The executor delivers nothing, so the only honest outcome is the one that under-claims.
|
|
expect(result.prompt).toEqual({ delivery: 'draft', outcome: 'not-delivered' })
|
|
})
|
|
|
|
it('omits the receipt when no prompt was requested', async () => {
|
|
const h = harness({})
|
|
expect((await h.run(CREATE_INTENT)).prompt).toBeUndefined()
|
|
})
|
|
})
|