From df9e4c5665d885b50219686d203671241d51bac3 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 07:25:19 -0400 Subject: [PATCH 01/15] feat(sdk): expose provider spawn facade methods --- .../2026-05/traj_bvo77swtj1br/summary.md | 31 +++ .../2026-05/traj_bvo77swtj1br/trajectory.json | 53 +++++ CHANGELOG.md | 1 + packages/sdk/README.md | 8 + .../__tests__/orchestration-upgrades.test.ts | 105 ++++++++- .../sdk/src/__tests__/spawn-token.test.ts | 20 +- packages/sdk/src/relay.ts | 212 ++++++++++++------ packages/sdk/src/types.ts | 11 +- 8 files changed, 369 insertions(+), 72 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md new file mode 100644 index 000000000..6243d162a --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md @@ -0,0 +1,31 @@ +# Trajectory: Surface AgentRelay provider and headless spawns + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 27, 2026 at 07:24 AM +> **Completed:** May 27, 2026 at 07:24 AM + +--- + +## Summary + +Added high-level AgentRelay.spawnProvider and AgentRelay.spawnHeadless facade methods, widened SpawnHeadlessInput for harness-backed provider metadata, documented the API, updated the changelog, and verified with SDK typecheck, build, formatting, and focused Vitest coverage. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Expose typed AgentRelay spawnProvider and spawnHeadless methods +- **Chose:** Expose typed AgentRelay spawnProvider and spawnHeadless methods +- **Reasoning:** Issue 998 needs provider-backed and headless app-server agents to use the high-level facade lifecycle hooks, result contracts, channel handles, and harness resolution instead of dropping to AgentRelayClient. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Expose typed AgentRelay spawnProvider and spawnHeadless methods: Expose typed AgentRelay spawnProvider and spawnHeadless methods diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json new file mode 100644 index 000000000..46dec93cf --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json @@ -0,0 +1,53 @@ +{ + "id": "traj_bvo77swtj1br", + "version": 1, + "task": { + "title": "Surface AgentRelay provider and headless spawns" + }, + "status": "completed", + "startedAt": "2026-05-27T11:24:25.352Z", + "completedAt": "2026-05-27T11:24:34.780Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-27T11:24:30.208Z" + } + ], + "chapters": [ + { + "id": "chap_fscokieqprhr", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-27T11:24:30.208Z", + "endedAt": "2026-05-27T11:24:34.780Z", + "events": [ + { + "ts": 1779881070209, + "type": "decision", + "content": "Expose typed AgentRelay spawnProvider and spawnHeadless methods: Expose typed AgentRelay spawnProvider and spawnHeadless methods", + "raw": { + "question": "Expose typed AgentRelay spawnProvider and spawnHeadless methods", + "chosen": "Expose typed AgentRelay spawnProvider and spawnHeadless methods", + "alternatives": [], + "reasoning": "Issue 998 needs provider-backed and headless app-server agents to use the high-level facade lifecycle hooks, result contracts, channel handles, and harness resolution instead of dropping to AgentRelayClient." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Added high-level AgentRelay.spawnProvider and AgentRelay.spawnHeadless facade methods, widened SpawnHeadlessInput for harness-backed provider metadata, documented the API, updated the changelog, and verified with SDK typecheck, build, formatting, and focused Vitest coverage.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "f904124865b9575c1def48d20e333298cc03a1f7", + "endRef": "f904124865b9575c1def48d20e333298cc03a1f7" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e5d0e6f..e54ef49f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker and TypeScript SDK structured result contracts add the `submit_result` MCP tool, `agent.waitForResult()`, per-spawn `result.onResult`, and `relay.addListener('agentResult', ...)` for typed JSON worker outcomes. - `@agent-relay/sdk` and `agent-relay-broker` add broker-executable `pty` and `headless` harness configs, so custom CLIs can be configured without Rust changes while spawn requests remain self-contained. - `agent-relay-broker` accepts resolved harness configs on spawn and adds a headless app-server driver for delivering Relay messages to existing OpenCode server sessions. +- `@agent-relay/sdk` exposes `AgentRelay.spawnProvider()` and `AgentRelay.spawnHeadless()` so high-level facade callers can launch provider-backed and headless harness agents with the same lifecycle hooks and handles as `spawnPty()`. - `@agent-relay/sdk` adds `AgentRelay.getPersonaSpawnPlan(id)` and a `getPersonaSpawnPlan` export for dry-run inspection of a persona's resolved harness argv, skill installs, mount policy, sidecars, and inputs. ### Changed diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 92ee91b6c..2b82c3ed1 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -58,6 +58,14 @@ const agent = await relay.spawn('Worker2', 'codex', 'Build the API', { model: 'gpt-4o', }); +// Provider-backed and headless harnesses keep the same Agent handle surface +const reviewer = await relay.spawnHeadless({ + name: 'HeadlessReviewer', + provider: 'opencode', + channels: ['reviews'], + task: 'Review the current branch', +}); + // Wait for agent to finish (go idle or exit) const result = await agent.waitForIdle(120_000); diff --git a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts index 02a524691..611836393 100644 --- a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +++ b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts @@ -24,7 +24,11 @@ function createMockFacadeClient() { const mock = { spawnPty: vi.fn(async (input: { name: string }) => ({ name: input.name, runtime: 'pty' as const })), - spawnProvider: vi.fn(async (input: { name: string }) => ({ + spawnProvider: vi.fn(async (input: { name: string; transport?: 'pty' | 'headless' }) => ({ + name: input.name, + runtime: input.transport === 'pty' ? ('pty' as const) : ('headless' as const), + })), + spawnHeadless: vi.fn(async (input: { name: string }) => ({ name: input.name, runtime: 'headless' as const, })), @@ -544,6 +548,105 @@ describe('AgentRelay orchestration handles', () => { } }); + it('spawnProvider exposes provider-backed spawns through the facade', async () => { + const { client, mock } = createMockFacadeClient(); + const relay = createWiredRelay(client); + + try { + const agent = await relay.spawnProvider({ + name: 'provider-facade', + provider: 'claude', + transport: 'pty', + channels: ['ops'], + cwd: '/workspace/provider', + continueFrom: 'session-123', + agentToken: 'agent-token-provider', + }); + + expect(agent).toMatchObject({ + name: 'provider-facade', + runtime: 'pty', + channels: ['ops'], + }); + expect(mock.spawnProvider).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'provider-facade', + provider: 'claude', + transport: 'pty', + channels: ['ops'], + cwd: '/workspace/provider', + continueFrom: 'session-123', + agentToken: 'agent-token-provider', + }) + ); + } finally { + await relay.shutdown(); + } + }); + + it('spawnHeadless preserves facade lifecycle hooks and custom headless harness config', async () => { + const { client, mock } = createMockFacadeClient(); + const relay = createWiredRelay(client); + const onStart = vi.fn(); + const onSuccess = vi.fn(); + const harnessConfig = { + runtime: 'headless' as const, + protocol: 'custom-app', + endpoint: 'http://127.0.0.1:4099', + sessionId: 'session-headless', + }; + const jsonSchema = { type: 'object', properties: { ok: { type: 'boolean' } } }; + + try { + const agent = await relay.spawnHeadless<{ ok: boolean }>({ + name: 'headless-facade', + provider: 'custom-app', + channels: ['reviews'], + task: 'Review the change', + harnessConfig, + result: { jsonSchema }, + onStart, + onSuccess, + }); + + expect(agent).toMatchObject({ + name: 'headless-facade', + runtime: 'headless', + channels: ['reviews'], + }); + expect(onStart).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'headless-facade', + cli: 'custom-app', + provider: 'custom-app', + channels: ['reviews'], + task: 'Review the change', + }) + ); + expect(onSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'headless-facade', + cli: 'custom-app', + provider: 'custom-app', + runtime: 'headless', + }) + ); + expect(mock.spawnHeadless).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'headless-facade', + provider: 'custom-app', + transport: 'headless', + channels: ['reviews'], + task: 'Review the change', + harnessConfig, + agentResultSchema: jsonSchema, + }) + ); + } finally { + await relay.shutdown(); + } + }); + it('agent.waitForReady resolves after worker_ready event', async () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); diff --git a/packages/sdk/src/__tests__/spawn-token.test.ts b/packages/sdk/src/__tests__/spawn-token.test.ts index 0e28703f3..0b2f7b365 100644 --- a/packages/sdk/src/__tests__/spawn-token.test.ts +++ b/packages/sdk/src/__tests__/spawn-token.test.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest'; -import type { SpawnProviderInput, SpawnPtyInput } from '../types.js'; +import type { SpawnHeadlessInput, SpawnProviderInput, SpawnPtyInput } from '../types.js'; describe('spawn input agentToken types', () => { it('SpawnPtyInput accepts agentToken when present or omitted', () => { @@ -38,4 +38,22 @@ describe('spawn input agentToken types', () => { expectTypeOf(withToken.agentToken).toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); + + it('SpawnHeadlessInput accepts provider harness metadata and agentToken', () => { + const withHeadlessHarness = { + name: 'headless-with-harness', + provider: 'custom-app-server', + agentToken: 'jwt-token', + harnessConfig: { + runtime: 'headless', + protocol: 'custom-app-server', + endpoint: 'http://127.0.0.1:4099', + sessionId: 'session-headless', + }, + } satisfies SpawnHeadlessInput; + + expectTypeOf(withHeadlessHarness).toMatchTypeOf(); + expectTypeOf(withHeadlessHarness.provider).toMatchTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index da3d1c0e6..2d35faee8 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -41,12 +41,19 @@ import { } from './harness.js'; import type { AgentRelayEvents, BeforeAgentSpawnHandler } from './lifecycle-hooks.js'; import { AgentRelayProtocolError } from './transport.js'; -import type { JsonSchema, SendMessageInput, SpawnAgentResult, SpawnPtyInput } from './types.js'; +import type { + AgentTransport, + JsonSchema, + SendMessageInput, + SpawnAgentResult, + SpawnHeadlessInput, + SpawnProviderInput, + SpawnPtyInput, +} from './types.js'; import type { AgentRuntime, BrokerEvent, BrokerStatus, - HeadlessProvider, MessageInjectionMode, RestartPolicy, } from './protocol.js'; @@ -192,6 +199,9 @@ export interface AgentResultOptions { onResult?: (data: T, meta: AgentResultMeta) => void | Promise; } +type SpawnWithLifecycle = TInput & + SpawnLifecycleHooks & { result?: AgentResultOptions }; + export type AgentStatus = 'spawning' | 'ready' | 'idle' | 'exited'; export type DeliveryWaitStatus = 'ack' | 'failed' | 'timeout'; export type DeliveryStateStatus = 'queued' | 'injected' | 'active' | 'verified' | 'failed'; @@ -230,7 +240,10 @@ export interface AgentActivityChange { export interface SpawnLifecycleContext { name: string; + /** CLI or provider identifier used to launch the agent. */ cli: string; + /** Provider identifier for provider-backed spawns. */ + provider?: string; channels: string[]; task?: string; } @@ -701,7 +714,7 @@ export class AgentRelay { this.harnesses[key] = definition; } - private findHarnessDefinition(cli: string): HarnessDefinition | undefined { + findHarnessDefinition(cli: string): HarnessDefinition | undefined { for (const key of harnessLookupKeys(cli)) { const definition = this.harnesses[key]; if (definition) return definition; @@ -782,7 +795,7 @@ export class AgentRelay { // ── Spawning ──────────────────────────────────────────────────────────── async spawnPty( - input: SpawnPtyInput & SpawnLifecycleHooks & { result?: AgentResultOptions } + input: SpawnWithLifecycle ): Promise> { const client = await this.ensureStarted(); if (!input.channels || input.channels.length === 0) { @@ -818,11 +831,12 @@ export class AgentRelay { agentToken: input.agentToken, shadowOf: input.shadowOf, shadowMode: input.shadowMode, + continueFrom: input.continueFrom, idleThresholdSecs: input.idleThresholdSecs, restartPolicy: input.restartPolicy, harnessConfig, skipRelayPrompt: input.skipRelayPrompt, - agentResultSchema: resultContract?.jsonSchema, + agentResultSchema: resultContract?.jsonSchema ?? input.agentResultSchema, }); } catch (error) { if (resultContract) { @@ -862,6 +876,24 @@ export class AgentRelay { return agent; } + async spawnProvider( + input: SpawnWithLifecycle + ): Promise> { + return this.spawnProviderWithLifecycle('spawnProvider', input, (client, request) => + client.spawnProvider(request) + ); + } + + async spawnHeadless( + input: SpawnWithLifecycle + ): Promise> { + return this.spawnProviderWithLifecycle( + 'spawnHeadless', + { ...input, transport: 'headless' }, + (client, request) => client.spawnHeadless(request) + ); + } + async spawn( name: string, cli: string, @@ -905,6 +937,101 @@ export class AgentRelay { return this.waitForAgentReady(name, timeoutMs ?? 60_000) as Promise>; } + private async spawnProviderWithLifecycle( + methodName: 'spawn' | 'spawnProvider' | 'spawnHeadless', + input: SpawnWithLifecycle, + invoke: (client: AgentRelayClient, request: SpawnProviderInput) => Promise, + defaultTransport?: AgentTransport + ): Promise> { + const client = await this.ensureStarted(); + if (!input.channels || input.channels.length === 0) { + console.warn( + `[AgentRelay] ${methodName}("${input.name}"): no channels specified, defaulting to "general". ` + + 'Set explicit channels for workflow isolation.' + ); + } + const channels = input.channels ?? ['general']; + const lifecycleContext: SpawnLifecycleContext = { + name: input.name, + cli: input.provider, + provider: input.provider, + channels, + task: input.task, + }; + await this.invokeLifecycleHook(input.onStart, lifecycleContext, `${methodName}("${input.name}") onStart`); + let result: SpawnAgentResult; + const resultContract = this.prepareAgentResultContract(input.result); + if (resultContract) { + this.resultContracts.set(input.name, resultContract as InternalAgentResultContract); + } + try { + const harnessConfig = this.resolveHarnessConfig({ + name: input.name, + cli: input.provider, + args: input.args, + task: input.task, + model: input.model, + cwd: input.cwd, + harnessConfig: input.harnessConfig, + }); + result = await invoke(client, { + name: input.name, + provider: input.provider, + transport: input.transport ?? harnessConfig?.runtime ?? defaultTransport, + args: input.args, + channels, + task: input.task, + model: input.model, + cwd: input.cwd, + team: input.team, + agentToken: input.agentToken, + shadowOf: input.shadowOf, + shadowMode: input.shadowMode, + idleThresholdSecs: input.idleThresholdSecs, + restartPolicy: input.restartPolicy, + continueFrom: input.continueFrom, + harnessConfig, + skipRelayPrompt: input.skipRelayPrompt, + agentResultSchema: resultContract?.jsonSchema ?? input.agentResultSchema, + }); + } catch (error) { + if (resultContract) { + this.resultContracts.delete(input.name); + } + await this.invokeLifecycleHook( + input.onError, + { + ...lifecycleContext, + error, + }, + `${methodName}("${input.name}") onError` + ); + throw error; + } + this.resetAgentLifecycleState(result.name); + if (result.name !== input.name && resultContract) { + this.resultContracts.delete(input.name); + this.resultContracts.set(result.name, resultContract as InternalAgentResultContract); + } + const agent = this.ensureAgentHandle( + result.name, + result.runtime, + channels, + result + ) as Agent; + await this.invokeLifecycleHook( + input.onSuccess, + { + ...lifecycleContext, + name: result.name, + runtime: result.runtime, + sessionId: result.sessionId, + }, + `${methodName}("${input.name}") onSuccess` + ); + return agent; + } + // ── Human source ──────────────────────────────────────────────────────── human(opts: { name: string }): HumanHandle { @@ -2223,81 +2350,28 @@ export class AgentRelay { }); } - const client = await this.ensureStarted(); - const lifecycleContext: SpawnLifecycleContext = { - name, - cli, - channels, - task, - }; - await this.invokeLifecycleHook(options?.onStart, lifecycleContext, `spawn("${name}") onStart`); - let result: SpawnAgentResult; - const resultContract = this.prepareAgentResultContract(options?.result); - if (resultContract) { - this.resultContracts.set(name, resultContract as InternalAgentResultContract); - } - try { - const harnessConfig = this.resolveHarnessConfig({ - name, - cli, - args, - task, - model: options?.model, - cwd: options?.cwd, - harnessConfig: options?.harnessConfig, - }); - result = await client.spawnProvider({ + return this.spawnProviderWithLifecycle( + 'spawn', + { name, - provider: cli as HeadlessProvider, - transport: harnessConfig?.runtime ?? 'headless', + provider: cli, args, channels, task, model: options?.model, cwd: options?.cwd, + harnessConfig: options?.harnessConfig, idleThresholdSecs: options?.idleThresholdSecs, - harnessConfig, agentToken: options?.agentToken, skipRelayPrompt: options?.skipRelayPrompt, - agentResultSchema: resultContract?.jsonSchema, - }); - } catch (error) { - if (resultContract) { - this.resultContracts.delete(name); - } - await this.invokeLifecycleHook( - options?.onError, - { - ...lifecycleContext, - error, - }, - `spawn("${name}") onError` - ); - throw error; - } - - this.resetAgentLifecycleState(result.name); - if (result.name !== name && resultContract) { - this.resultContracts.delete(name); - this.resultContracts.set(result.name, resultContract as InternalAgentResultContract); - } - const agent = this.ensureAgentHandle( - result.name, - result.runtime, - channels, - result - ) as Agent; - await this.invokeLifecycleHook( - options?.onSuccess, - { - ...lifecycleContext, - name: result.name, - runtime: result.runtime, - sessionId: result.sessionId, + result: options?.result, + onStart: options?.onStart, + onSuccess: options?.onSuccess, + onError: options?.onError, }, - `spawn("${name}") onSuccess` + (client, request) => client.spawnProvider(request), + 'headless' ); - return agent; }, }; } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 37c4fc451..5e4bbf1cd 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -42,10 +42,19 @@ export interface SpawnPtyInput { export interface SpawnHeadlessInput { name: string; - provider: HeadlessProvider; + provider: string; args?: string[]; channels?: string[]; task?: string; + model?: string; + cwd?: string; + team?: string; + shadowOf?: string; + shadowMode?: string; + idleThresholdSecs?: number; + restartPolicy?: RestartPolicy; + continueFrom?: string; + harnessConfig?: ResolvedHarnessConfig; skipRelayPrompt?: boolean; agentResultSchema?: JsonSchema; /** Optional pre-minted relaycast agent token (`at_live_`, from From 61d1bbfb5914f9c15ff0fd0b23039c5f825c734c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 11:27:18 +0000 Subject: [PATCH 02/15] style: auto-format with Prettier --- .../completed/2026-05/traj_bvo77swtj1br/summary.md | 4 +++- .../completed/2026-05/traj_bvo77swtj1br/trajectory.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md index 6243d162a..5ac14b2a3 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md +++ b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md @@ -18,6 +18,7 @@ Added high-level AgentRelay.spawnProvider and AgentRelay.spawnHeadless facade me ## Key Decisions ### Expose typed AgentRelay spawnProvider and spawnHeadless methods + - **Chose:** Expose typed AgentRelay spawnProvider and spawnHeadless methods - **Reasoning:** Issue 998 needs provider-backed and headless app-server agents to use the high-level facade lifecycle hooks, result contracts, channel handles, and harness resolution instead of dropping to AgentRelayClient. @@ -26,6 +27,7 @@ Added high-level AgentRelay.spawnProvider and AgentRelay.spawnHeadless facade me ## Chapters ### 1. Work -*Agent: default* + +_Agent: default_ - Expose typed AgentRelay spawnProvider and spawnHeadless methods: Expose typed AgentRelay spawnProvider and spawnHeadless methods diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json index 46dec93cf..fcf35cbb2 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json @@ -50,4 +50,4 @@ "startRef": "f904124865b9575c1def48d20e333298cc03a1f7", "endRef": "f904124865b9575c1def48d20e333298cc03a1f7" } -} \ No newline at end of file +} From f54552b5ac225df762bacbbad08cb66ef46ae5f7 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 08:36:38 -0400 Subject: [PATCH 03/15] refactor(sdk): make headless facade cli-based --- .../2026-05/traj_33ykjz5a7avh/summary.md | 31 +++++++ .../2026-05/traj_33ykjz5a7avh/trajectory.json | 53 ++++++++++++ CHANGELOG.md | 2 +- packages/sdk/README.md | 4 +- .../__tests__/orchestration-upgrades.test.ts | 48 +---------- .../sdk/src/__tests__/spawn-token.test.ts | 20 ++++- packages/sdk/src/relay.ts | 85 +++++++------------ 7 files changed, 142 insertions(+), 101 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md new file mode 100644 index 000000000..1fc9872f8 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md @@ -0,0 +1,31 @@ +# Trajectory: Revise AgentRelay headless facade naming + +> **Status:** ✅ Completed +> **Confidence:** 92% +> **Started:** May 27, 2026 at 08:32 AM +> **Completed:** May 27, 2026 at 08:36 AM + +--- + +## Summary + +Revised the PR API to avoid exposing provider terminology at the AgentRelay facade: removed public AgentRelay.spawnProvider, changed spawnHeadless to accept cli, routed headless property spawners through the same helper, and updated docs/tests/changelog. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider +- **Chose:** Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider +- **Reasoning:** The high-level facade should present runtime choice as spawnPty versus spawnHeadless. Provider is a lower-level client implementation detail and reads poorly at the recipe layer where callers already resolve a CLI/harness plan. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider: Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json new file mode 100644 index 000000000..665363cc9 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json @@ -0,0 +1,53 @@ +{ + "id": "traj_33ykjz5a7avh", + "version": 1, + "task": { + "title": "Revise AgentRelay headless facade naming" + }, + "status": "completed", + "startedAt": "2026-05-27T12:32:23.768Z", + "completedAt": "2026-05-27T12:36:20.398Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-27T12:32:28.346Z" + } + ], + "chapters": [ + { + "id": "chap_d3cnk7kkc43m", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-27T12:32:28.346Z", + "endedAt": "2026-05-27T12:36:20.398Z", + "events": [ + { + "ts": 1779885148347, + "type": "decision", + "content": "Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider: Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider", + "raw": { + "question": "Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider", + "chosen": "Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider", + "alternatives": [], + "reasoning": "The high-level facade should present runtime choice as spawnPty versus spawnHeadless. Provider is a lower-level client implementation detail and reads poorly at the recipe layer where callers already resolve a CLI/harness plan." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Revised the PR API to avoid exposing provider terminology at the AgentRelay facade: removed public AgentRelay.spawnProvider, changed spawnHeadless to accept cli, routed headless property spawners through the same helper, and updated docs/tests/changelog.", + "approach": "Standard approach", + "confidence": 0.92 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "df9e4c5665d885b50219686d203671241d51bac3", + "endRef": "df9e4c5665d885b50219686d203671241d51bac3" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e54ef49f0..8a6e5483d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker and TypeScript SDK structured result contracts add the `submit_result` MCP tool, `agent.waitForResult()`, per-spawn `result.onResult`, and `relay.addListener('agentResult', ...)` for typed JSON worker outcomes. - `@agent-relay/sdk` and `agent-relay-broker` add broker-executable `pty` and `headless` harness configs, so custom CLIs can be configured without Rust changes while spawn requests remain self-contained. - `agent-relay-broker` accepts resolved harness configs on spawn and adds a headless app-server driver for delivering Relay messages to existing OpenCode server sessions. -- `@agent-relay/sdk` exposes `AgentRelay.spawnProvider()` and `AgentRelay.spawnHeadless()` so high-level facade callers can launch provider-backed and headless harness agents with the same lifecycle hooks and handles as `spawnPty()`. +- `@agent-relay/sdk` exposes `AgentRelay.spawnHeadless({ cli, ... })` so high-level facade callers can launch headless harness agents with the same lifecycle hooks and handles as `spawnPty()`. - `@agent-relay/sdk` adds `AgentRelay.getPersonaSpawnPlan(id)` and a `getPersonaSpawnPlan` export for dry-run inspection of a persona's resolved harness argv, skill installs, mount policy, sidecars, and inputs. ### Changed diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 2b82c3ed1..b2768a8f8 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -58,10 +58,10 @@ const agent = await relay.spawn('Worker2', 'codex', 'Build the API', { model: 'gpt-4o', }); -// Provider-backed and headless harnesses keep the same Agent handle surface +// Headless harnesses keep the same Agent handle surface const reviewer = await relay.spawnHeadless({ name: 'HeadlessReviewer', - provider: 'opencode', + cli: 'opencode', channels: ['reviews'], task: 'Review the current branch', }); diff --git a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts index 611836393..e7be4b2a7 100644 --- a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +++ b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts @@ -24,9 +24,9 @@ function createMockFacadeClient() { const mock = { spawnPty: vi.fn(async (input: { name: string }) => ({ name: input.name, runtime: 'pty' as const })), - spawnProvider: vi.fn(async (input: { name: string; transport?: 'pty' | 'headless' }) => ({ + spawnProvider: vi.fn(async (input: { name: string }) => ({ name: input.name, - runtime: input.transport === 'pty' ? ('pty' as const) : ('headless' as const), + runtime: 'headless' as const, })), spawnHeadless: vi.fn(async (input: { name: string }) => ({ name: input.name, @@ -535,11 +535,10 @@ describe('AgentRelay orchestration handles', () => { agentToken: 'agent-token-codex', }) ); - expect(mock.spawnProvider).toHaveBeenCalledWith( + expect(mock.spawnHeadless).toHaveBeenCalledWith( expect.objectContaining({ name: 'opencode-token', provider: 'opencode', - transport: 'headless', agentToken: 'agent-token-opencode', }) ); @@ -548,42 +547,6 @@ describe('AgentRelay orchestration handles', () => { } }); - it('spawnProvider exposes provider-backed spawns through the facade', async () => { - const { client, mock } = createMockFacadeClient(); - const relay = createWiredRelay(client); - - try { - const agent = await relay.spawnProvider({ - name: 'provider-facade', - provider: 'claude', - transport: 'pty', - channels: ['ops'], - cwd: '/workspace/provider', - continueFrom: 'session-123', - agentToken: 'agent-token-provider', - }); - - expect(agent).toMatchObject({ - name: 'provider-facade', - runtime: 'pty', - channels: ['ops'], - }); - expect(mock.spawnProvider).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'provider-facade', - provider: 'claude', - transport: 'pty', - channels: ['ops'], - cwd: '/workspace/provider', - continueFrom: 'session-123', - agentToken: 'agent-token-provider', - }) - ); - } finally { - await relay.shutdown(); - } - }); - it('spawnHeadless preserves facade lifecycle hooks and custom headless harness config', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); @@ -600,7 +563,7 @@ describe('AgentRelay orchestration handles', () => { try { const agent = await relay.spawnHeadless<{ ok: boolean }>({ name: 'headless-facade', - provider: 'custom-app', + cli: 'custom-app', channels: ['reviews'], task: 'Review the change', harnessConfig, @@ -618,7 +581,6 @@ describe('AgentRelay orchestration handles', () => { expect.objectContaining({ name: 'headless-facade', cli: 'custom-app', - provider: 'custom-app', channels: ['reviews'], task: 'Review the change', }) @@ -627,7 +589,6 @@ describe('AgentRelay orchestration handles', () => { expect.objectContaining({ name: 'headless-facade', cli: 'custom-app', - provider: 'custom-app', runtime: 'headless', }) ); @@ -635,7 +596,6 @@ describe('AgentRelay orchestration handles', () => { expect.objectContaining({ name: 'headless-facade', provider: 'custom-app', - transport: 'headless', channels: ['reviews'], task: 'Review the change', harnessConfig, diff --git a/packages/sdk/src/__tests__/spawn-token.test.ts b/packages/sdk/src/__tests__/spawn-token.test.ts index 0b2f7b365..81c00d6e8 100644 --- a/packages/sdk/src/__tests__/spawn-token.test.ts +++ b/packages/sdk/src/__tests__/spawn-token.test.ts @@ -1,5 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest'; +import type { SpawnHeadlessAgentInput } from '../relay.js'; import type { SpawnHeadlessInput, SpawnProviderInput, SpawnPtyInput } from '../types.js'; describe('spawn input agentToken types', () => { @@ -39,7 +40,7 @@ describe('spawn input agentToken types', () => { expectTypeOf().toEqualTypeOf(); }); - it('SpawnHeadlessInput accepts provider harness metadata and agentToken', () => { + it('SpawnHeadlessInput accepts custom harness metadata and agentToken', () => { const withHeadlessHarness = { name: 'headless-with-harness', provider: 'custom-app-server', @@ -56,4 +57,21 @@ describe('spawn input agentToken types', () => { expectTypeOf(withHeadlessHarness.provider).toMatchTypeOf(); expectTypeOf().toEqualTypeOf(); }); + + it('SpawnHeadlessAgentInput uses cli for the high-level facade', () => { + const withHeadlessHarness = { + name: 'headless-facade', + cli: 'custom-app-server', + agentToken: 'jwt-token', + harnessConfig: { + runtime: 'headless', + protocol: 'custom-app-server', + endpoint: 'http://127.0.0.1:4099', + sessionId: 'session-headless', + }, + } satisfies SpawnHeadlessAgentInput; + + expectTypeOf(withHeadlessHarness).toMatchTypeOf(); + expectTypeOf(withHeadlessHarness.cli).toMatchTypeOf(); + }); }); diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index 2d35faee8..56d321b10 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -42,12 +42,10 @@ import { import type { AgentRelayEvents, BeforeAgentSpawnHandler } from './lifecycle-hooks.js'; import { AgentRelayProtocolError } from './transport.js'; import type { - AgentTransport, JsonSchema, SendMessageInput, SpawnAgentResult, - SpawnHeadlessInput, - SpawnProviderInput, + SpawnHeadlessInput as ClientSpawnHeadlessInput, SpawnPtyInput, } from './types.js'; import type { @@ -202,6 +200,10 @@ export interface AgentResultOptions { type SpawnWithLifecycle = TInput & SpawnLifecycleHooks & { result?: AgentResultOptions }; +export type SpawnHeadlessAgentInput = Omit & { + cli: string; +} & SpawnLifecycleHooks & { result?: AgentResultOptions }; + export type AgentStatus = 'spawning' | 'ready' | 'idle' | 'exited'; export type DeliveryWaitStatus = 'ack' | 'failed' | 'timeout'; export type DeliveryStateStatus = 'queued' | 'injected' | 'active' | 'verified' | 'failed'; @@ -240,10 +242,8 @@ export interface AgentActivityChange { export interface SpawnLifecycleContext { name: string; - /** CLI or provider identifier used to launch the agent. */ + /** CLI identifier used to launch the agent. */ cli: string; - /** Provider identifier for provider-backed spawns. */ - provider?: string; channels: string[]; task?: string; } @@ -876,22 +876,10 @@ export class AgentRelay { return agent; } - async spawnProvider( - input: SpawnWithLifecycle - ): Promise> { - return this.spawnProviderWithLifecycle('spawnProvider', input, (client, request) => - client.spawnProvider(request) - ); - } - async spawnHeadless( - input: SpawnWithLifecycle + input: SpawnHeadlessAgentInput ): Promise> { - return this.spawnProviderWithLifecycle( - 'spawnHeadless', - { ...input, transport: 'headless' }, - (client, request) => client.spawnHeadless(request) - ); + return this.spawnHeadlessWithLifecycle('spawnHeadless', input); } async spawn( @@ -937,11 +925,9 @@ export class AgentRelay { return this.waitForAgentReady(name, timeoutMs ?? 60_000) as Promise>; } - private async spawnProviderWithLifecycle( - methodName: 'spawn' | 'spawnProvider' | 'spawnHeadless', - input: SpawnWithLifecycle, - invoke: (client: AgentRelayClient, request: SpawnProviderInput) => Promise, - defaultTransport?: AgentTransport + private async spawnHeadlessWithLifecycle( + methodName: 'spawn' | 'spawnHeadless', + input: SpawnHeadlessAgentInput ): Promise> { const client = await this.ensureStarted(); if (!input.channels || input.channels.length === 0) { @@ -953,8 +939,7 @@ export class AgentRelay { const channels = input.channels ?? ['general']; const lifecycleContext: SpawnLifecycleContext = { name: input.name, - cli: input.provider, - provider: input.provider, + cli: input.cli, channels, task: input.task, }; @@ -967,17 +952,16 @@ export class AgentRelay { try { const harnessConfig = this.resolveHarnessConfig({ name: input.name, - cli: input.provider, + cli: input.cli, args: input.args, task: input.task, model: input.model, cwd: input.cwd, harnessConfig: input.harnessConfig, }); - result = await invoke(client, { + result = await client.spawnHeadless({ name: input.name, - provider: input.provider, - transport: input.transport ?? harnessConfig?.runtime ?? defaultTransport, + provider: input.cli, args: input.args, channels, task: input.task, @@ -2350,28 +2334,23 @@ export class AgentRelay { }); } - return this.spawnProviderWithLifecycle( - 'spawn', - { - name, - provider: cli, - args, - channels, - task, - model: options?.model, - cwd: options?.cwd, - harnessConfig: options?.harnessConfig, - idleThresholdSecs: options?.idleThresholdSecs, - agentToken: options?.agentToken, - skipRelayPrompt: options?.skipRelayPrompt, - result: options?.result, - onStart: options?.onStart, - onSuccess: options?.onSuccess, - onError: options?.onError, - }, - (client, request) => client.spawnProvider(request), - 'headless' - ); + return this.spawnHeadlessWithLifecycle('spawn', { + name, + cli, + args, + channels, + task, + model: options?.model, + cwd: options?.cwd, + harnessConfig: options?.harnessConfig, + idleThresholdSecs: options?.idleThresholdSecs, + agentToken: options?.agentToken, + skipRelayPrompt: options?.skipRelayPrompt, + result: options?.result, + onStart: options?.onStart, + onSuccess: options?.onSuccess, + onError: options?.onError, + }); }, }; } From 910adcfbbd3bc01d7afa6b4984e7b141560d9f87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 12:38:21 +0000 Subject: [PATCH 04/15] style: auto-format with Prettier --- .../completed/2026-05/traj_33ykjz5a7avh/summary.md | 4 +++- .../completed/2026-05/traj_33ykjz5a7avh/trajectory.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md index 1fc9872f8..e33267802 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md +++ b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md @@ -18,6 +18,7 @@ Revised the PR API to avoid exposing provider terminology at the AgentRelay faca ## Key Decisions ### Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider + - **Chose:** Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider - **Reasoning:** The high-level facade should present runtime choice as spawnPty versus spawnHeadless. Provider is a lower-level client implementation detail and reads poorly at the recipe layer where callers already resolve a CLI/harness plan. @@ -26,6 +27,7 @@ Revised the PR API to avoid exposing provider terminology at the AgentRelay faca ## Chapters ### 1. Work -*Agent: default* + +_Agent: default_ - Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider: Use cli-based AgentRelay.spawnHeadless instead of public spawnProvider diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json index 665363cc9..95530dee3 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json @@ -50,4 +50,4 @@ "startRef": "df9e4c5665d885b50219686d203671241d51bac3", "endRef": "df9e4c5665d885b50219686d203671241d51bac3" } -} \ No newline at end of file +} From 900b8528d6cbcfa148af53fad904c426afa3e412 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 08:57:22 -0400 Subject: [PATCH 05/15] refactor(sdk): rename spawn provider api to cli --- .../2026-05/traj_ei1zajpyq584/summary.md | 31 ++++++++++ .../2026-05/traj_ei1zajpyq584/trajectory.json | 53 ++++++++++++++++ CHANGELOG.md | 4 +- packages/gateway/src/types.ts | 4 +- packages/sdk/README.md | 8 +-- .../sdk/src/__tests__/integration.test.ts | 2 +- .../sdk/src/__tests__/lifecycle-hooks.test.ts | 16 ++--- .../__tests__/orchestration-upgrades.test.ts | 10 ++-- .../sdk/src/__tests__/spawn-token.test.ts | 26 ++++---- packages/sdk/src/client.ts | 60 +++++++++++-------- packages/sdk/src/lifecycle-hooks.ts | 14 ++--- packages/sdk/src/relay.ts | 7 +-- packages/sdk/src/types.ts | 6 +- 13 files changed, 169 insertions(+), 72 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md new file mode 100644 index 000000000..e0a8e026b --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md @@ -0,0 +1,31 @@ +# Trajectory: Rename SDK spawn provider terminology + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 27, 2026 at 08:49 AM +> **Completed:** May 27, 2026 at 08:57 AM + +--- + +## Summary + +Renamed the SDK spawn API from provider terminology to CLI terminology for the major release: AgentRelayClient.spawnProvider -> spawnCli, SpawnProviderInput -> SpawnCliInput, SpawnHeadlessInput.provider -> cli, lifecycle kind provider -> cli/headless, with docs, changelog migration notes, gateway type update, and tests. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Rename SDK provider spawn vocabulary to cli/headless +- **Chose:** Rename SDK provider spawn vocabulary to cli/headless +- **Reasoning:** The broker payload already uses cli, and provider is stale terminology now that harness configs represent the execution harness. Because this is a major release, the SDK can remove the legacy SpawnProviderInput/spawnProvider surface instead of layering aliases. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Rename SDK provider spawn vocabulary to cli/headless: Rename SDK provider spawn vocabulary to cli/headless diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json new file mode 100644 index 000000000..6a0998278 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json @@ -0,0 +1,53 @@ +{ + "id": "traj_ei1zajpyq584", + "version": 1, + "task": { + "title": "Rename SDK spawn provider terminology" + }, + "status": "completed", + "startedAt": "2026-05-27T12:49:51.440Z", + "completedAt": "2026-05-27T12:57:01.785Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-27T12:49:55.778Z" + } + ], + "chapters": [ + { + "id": "chap_6kshcl1sbi15", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-27T12:49:55.778Z", + "endedAt": "2026-05-27T12:57:01.785Z", + "events": [ + { + "ts": 1779886195779, + "type": "decision", + "content": "Rename SDK provider spawn vocabulary to cli/headless: Rename SDK provider spawn vocabulary to cli/headless", + "raw": { + "question": "Rename SDK provider spawn vocabulary to cli/headless", + "chosen": "Rename SDK provider spawn vocabulary to cli/headless", + "alternatives": [], + "reasoning": "The broker payload already uses cli, and provider is stale terminology now that harness configs represent the execution harness. Because this is a major release, the SDK can remove the legacy SpawnProviderInput/spawnProvider surface instead of layering aliases." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Renamed the SDK spawn API from provider terminology to CLI terminology for the major release: AgentRelayClient.spawnProvider -> spawnCli, SpawnProviderInput -> SpawnCliInput, SpawnHeadlessInput.provider -> cli, lifecycle kind provider -> cli/headless, with docs, changelog migration notes, gateway type update, and tests.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "f54552b5ac225df762bacbbad08cb66ef46ae5f7", + "endRef": "f54552b5ac225df762bacbbad08cb66ef46ae5f7" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6e5483d..f729cc7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `@agent-relay/sdk` swaps `@agentworkforce/harness-kit` + `@agentworkforce/workload-router` for `@agentworkforce/persona-kit@^3`. The persona tier system, the `tier` option on `spawnPersona`, the legacy relay-side `PersonaFile` / `PersonaTier` / `PersonaTierSpec` / `ResolvedPersona` / `PersonaSpawnSpec` / `MaterializedConfigFile` types, and the `buildPersonaSpawnSpec` / `materializePersonaConfigFiles` / `restorePersonaConfigFiles` helpers are removed. `loadPersona` now returns the canonical `PersonaSpec`, and `spawnPersona({ persona })` takes a `PersonaSpec` instead of a resolved persona. - `@agent-relay/sdk` removes persona support from the SDK surface: the `./personas` subpath, persona helper/type exports, `AgentRelay.spawnPersona()`, `AgentRelay.getPersonaSpawnPlan()`, and `AgentRelayOptions.personaDirs` are gone. The SDK no longer depends on `@agentworkforce/persona-kit`. +- `@agent-relay/sdk` renames the raw client spawn surface from provider terminology to CLI terminology: `AgentRelayClient.spawnProvider()` is now `spawnCli()`, `SpawnProviderInput` is now `SpawnCliInput`, and `SpawnHeadlessInput.provider` is now `SpawnHeadlessInput.cli`. - `agent-relay-broker`'s public Rust protocol types now require typed ID newtypes (`WorkerName`, `DeliveryId`, `EventId`, `WorkspaceId`, `WorkspaceAlias`, `ThreadId`, `AgentId`, `RequestId`, `ChannelName`, `MessageTarget`) on every protocol struct and enum variant in `protocol.rs`, `types.rs`, and `listen_api.rs::ListenApiRequest`. The new wrappers live in `crates/broker/src/lib.rs` under `pub mod ids`. JSON wire format is unchanged because every wrapper is `#[serde(transparent)]`, so the broker ↔ SDK channel and on-disk persisted state remain byte-compatible. - `agent-relay spawn` and SDK spawn calls now return harness `sessionId` metadata for resumable Claude and Codex PTY sessions. - `sdk-swift`: renamed the broker client class `RelayCast` → `AgentRelayClient`. @@ -38,7 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Personas relying on `tiers.*` need to be flattened to a single top-level `harness` / `model` / `systemPrompt`. The shape that persona-kit (and the `agentworkforce` CLI) consumes is now the only supported shape. - Callers that previously used `spawnPersona` to "just launch the harness" — without persona-kit's skill / mount / sidecar side effects — should use `AgentRelay.getPersonaSpawnPlan(id)` to inspect the plan and call `spawnPty` with the plan's `cli` + `args` themselves. -- Launch personas through the owning CLI or package and pass the resulting command to `relay.spawnPty(...)` or the SDK's headless provider APIs; for AgentWorkforce personas, use `npx agentworkforce persona run ` once available so persona side effects remain CLI-owned. +- Launch personas through the owning CLI or package and pass the resulting command to `relay.spawnPty(...)` or the SDK's headless CLI APIs; for AgentWorkforce personas, use `npx agentworkforce persona run ` once available so persona side effects remain CLI-owned. +- Replace `client.spawnProvider({ provider, ... })` with `client.spawnCli({ cli, ... })`; replace `client.spawnHeadless({ provider, ... })` with `client.spawnHeadless({ cli, ... })`. - Downstream Rust callers must construct identifiers via `relay_broker::ids::{WorkerName, DeliveryId, EventId, MessageTarget, …}` instead of `String`. Each newtype impls `From` / `From<&str>` and `Deref`, so most string-handling code keeps compiling; only construction sites (`HashMap` keys, struct literals, channel sends) need updates. - Replace ad-hoc target discrimination (`target.starts_with('#')`, `target == "thread"`) with `MessageTarget::kind()` and match on `MessageTargetKind::{Channel, Thread, DirectMessage, Conversation, Worker}`. - `sdk-swift`: replace `RelayCast(apiKey:baseURL:)` with `AgentRelayClient(apiKey:baseURL:)`. The public API surface is otherwise unchanged. diff --git a/packages/gateway/src/types.ts b/packages/gateway/src/types.ts index 86578fba0..cfb3304e8 100644 --- a/packages/gateway/src/types.ts +++ b/packages/gateway/src/types.ts @@ -1,4 +1,4 @@ -import type { SendMessageInput, SpawnProviderInput } from '@agent-relay/sdk'; +import type { SendMessageInput, SpawnCliInput } from '@agent-relay/sdk'; export type SurfaceType = 'whatsapp' | 'slack' | 'telegram'; @@ -99,7 +99,7 @@ interface GatewayActionBase { export interface SpawnAgentAction extends GatewayActionBase { type: 'spawn_agent'; - agent: SpawnProviderInput; + agent: SpawnCliInput; prompt?: string; } diff --git a/packages/sdk/README.md b/packages/sdk/README.md index b2768a8f8..00c6b5403 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -136,9 +136,9 @@ await codex.send('Review the current branch and report risks.'); The Codex adapter uses `codex app-server` over stdio JSON-RPC instead of the foreground PTY path. That gives background workers structured `thread/*`, `turn/*`, and `item/*` events for steering and completion; it does not render the Codex TUI. On startup it ensures the `relaycast` MCP server is present in Codex config so the agent can use Relaycast tools in addition to injected inbox messages. -### Provider + Transport Spawning (Opencode/Claude) +### CLI + Transport Spawning (Opencode/Claude) -Use provider-first spawn helpers and set `transport` when you want headless mode. +Use CLI-first spawn helpers and set `transport` when you want headless mode. ```ts import { AgentRelayClient } from '@agent-relay/sdk'; @@ -168,10 +168,10 @@ await client.shutdown(); Notes: -- Transport is a setting (`'pty'` or `'headless'`) on provider spawn methods. +- Transport is a setting (`'pty'` or `'headless'`) on CLI spawn methods. - `spawnClaude(...)` defaults to PTY unless you pass `transport: 'headless'`. - `spawnOpencode(...)` defaults to headless. -- You can also use `client.spawnProvider({ provider, transport, ... })` for generic provider-driven spawning. +- You can also use `client.spawnCli({ cli, transport, ... })` for generic CLI-driven spawning. ## Features diff --git a/packages/sdk/src/__tests__/integration.test.ts b/packages/sdk/src/__tests__/integration.test.ts index 568af1d57..065ddb960 100644 --- a/packages/sdk/src/__tests__/integration.test.ts +++ b/packages/sdk/src/__tests__/integration.test.ts @@ -102,7 +102,7 @@ test('sdk can start broker and manage agent lifecycle', async (t) => { } }); -test('sdk can spawn and release provider worker with transport override', async (t) => { +test('sdk can spawn and release CLI worker with transport override', async (t) => { const binaryPath = resolveBinaryPath(); if (!fs.existsSync(binaryPath)) { t.skip(`agent-relay-broker binary not found at ${binaryPath}`); diff --git a/packages/sdk/src/__tests__/lifecycle-hooks.test.ts b/packages/sdk/src/__tests__/lifecycle-hooks.test.ts index 7aaf4447a..ada189f41 100644 --- a/packages/sdk/src/__tests__/lifecycle-hooks.test.ts +++ b/packages/sdk/src/__tests__/lifecycle-hooks.test.ts @@ -210,7 +210,7 @@ describe('AgentRelayClient lifecycle hooks', () => { expect(captured!.kind).toBe('pty'); }); - it('spawnProvider fires the hooks with kind=provider', async () => { + it('spawnCli fires the hooks with kind=cli', async () => { const { fetchFn } = makeMockFetch(); const client = makeClient(fetchFn); const before = vi.fn(); @@ -218,15 +218,15 @@ describe('AgentRelayClient lifecycle hooks', () => { client.addListener('beforeAgentSpawn', before); client.addListener('afterAgentSpawn', after); - await client.spawnProvider({ name: 'p', provider: 'claude' }); + await client.spawnCli({ name: 'p', cli: 'claude' }); expect(before).toHaveBeenCalledTimes(1); - expect((before.mock.calls[0][0] as BeforeAgentSpawnContext).kind).toBe('provider'); + expect((before.mock.calls[0][0] as BeforeAgentSpawnContext).kind).toBe('cli'); expect(after).toHaveBeenCalledTimes(1); - expect((after.mock.calls[0][0] as AfterAgentSpawnContext).kind).toBe('provider'); + expect((after.mock.calls[0][0] as AfterAgentSpawnContext).kind).toBe('cli'); }); - it('recomputes provider transport after beforeAgentSpawn patches add a harness config', async () => { + it('recomputes cli transport after beforeAgentSpawn patches add a harness config', async () => { const { fetchFn, captures } = makeMockFetch(); const client = makeClient(fetchFn); @@ -240,14 +240,14 @@ describe('AgentRelayClient lifecycle hooks', () => { }, })); - await client.spawnProvider({ + await client.spawnCli({ name: 'patched-headless', - provider: 'custom-provider', + cli: 'custom-cli', }); expect(captures[0].body).toMatchObject({ name: 'patched-headless', - cli: 'custom-provider', + cli: 'custom-cli', transport: 'headless', harnessConfig: { runtime: 'headless', diff --git a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts index e7be4b2a7..5668aada4 100644 --- a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +++ b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts @@ -24,7 +24,7 @@ function createMockFacadeClient() { const mock = { spawnPty: vi.fn(async (input: { name: string }) => ({ name: input.name, runtime: 'pty' as const })), - spawnProvider: vi.fn(async (input: { name: string }) => ({ + spawnCli: vi.fn(async (input: { name: string }) => ({ name: input.name, runtime: 'headless' as const, })), @@ -198,7 +198,7 @@ describe('AgentRelayClient orchestration payloads', () => { }); }); - it('spawnHeadless forwards agentToken to headless provider spawns', async () => { + it('spawnHeadless forwards agentToken to headless cli spawns', async () => { const client = createProtocolClient(); const request = vi .spyOn((client as any).transport, 'request') @@ -206,7 +206,7 @@ describe('AgentRelayClient orchestration payloads', () => { await client.spawnHeadless({ name: 'agent-headless-token', - provider: 'opencode', + cli: 'opencode', channels: ['general'], task: 'run headless', agentToken: 'agent-token-headless', @@ -538,7 +538,7 @@ describe('AgentRelay orchestration handles', () => { expect(mock.spawnHeadless).toHaveBeenCalledWith( expect.objectContaining({ name: 'opencode-token', - provider: 'opencode', + cli: 'opencode', agentToken: 'agent-token-opencode', }) ); @@ -595,7 +595,7 @@ describe('AgentRelay orchestration handles', () => { expect(mock.spawnHeadless).toHaveBeenCalledWith( expect.objectContaining({ name: 'headless-facade', - provider: 'custom-app', + cli: 'custom-app', channels: ['reviews'], task: 'Review the change', harnessConfig, diff --git a/packages/sdk/src/__tests__/spawn-token.test.ts b/packages/sdk/src/__tests__/spawn-token.test.ts index 81c00d6e8..18ff5ddcd 100644 --- a/packages/sdk/src/__tests__/spawn-token.test.ts +++ b/packages/sdk/src/__tests__/spawn-token.test.ts @@ -1,7 +1,7 @@ import { describe, expectTypeOf, it } from 'vitest'; import type { SpawnHeadlessAgentInput } from '../relay.js'; -import type { SpawnHeadlessInput, SpawnProviderInput, SpawnPtyInput } from '../types.js'; +import type { SpawnCliInput, SpawnHeadlessInput, SpawnPtyInput } from '../types.js'; describe('spawn input agentToken types', () => { it('SpawnPtyInput accepts agentToken when present or omitted', () => { @@ -22,28 +22,28 @@ describe('spawn input agentToken types', () => { expectTypeOf().toEqualTypeOf(); }); - it('SpawnProviderInput accepts agentToken when present or omitted', () => { + it('SpawnCliInput accepts agentToken when present or omitted', () => { const withoutToken = { - name: 'provider-no-token', - provider: 'claude', - } satisfies SpawnProviderInput; + name: 'cli-no-token', + cli: 'claude', + } satisfies SpawnCliInput; const withToken = { - name: 'provider-with-token', - provider: 'claude', + name: 'cli-with-token', + cli: 'claude', agentToken: 'jwt-token', - } satisfies SpawnProviderInput; + } satisfies SpawnCliInput; - expectTypeOf(withoutToken).toMatchTypeOf(); - expectTypeOf(withToken).toMatchTypeOf(); + expectTypeOf(withoutToken).toMatchTypeOf(); + expectTypeOf(withToken).toMatchTypeOf(); expectTypeOf(withToken.agentToken).toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); }); it('SpawnHeadlessInput accepts custom harness metadata and agentToken', () => { const withHeadlessHarness = { name: 'headless-with-harness', - provider: 'custom-app-server', + cli: 'custom-app-server', agentToken: 'jwt-token', harnessConfig: { runtime: 'headless', @@ -54,7 +54,7 @@ describe('spawn input agentToken types', () => { } satisfies SpawnHeadlessInput; expectTypeOf(withHeadlessHarness).toMatchTypeOf(); - expectTypeOf(withHeadlessHarness.provider).toMatchTypeOf(); + expectTypeOf(withHeadlessHarness.cli).toMatchTypeOf(); expectTypeOf().toEqualTypeOf(); }); diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index a1fea3766..fb093faa4 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -36,9 +36,9 @@ import type { import type { AgentTransport, SpawnAgentResult, + SpawnCliInput, SpawnHeadlessInput, SpawnPtyInput, - SpawnProviderInput, SendMessageInput, ListAgent, } from './types.js'; @@ -164,14 +164,14 @@ interface BrokerStartupDebugContext { type BrokerExitListener = (info: BrokerExitInfo) => void; -function isHeadlessProvider(value: string): value is HeadlessProvider { +function isBundledHeadlessCli(value: string): value is HeadlessProvider { return value === 'claude' || value === 'opencode'; } -function resolveSpawnTransport(input: SpawnProviderInput): AgentTransport { +function resolveSpawnTransport(input: SpawnCliInput): AgentTransport { if (input.transport) return input.transport; if (input.harnessConfig) return input.harnessConfig.runtime; - return input.provider === 'opencode' ? 'headless' : 'pty'; + return input.cli === 'opencode' ? 'headless' : 'pty'; } /** @@ -201,13 +201,10 @@ function buildSpawnPtyBody(input: SpawnPtyInput): Record { }; } -function buildSpawnProviderBody( - input: SpawnProviderInput, - transport: AgentTransport -): Record { +function buildSpawnCliBody(input: SpawnCliInput, transport: AgentTransport): Record { return { name: input.name, - cli: input.provider, + cli: input.cli, ...(input.model !== undefined ? { model: input.model } : {}), args: input.args ?? [], ...(input.task !== undefined ? { task: input.task } : {}), @@ -337,13 +334,13 @@ export class AgentRelayClient { * shallow-merged over the running result. Handler exceptions are caught * and logged but do not abort the chain. */ - private async runBeforeSpawn(ctx: BeforeAgentSpawnContext): Promise { - let resolved: SpawnPtyInput | SpawnProviderInput = { ...ctx.input }; + private async runBeforeSpawn(ctx: BeforeAgentSpawnContext): Promise { + let resolved: SpawnPtyInput | SpawnCliInput = { ...ctx.input }; for (const handler of this.eventBus.listeners('beforeAgentSpawn') as Array) { try { const patch = await handler({ ...ctx, input: resolved as Readonly }); if (patch && typeof patch === 'object') { - resolved = { ...resolved, ...(patch as SpawnPatch) } as SpawnPtyInput | SpawnProviderInput; + resolved = { ...resolved, ...(patch as SpawnPatch) } as SpawnPtyInput | SpawnCliInput; } } catch (err) { console.error('[agent-relay] beforeAgentSpawn listener threw:', err); @@ -629,31 +626,38 @@ export class AgentRelayClient { } } - async spawnProvider(input: SpawnProviderInput): Promise { + async spawnCli(input: SpawnCliInput): Promise { const beforeCtx: BeforeAgentSpawnContext = { - kind: 'provider', + kind: 'cli', input, spawnerPid: process.pid, spawnStartTs: new Date().toISOString(), baseUrl: this.baseUrl, }; + return this.spawnCliWithContext(beforeCtx, input); + } + + private async spawnCliWithContext( + beforeCtx: BeforeAgentSpawnContext, + input: SpawnCliInput + ): Promise { const t0 = Date.now(); - const resolvedInput = (await this.runBeforeSpawn(beforeCtx)) as SpawnProviderInput; + const resolvedInput = (await this.runBeforeSpawn(beforeCtx)) as SpawnCliInput; const transport = resolveSpawnTransport(resolvedInput); if ( transport === 'headless' && - !isHeadlessProvider(resolvedInput.provider) && + !isBundledHeadlessCli(resolvedInput.cli) && !resolvedInput.harnessConfig ) { throw new Error( - `provider '${resolvedInput.provider}' does not support headless transport (supported: claude, opencode)` + `cli '${resolvedInput.cli}' does not support headless transport (supported: claude, opencode)` ); } try { const rawResult = await this.transport.request('/api/spawn', { method: 'POST', - body: JSON.stringify(buildSpawnProviderBody(resolvedInput, transport)), + body: JSON.stringify(buildSpawnCliBody(resolvedInput, transport)), }); const result = SpawnAgentResultSchema.parse(rawResult); await this.emitAfterSpawn(beforeCtx, resolvedInput, t0, result, undefined); @@ -665,15 +669,23 @@ export class AgentRelayClient { } async spawnHeadless(input: SpawnHeadlessInput): Promise { - return this.spawnProvider({ ...input, transport: 'headless' }); + const cliInput: SpawnCliInput = { ...input, transport: 'headless' }; + const beforeCtx: BeforeAgentSpawnContext = { + kind: 'headless', + input: cliInput, + spawnerPid: process.pid, + spawnStartTs: new Date().toISOString(), + baseUrl: this.baseUrl, + }; + return this.spawnCliWithContext(beforeCtx, cliInput); } - async spawnClaude(input: Omit): Promise { - return this.spawnProvider({ ...input, provider: 'claude' }); + async spawnClaude(input: Omit): Promise { + return this.spawnCli({ ...input, cli: 'claude' }); } - async spawnOpencode(input: Omit): Promise { - return this.spawnProvider({ ...input, provider: 'opencode' }); + async spawnOpencode(input: Omit): Promise { + return this.spawnCli({ ...input, cli: 'opencode' }); } async release(name: string, reason?: string): Promise<{ name: string }> { @@ -707,7 +719,7 @@ export class AgentRelayClient { private async emitAfterSpawn( beforeCtx: BeforeAgentSpawnContext, - resolvedInput: SpawnPtyInput | SpawnProviderInput, + resolvedInput: SpawnPtyInput | SpawnCliInput, startMs: number, result: SpawnAgentResult | undefined, error: unknown diff --git a/packages/sdk/src/lifecycle-hooks.ts b/packages/sdk/src/lifecycle-hooks.ts index b21e4fabc..5af0dd965 100644 --- a/packages/sdk/src/lifecycle-hooks.ts +++ b/packages/sdk/src/lifecycle-hooks.ts @@ -28,15 +28,15 @@ import type { BrokerEvent } from './protocol.js'; import type { Agent, AgentActivityChange, AgentResult, Message } from './relay.js'; -import type { SpawnAgentResult, SpawnPtyInput, SpawnProviderInput } from './types.js'; +import type { SpawnAgentResult, SpawnCliInput, SpawnPtyInput } from './types.js'; // ── SpawnPatch ───────────────────────────────────────────────────────────── /** - * The subset of {@link SpawnPtyInput} / {@link SpawnProviderInput} fields a + * The subset of {@link SpawnPtyInput} / {@link SpawnCliInput} fields a * `beforeAgentSpawn` handler may patch. Keeping this narrower than the full * input type stops handlers from rewriting identity (`name`, `cli`, - * `provider`, `cwd`) — those need to come from the caller. + * `cwd`) — those need to come from the caller. * * For array fields (`args`, `channels`) a patch *replaces* the array. To * extend rather than replace, spread the current value: @@ -53,7 +53,7 @@ import type { SpawnAgentResult, SpawnPtyInput, SpawnProviderInput } from './type */ export type SpawnPatch = Partial< Pick< - SpawnPtyInput & SpawnProviderInput, + SpawnPtyInput & SpawnCliInput, 'args' | 'channels' | 'task' | 'model' | 'team' | 'agentToken' | 'harnessConfig' > >; @@ -62,9 +62,9 @@ export type SpawnPatch = Partial< export interface BeforeAgentSpawnContext { /** Which spawn API was called. */ - kind: 'pty' | 'provider'; + kind: 'pty' | 'cli' | 'headless'; /** Raw input the caller passed in. Treat as read-only — return a {@link SpawnPatch} to modify. */ - input: Readonly; + input: Readonly; /** `process.pid` of the calling Node process. Useful for burn-style stamping. */ spawnerPid: number; /** ISO timestamp captured the instant the hook chain started. */ @@ -79,7 +79,7 @@ export type BeforeAgentSpawnHandler = ( export interface AfterAgentSpawnContext extends BeforeAgentSpawnContext { /** Final input that was sent to the broker — original input merged with every handler's patch. */ - resolvedInput: SpawnPtyInput | SpawnProviderInput; + resolvedInput: SpawnPtyInput | SpawnCliInput; /** Broker reply on success. */ result?: SpawnAgentResult; /** Set when the broker call rejected. Mutually exclusive with `result`. */ diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index 56d321b10..d0695c73b 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -200,9 +200,8 @@ export interface AgentResultOptions { type SpawnWithLifecycle = TInput & SpawnLifecycleHooks & { result?: AgentResultOptions }; -export type SpawnHeadlessAgentInput = Omit & { - cli: string; -} & SpawnLifecycleHooks & { result?: AgentResultOptions }; +export type SpawnHeadlessAgentInput = ClientSpawnHeadlessInput & + SpawnLifecycleHooks & { result?: AgentResultOptions }; export type AgentStatus = 'spawning' | 'ready' | 'idle' | 'exited'; export type DeliveryWaitStatus = 'ack' | 'failed' | 'timeout'; @@ -961,7 +960,7 @@ export class AgentRelay { }); result = await client.spawnHeadless({ name: input.name, - provider: input.cli, + cli: input.cli, args: input.args, channels, task: input.task, diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 5e4bbf1cd..a189521de 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -42,7 +42,7 @@ export interface SpawnPtyInput { export interface SpawnHeadlessInput { name: string; - provider: string; + cli: string; args?: string[]; channels?: string[]; task?: string; @@ -76,9 +76,9 @@ export interface SpawnAgentResult { pid?: number; } -export interface SpawnProviderInput { +export interface SpawnCliInput { name: string; - provider: string; + cli: string; transport?: AgentTransport; args?: string[]; channels?: string[]; From d1aa908c0fa2319c8e0ff22faf716718e425015e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 12:59:37 +0000 Subject: [PATCH 06/15] style: auto-format with Prettier --- .../completed/2026-05/traj_ei1zajpyq584/summary.md | 4 +++- .../completed/2026-05/traj_ei1zajpyq584/trajectory.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md index e0a8e026b..a02946b05 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md @@ -18,6 +18,7 @@ Renamed the SDK spawn API from provider terminology to CLI terminology for the m ## Key Decisions ### Rename SDK provider spawn vocabulary to cli/headless + - **Chose:** Rename SDK provider spawn vocabulary to cli/headless - **Reasoning:** The broker payload already uses cli, and provider is stale terminology now that harness configs represent the execution harness. Because this is a major release, the SDK can remove the legacy SpawnProviderInput/spawnProvider surface instead of layering aliases. @@ -26,6 +27,7 @@ Renamed the SDK spawn API from provider terminology to CLI terminology for the m ## Chapters ### 1. Work -*Agent: default* + +_Agent: default_ - Rename SDK provider spawn vocabulary to cli/headless: Rename SDK provider spawn vocabulary to cli/headless diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json index 6a0998278..69ff56b1a 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/trajectory.json @@ -50,4 +50,4 @@ "startRef": "f54552b5ac225df762bacbbad08cb66ef46ae5f7", "endRef": "f54552b5ac225df762bacbbad08cb66ef46ae5f7" } -} \ No newline at end of file +} From 9a709eec38f3f8b4ed697171f8c17807e5e70fb8 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 10:16:12 -0400 Subject: [PATCH 07/15] fix(sdk): avoid headless result contract clobber --- .../__tests__/orchestration-upgrades.test.ts | 35 +++++++++++++++++++ packages/sdk/src/relay.ts | 9 +---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts index 5668aada4..4ec8ec2f5 100644 --- a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +++ b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts @@ -607,6 +607,41 @@ describe('AgentRelay orchestration handles', () => { } }); + it('spawnHeadless stores result contracts under the finalized broker name', async () => { + const { client, mock } = createMockFacadeClient(); + const relay = createWiredRelay(client); + const resultContracts = (relay as any).resultContracts as Map; + const existingContract = { jsonSchema: { type: 'boolean' } }; + const jsonSchema = { type: 'object', properties: { ok: { type: 'boolean' } } }; + + resultContracts.set('requested-headless', existingContract); + mock.spawnHeadless.mockImplementationOnce(async () => { + expect(resultContracts.get('requested-headless')).toBe(existingContract); + return { name: 'final-headless', runtime: 'headless' as const }; + }); + + try { + const agent = await relay.spawnHeadless<{ ok: boolean }>({ + name: 'requested-headless', + cli: 'custom-app', + channels: ['reviews'], + harnessConfig: { + runtime: 'headless', + protocol: 'custom-app', + endpoint: 'http://127.0.0.1:4099', + sessionId: 'session-headless', + }, + result: { jsonSchema }, + }); + + expect(agent.name).toBe('final-headless'); + expect(resultContracts.get('requested-headless')).toBe(existingContract); + expect(resultContracts.get('final-headless')).toMatchObject({ jsonSchema }); + } finally { + await relay.shutdown(); + } + }); + it('agent.waitForReady resolves after worker_ready event', async () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index d0695c73b..31eb81c4e 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -945,9 +945,6 @@ export class AgentRelay { await this.invokeLifecycleHook(input.onStart, lifecycleContext, `${methodName}("${input.name}") onStart`); let result: SpawnAgentResult; const resultContract = this.prepareAgentResultContract(input.result); - if (resultContract) { - this.resultContracts.set(input.name, resultContract as InternalAgentResultContract); - } try { const harnessConfig = this.resolveHarnessConfig({ name: input.name, @@ -978,9 +975,6 @@ export class AgentRelay { agentResultSchema: resultContract?.jsonSchema ?? input.agentResultSchema, }); } catch (error) { - if (resultContract) { - this.resultContracts.delete(input.name); - } await this.invokeLifecycleHook( input.onError, { @@ -992,8 +986,7 @@ export class AgentRelay { throw error; } this.resetAgentLifecycleState(result.name); - if (result.name !== input.name && resultContract) { - this.resultContracts.delete(input.name); + if (resultContract) { this.resultContracts.set(result.name, resultContract as InternalAgentResultContract); } const agent = this.ensureAgentHandle( From b9a68bbbb2d028b9169422bf1d344f08d009b57a Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 10:23:50 -0400 Subject: [PATCH 08/15] fix(sdk): type spawn patch merging --- .../2026-05/traj_0kqt1gnfi3v8/summary.md | 32 +++++++++++ .../2026-05/traj_0kqt1gnfi3v8/trajectory.json | 57 +++++++++++++++++++ .../sdk/src/__tests__/lifecycle-hooks.test.ts | 18 +++--- packages/sdk/src/client.ts | 40 +++++++++---- packages/sdk/src/lifecycle-hooks.ts | 17 +++--- 5 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md new file mode 100644 index 000000000..d9ebe1bd1 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md @@ -0,0 +1,32 @@ +# Trajectory: Remove unsafe spawn patch casts + +> **Status:** ✅ Completed +> **Task:** PR-1003 +> **Confidence:** 93% +> **Started:** May 27, 2026 at 10:17 AM +> **Completed:** May 27, 2026 at 10:23 AM + +--- + +## Summary + +Removed unsafe before-spawn patch assertions by preserving concrete spawn input types through runBeforeSpawn and applying allowed SpawnPatch fields explicitly; validated SDK check, lifecycle hook tests, formatting, diff check, and build. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Typed before-spawn patch flow instead of asserting patch shape +- **Chose:** Typed before-spawn patch flow instead of asserting patch shape +- **Reasoning:** The lifecycle hook return is SDK user code, not a broker response, so the safer fix is to preserve the concrete SpawnPtyInput or SpawnCliInput generic through runBeforeSpawn and apply only the allowed SpawnPatch fields without type assertions. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Typed before-spawn patch flow instead of asserting patch shape: Typed before-spawn patch flow instead of asserting patch shape diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json new file mode 100644 index 000000000..23b2eb83d --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json @@ -0,0 +1,57 @@ +{ + "id": "traj_0kqt1gnfi3v8", + "version": 1, + "task": { + "title": "Remove unsafe spawn patch casts", + "source": { + "system": "plain", + "id": "PR-1003" + } + }, + "status": "completed", + "startedAt": "2026-05-27T14:17:32.363Z", + "completedAt": "2026-05-27T14:23:24.058Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-27T14:23:11.212Z" + } + ], + "chapters": [ + { + "id": "chap_dkv8jcg4cvsh", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-27T14:23:11.212Z", + "endedAt": "2026-05-27T14:23:24.058Z", + "events": [ + { + "ts": 1779891791213, + "type": "decision", + "content": "Typed before-spawn patch flow instead of asserting patch shape: Typed before-spawn patch flow instead of asserting patch shape", + "raw": { + "question": "Typed before-spawn patch flow instead of asserting patch shape", + "chosen": "Typed before-spawn patch flow instead of asserting patch shape", + "alternatives": [], + "reasoning": "The lifecycle hook return is SDK user code, not a broker response, so the safer fix is to preserve the concrete SpawnPtyInput or SpawnCliInput generic through runBeforeSpawn and apply only the allowed SpawnPatch fields without type assertions." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Removed unsafe before-spawn patch assertions by preserving concrete spawn input types through runBeforeSpawn and applying allowed SpawnPatch fields explicitly; validated SDK check, lifecycle hook tests, formatting, diff check, and build.", + "approach": "Standard approach", + "confidence": 0.93 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "9a709eec38f3f8b4ed697171f8c17807e5e70fb8", + "endRef": "9a709eec38f3f8b4ed697171f8c17807e5e70fb8" + } +} \ No newline at end of file diff --git a/packages/sdk/src/__tests__/lifecycle-hooks.test.ts b/packages/sdk/src/__tests__/lifecycle-hooks.test.ts index ada189f41..6881c3ccd 100644 --- a/packages/sdk/src/__tests__/lifecycle-hooks.test.ts +++ b/packages/sdk/src/__tests__/lifecycle-hooks.test.ts @@ -213,17 +213,19 @@ describe('AgentRelayClient lifecycle hooks', () => { it('spawnCli fires the hooks with kind=cli', async () => { const { fetchFn } = makeMockFetch(); const client = makeClient(fetchFn); - const before = vi.fn(); - const after = vi.fn(); - client.addListener('beforeAgentSpawn', before); - client.addListener('afterAgentSpawn', after); + const beforeKinds: Array = []; + const afterKinds: Array = []; + client.addListener('beforeAgentSpawn', (ctx) => { + beforeKinds.push(ctx.kind); + }); + client.addListener('afterAgentSpawn', (ctx) => { + afterKinds.push(ctx.kind); + }); await client.spawnCli({ name: 'p', cli: 'claude' }); - expect(before).toHaveBeenCalledTimes(1); - expect((before.mock.calls[0][0] as BeforeAgentSpawnContext).kind).toBe('cli'); - expect(after).toHaveBeenCalledTimes(1); - expect((after.mock.calls[0][0] as AfterAgentSpawnContext).kind).toBe('cli'); + expect(beforeKinds).toEqual(['cli']); + expect(afterKinds).toEqual(['cli']); }); it('recomputes cli transport after beforeAgentSpawn patches add a harness config', async () => { diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index fb093faa4..daf1fb179 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -224,6 +224,20 @@ function buildSpawnCliBody(input: SpawnCliInput, transport: AgentTransport): Rec }; } +function applySpawnPatch( + input: TInput, + patch: SpawnPatch +): TInput { + if (Object.hasOwn(patch, 'args')) input.args = patch.args; + if (Object.hasOwn(patch, 'channels')) input.channels = patch.channels; + if (Object.hasOwn(patch, 'task')) input.task = patch.task; + if (Object.hasOwn(patch, 'model')) input.model = patch.model; + if (Object.hasOwn(patch, 'team')) input.team = patch.team; + if (Object.hasOwn(patch, 'agentToken')) input.agentToken = patch.agentToken; + if (Object.hasOwn(patch, 'harnessConfig')) input.harnessConfig = patch.harnessConfig; + return input; +} + function isProcessRunning(pid: number): boolean { if (!Number.isInteger(pid) || pid <= 0) { return false; @@ -334,13 +348,17 @@ export class AgentRelayClient { * shallow-merged over the running result. Handler exceptions are caught * and logged but do not abort the chain. */ - private async runBeforeSpawn(ctx: BeforeAgentSpawnContext): Promise { - let resolved: SpawnPtyInput | SpawnCliInput = { ...ctx.input }; - for (const handler of this.eventBus.listeners('beforeAgentSpawn') as Array) { + private async runBeforeSpawn( + ctx: BeforeAgentSpawnContext + ): Promise { + let resolved: TInput = { ...ctx.input }; + for (const handler of this.eventBus.listeners<'beforeAgentSpawn', void | SpawnPatch>( + 'beforeAgentSpawn' + )) { try { - const patch = await handler({ ...ctx, input: resolved as Readonly }); + const patch = await handler({ ...ctx, input: resolved }); if (patch && typeof patch === 'object') { - resolved = { ...resolved, ...(patch as SpawnPatch) } as SpawnPtyInput | SpawnCliInput; + resolved = applySpawnPatch(resolved, patch); } } catch (err) { console.error('[agent-relay] beforeAgentSpawn listener threw:', err); @@ -603,7 +621,7 @@ export class AgentRelayClient { // ── Agent lifecycle ──────────────────────────────────────────────── async spawnPty(input: SpawnPtyInput): Promise { - const beforeCtx: BeforeAgentSpawnContext = { + const beforeCtx: BeforeAgentSpawnContext = { kind: 'pty', input, spawnerPid: process.pid, @@ -611,7 +629,7 @@ export class AgentRelayClient { baseUrl: this.baseUrl, }; const t0 = Date.now(); - const resolvedInput = (await this.runBeforeSpawn(beforeCtx)) as SpawnPtyInput; + const resolvedInput = await this.runBeforeSpawn(beforeCtx); try { const rawResult = await this.transport.request('/api/spawn', { method: 'POST', @@ -627,7 +645,7 @@ export class AgentRelayClient { } async spawnCli(input: SpawnCliInput): Promise { - const beforeCtx: BeforeAgentSpawnContext = { + const beforeCtx: BeforeAgentSpawnContext = { kind: 'cli', input, spawnerPid: process.pid, @@ -638,11 +656,11 @@ export class AgentRelayClient { } private async spawnCliWithContext( - beforeCtx: BeforeAgentSpawnContext, + beforeCtx: BeforeAgentSpawnContext, input: SpawnCliInput ): Promise { const t0 = Date.now(); - const resolvedInput = (await this.runBeforeSpawn(beforeCtx)) as SpawnCliInput; + const resolvedInput = await this.runBeforeSpawn(beforeCtx); const transport = resolveSpawnTransport(resolvedInput); if ( transport === 'headless' && @@ -670,7 +688,7 @@ export class AgentRelayClient { async spawnHeadless(input: SpawnHeadlessInput): Promise { const cliInput: SpawnCliInput = { ...input, transport: 'headless' }; - const beforeCtx: BeforeAgentSpawnContext = { + const beforeCtx: BeforeAgentSpawnContext = { kind: 'headless', input: cliInput, spawnerPid: process.pid, diff --git a/packages/sdk/src/lifecycle-hooks.ts b/packages/sdk/src/lifecycle-hooks.ts index 5af0dd965..d670ccd18 100644 --- a/packages/sdk/src/lifecycle-hooks.ts +++ b/packages/sdk/src/lifecycle-hooks.ts @@ -30,6 +30,8 @@ import type { BrokerEvent } from './protocol.js'; import type { Agent, AgentActivityChange, AgentResult, Message } from './relay.js'; import type { SpawnAgentResult, SpawnCliInput, SpawnPtyInput } from './types.js'; +type SpawnInput = SpawnPtyInput | SpawnCliInput; + // ── SpawnPatch ───────────────────────────────────────────────────────────── /** @@ -47,9 +49,8 @@ import type { SpawnAgentResult, SpawnCliInput, SpawnPtyInput } from './types.js' * })); * ``` * - * When multiple handlers return patches, they merge in registration order - * via shallow `Object.assign` — later handlers override earlier ones for - * the same key. + * When multiple handlers return patches, allowed patch fields merge in + * registration order; later handlers override earlier ones for the same key. */ export type SpawnPatch = Partial< Pick< @@ -60,11 +61,11 @@ export type SpawnPatch = Partial< // ── Call-site contexts ───────────────────────────────────────────────────── -export interface BeforeAgentSpawnContext { +export interface BeforeAgentSpawnContext { /** Which spawn API was called. */ kind: 'pty' | 'cli' | 'headless'; /** Raw input the caller passed in. Treat as read-only — return a {@link SpawnPatch} to modify. */ - input: Readonly; + input: Readonly; /** `process.pid` of the calling Node process. Useful for burn-style stamping. */ spawnerPid: number; /** ISO timestamp captured the instant the hook chain started. */ @@ -77,9 +78,11 @@ export type BeforeAgentSpawnHandler = ( ctx: BeforeAgentSpawnContext ) => void | SpawnPatch | Promise; -export interface AfterAgentSpawnContext extends BeforeAgentSpawnContext { +export interface AfterAgentSpawnContext< + TInput extends SpawnInput = SpawnInput, +> extends BeforeAgentSpawnContext { /** Final input that was sent to the broker — original input merged with every handler's patch. */ - resolvedInput: SpawnPtyInput | SpawnCliInput; + resolvedInput: TInput; /** Broker reply on success. */ result?: SpawnAgentResult; /** Set when the broker call rejected. Mutually exclusive with `result`. */ From f45d253e8c367918584d30d61ec4776f2449f00b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 14:25:38 +0000 Subject: [PATCH 09/15] style: auto-format with Prettier --- .../completed/2026-05/traj_0kqt1gnfi3v8/summary.md | 4 +++- .../completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md index d9ebe1bd1..514069aeb 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md +++ b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md @@ -19,6 +19,7 @@ Removed unsafe before-spawn patch assertions by preserving concrete spawn input ## Key Decisions ### Typed before-spawn patch flow instead of asserting patch shape + - **Chose:** Typed before-spawn patch flow instead of asserting patch shape - **Reasoning:** The lifecycle hook return is SDK user code, not a broker response, so the safer fix is to preserve the concrete SpawnPtyInput or SpawnCliInput generic through runBeforeSpawn and apply only the allowed SpawnPatch fields without type assertions. @@ -27,6 +28,7 @@ Removed unsafe before-spawn patch assertions by preserving concrete spawn input ## Chapters ### 1. Work -*Agent: default* + +_Agent: default_ - Typed before-spawn patch flow instead of asserting patch shape: Typed before-spawn patch flow instead of asserting patch shape diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json index 23b2eb83d..68b794114 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/trajectory.json @@ -54,4 +54,4 @@ "startRef": "9a709eec38f3f8b4ed697171f8c17807e5e70fb8", "endRef": "9a709eec38f3f8b4ed697171f8c17807e5e70fb8" } -} \ No newline at end of file +} From 9adfd4c12bba63a108062d862ae84a8e7e06ef24 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 10:43:41 -0400 Subject: [PATCH 10/15] refactor(sdk): simplify high-level spawn facade --- .../2026-05/traj_new9hq87ca49/summary.md | 37 ++ .../2026-05/traj_new9hq87ca49/trajectory.json | 69 ++++ CHANGELOG.md | 8 +- packages/sdk/README.md | 16 +- packages/sdk/src/__tests__/facade.test.ts | 22 +- .../__tests__/orchestration-upgrades.test.ts | 258 +++++++------ packages/sdk/src/__tests__/quickstart.test.ts | 12 +- .../sdk/src/__tests__/spawn-token.test.ts | 10 +- packages/sdk/src/__tests__/unit.test.ts | 9 +- packages/sdk/src/examples/demo.ts | 16 +- packages/sdk/src/examples/quickstart.ts | 9 +- packages/sdk/src/examples/ralph-loop.ts | 8 +- packages/sdk/src/relay.ts | 359 ++++-------------- packages/sdk/src/spawn-from-env.ts | 3 +- 14 files changed, 391 insertions(+), 445 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md new file mode 100644 index 000000000..032d6b184 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md @@ -0,0 +1,37 @@ +# Trajectory: Simplify high-level spawn facade + +> **Status:** ✅ Completed +> **Task:** PR-1003 +> **Confidence:** 91% +> **Started:** May 27, 2026 at 10:33 AM +> **Completed:** May 27, 2026 at 10:43 AM + +--- + +## Summary + +Simplified the high-level AgentRelay spawn facade to a single overloaded spawnAgent(config) API with runtime-discriminated pty/headless configs; removed shorthand CLI spawners and high-level spawnPty/spawnHeadless/spawn/spawnAndWait surfaces; updated SDK docs, examples, and focused tests. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Use AgentRelay.spawnAgent as the only high-level spawn facade +- **Chose:** Use AgentRelay.spawnAgent as the only high-level spawn facade +- **Reasoning:** The user asked to simplify the high-level SDK and avoid separate pty/headless/named CLI entry points. A single overloaded spawnAgent(config) keeps one public spelling while preserving runtime-specific typing through a pty/headless config discriminant. + +### Recommend narrowing Agent Relay around communication core +- **Chose:** Recommend narrowing Agent Relay around communication core +- **Reasoning:** The repo's public promise is real-time agent-to-agent communication, but the default CLI and SDK also expose cloud runtime, proactive agents, drive/relayfile, memory/policy/hooks/trajectory, workflow primitives, GitHub/Slack/browser primitives, personas, web/brand, and multiple bridge surfaces. Keep broker, messaging, spawning, MCP, lifecycle, logs, and minimal SDK as core; move higher-level orchestration and integrations behind extension packages or separate workspaces. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Use AgentRelay.spawnAgent as the only high-level spawn facade: Use AgentRelay.spawnAgent as the only high-level spawn facade +- Recommend narrowing Agent Relay around communication core: Recommend narrowing Agent Relay around communication core diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json new file mode 100644 index 000000000..47cd2d4e3 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json @@ -0,0 +1,69 @@ +{ + "id": "traj_new9hq87ca49", + "version": 1, + "task": { + "title": "Simplify high-level spawn facade", + "source": { + "system": "plain", + "id": "PR-1003" + } + }, + "status": "completed", + "startedAt": "2026-05-27T14:33:21.756Z", + "completedAt": "2026-05-27T14:43:14.398Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-27T14:35:38.895Z" + } + ], + "chapters": [ + { + "id": "chap_b6rs7s6fn18n", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-27T14:35:38.895Z", + "endedAt": "2026-05-27T14:43:14.398Z", + "events": [ + { + "ts": 1779892538896, + "type": "decision", + "content": "Use AgentRelay.spawnAgent as the only high-level spawn facade: Use AgentRelay.spawnAgent as the only high-level spawn facade", + "raw": { + "question": "Use AgentRelay.spawnAgent as the only high-level spawn facade", + "chosen": "Use AgentRelay.spawnAgent as the only high-level spawn facade", + "alternatives": [], + "reasoning": "The user asked to simplify the high-level SDK and avoid separate pty/headless/named CLI entry points. A single overloaded spawnAgent(config) keeps one public spelling while preserving runtime-specific typing through a pty/headless config discriminant." + }, + "significance": "high" + }, + { + "ts": 1779892640276, + "type": "decision", + "content": "Recommend narrowing Agent Relay around communication core: Recommend narrowing Agent Relay around communication core", + "raw": { + "question": "Recommend narrowing Agent Relay around communication core", + "chosen": "Recommend narrowing Agent Relay around communication core", + "alternatives": [], + "reasoning": "The repo's public promise is real-time agent-to-agent communication, but the default CLI and SDK also expose cloud runtime, proactive agents, drive/relayfile, memory/policy/hooks/trajectory, workflow primitives, GitHub/Slack/browser primitives, personas, web/brand, and multiple bridge surfaces. Keep broker, messaging, spawning, MCP, lifecycle, logs, and minimal SDK as core; move higher-level orchestration and integrations behind extension packages or separate workspaces." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Simplified the high-level AgentRelay spawn facade to a single overloaded spawnAgent(config) API with runtime-discriminated pty/headless configs; removed shorthand CLI spawners and high-level spawnPty/spawnHeadless/spawn/spawnAndWait surfaces; updated SDK docs, examples, and focused tests.", + "approach": "Standard approach", + "confidence": 0.91 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "b9a68bbbb2d028b9169422bf1d344f08d009b57a", + "endRef": "b9a68bbbb2d028b9169422bf1d344f08d009b57a" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f729cc7db..1cf777537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Broker and TypeScript SDK structured result contracts add the `submit_result` MCP tool, `agent.waitForResult()`, per-spawn `result.onResult`, and `relay.addListener('agentResult', ...)` for typed JSON worker outcomes. - `@agent-relay/sdk` and `agent-relay-broker` add broker-executable `pty` and `headless` harness configs, so custom CLIs can be configured without Rust changes while spawn requests remain self-contained. - `agent-relay-broker` accepts resolved harness configs on spawn and adds a headless app-server driver for delivering Relay messages to existing OpenCode server sessions. -- `@agent-relay/sdk` exposes `AgentRelay.spawnHeadless({ cli, ... })` so high-level facade callers can launch headless harness agents with the same lifecycle hooks and handles as `spawnPty()`. +- `@agent-relay/sdk` exposes `AgentRelay.spawnAgent({ runtime, cli, ... })` as the single high-level spawn facade for both PTY and headless agents. - `@agent-relay/sdk` adds `AgentRelay.getPersonaSpawnPlan(id)` and a `getPersonaSpawnPlan` export for dry-run inspection of a persona's resolved harness argv, skill installs, mount policy, sidecars, and inputs. ### Changed @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `@agent-relay/sdk` swaps `@agentworkforce/harness-kit` + `@agentworkforce/workload-router` for `@agentworkforce/persona-kit@^3`. The persona tier system, the `tier` option on `spawnPersona`, the legacy relay-side `PersonaFile` / `PersonaTier` / `PersonaTierSpec` / `ResolvedPersona` / `PersonaSpawnSpec` / `MaterializedConfigFile` types, and the `buildPersonaSpawnSpec` / `materializePersonaConfigFiles` / `restorePersonaConfigFiles` helpers are removed. `loadPersona` now returns the canonical `PersonaSpec`, and `spawnPersona({ persona })` takes a `PersonaSpec` instead of a resolved persona. - `@agent-relay/sdk` removes persona support from the SDK surface: the `./personas` subpath, persona helper/type exports, `AgentRelay.spawnPersona()`, `AgentRelay.getPersonaSpawnPlan()`, and `AgentRelayOptions.personaDirs` are gone. The SDK no longer depends on `@agentworkforce/persona-kit`. - `@agent-relay/sdk` renames the raw client spawn surface from provider terminology to CLI terminology: `AgentRelayClient.spawnProvider()` is now `spawnCli()`, `SpawnProviderInput` is now `SpawnCliInput`, and `SpawnHeadlessInput.provider` is now `SpawnHeadlessInput.cli`. +- `@agent-relay/sdk` removes the high-level `AgentRelay.spawnPty()`, `AgentRelay.spawnHeadless()`, positional `AgentRelay.spawn()`, `AgentRelay.spawnAndWait()`, and shorthand CLI spawners such as `relay.claude.spawn()`. Use `AgentRelay.spawnAgent({ runtime: "pty" | "headless", cli, ... })`. - `agent-relay-broker`'s public Rust protocol types now require typed ID newtypes (`WorkerName`, `DeliveryId`, `EventId`, `WorkspaceId`, `WorkspaceAlias`, `ThreadId`, `AgentId`, `RequestId`, `ChannelName`, `MessageTarget`) on every protocol struct and enum variant in `protocol.rs`, `types.rs`, and `listen_api.rs::ListenApiRequest`. The new wrappers live in `crates/broker/src/lib.rs` under `pub mod ids`. JSON wire format is unchanged because every wrapper is `#[serde(transparent)]`, so the broker ↔ SDK channel and on-disk persisted state remain byte-compatible. - `agent-relay spawn` and SDK spawn calls now return harness `sessionId` metadata for resumable Claude and Codex PTY sessions. - `sdk-swift`: renamed the broker client class `RelayCast` → `AgentRelayClient`. @@ -38,8 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Migration Guidance - Personas relying on `tiers.*` need to be flattened to a single top-level `harness` / `model` / `systemPrompt`. The shape that persona-kit (and the `agentworkforce` CLI) consumes is now the only supported shape. -- Callers that previously used `spawnPersona` to "just launch the harness" — without persona-kit's skill / mount / sidecar side effects — should use `AgentRelay.getPersonaSpawnPlan(id)` to inspect the plan and call `spawnPty` with the plan's `cli` + `args` themselves. -- Launch personas through the owning CLI or package and pass the resulting command to `relay.spawnPty(...)` or the SDK's headless CLI APIs; for AgentWorkforce personas, use `npx agentworkforce persona run ` once available so persona side effects remain CLI-owned. +- Callers that previously used `spawnPersona` to "just launch the harness" — without persona-kit's skill / mount / sidecar side effects — should use `AgentRelay.getPersonaSpawnPlan(id)` to inspect the plan and call `spawnAgent({ runtime: "pty", ... })` with the plan's `cli` + `args` themselves. +- Launch personas through the owning CLI or package and pass the resulting command to `relay.spawnAgent({ runtime: "pty", ... })` or `relay.spawnAgent({ runtime: "headless", ... })`; for AgentWorkforce personas, use `npx agentworkforce persona run ` once available so persona side effects remain CLI-owned. +- Replace high-level `relay.spawnPty(...)`, `relay.spawnHeadless(...)`, `relay.spawn(...)`, `relay.spawnAndWait(...)`, and `relay..spawn(...)` calls with `relay.spawnAgent({ runtime: "pty" | "headless", cli, ... })`; wait explicitly with the returned agent handle when needed. - Replace `client.spawnProvider({ provider, ... })` with `client.spawnCli({ cli, ... })`; replace `client.spawnHeadless({ provider, ... })` with `client.spawnHeadless({ cli, ... })`. - Downstream Rust callers must construct identifiers via `relay_broker::ids::{WorkerName, DeliveryId, EventId, MessageTarget, …}` instead of `String`. Each newtype impls `From` / `From<&str>` and `Deref`, so most string-handling code keeps compiling; only construction sites (`HashMap` keys, struct literals, channel sends) need updates. - Replace ad-hoc target discrimination (`target.starts_with('#')`, `target == "thread"`) with `MessageTarget::kind()` and match on `MessageTargetKind::{Channel, Thread, DirectMessage, Conversation, Worker}`. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 00c6b5403..d9853f083 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -42,9 +42,11 @@ relay.onAgentActivityChanged = ({ name, active, pendingDeliveries }) => { updateThinkingBadge(name, active ? `Thinking (${pendingDeliveries})` : 'Idle'); }; -// Spawn agents using shorthand spawners -const worker = await relay.claude.spawn({ +// Spawn a PTY-backed agent +const worker = await relay.spawnAgent({ name: 'Worker1', + cli: 'claude', + runtime: 'pty', channels: ['general'], // Lifecycle hooks can be sync or async functions. onStart: ({ name }) => console.log(`spawning ${name}`), @@ -52,16 +54,20 @@ const worker = await relay.claude.spawn({ onError: ({ name, error }) => console.error(`failed to spawn ${name}`, error), }); -// Or use the generic spawn method -const agent = await relay.spawn('Worker2', 'codex', 'Build the API', { +const agent = await relay.spawnAgent({ + name: 'Worker2', + cli: 'codex', + runtime: 'pty', + task: 'Build the API', channels: ['dev'], model: 'gpt-4o', }); // Headless harnesses keep the same Agent handle surface -const reviewer = await relay.spawnHeadless({ +const reviewer = await relay.spawnAgent({ name: 'HeadlessReviewer', cli: 'opencode', + runtime: 'headless', channels: ['reviews'], task: 'Review the current branch', }); diff --git a/packages/sdk/src/__tests__/facade.test.ts b/packages/sdk/src/__tests__/facade.test.ts index 81c0f03ca..577165611 100644 --- a/packages/sdk/src/__tests__/facade.test.ts +++ b/packages/sdk/src/__tests__/facade.test.ts @@ -63,7 +63,8 @@ test('facade: spawn with initial task delivers task after worker_ready', async ( relay.addListener('agentReady', (agent) => readyNames.push(agent.name)); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: `Task-${suffix}`, cli: 'cat', channels: ['general'], @@ -95,7 +96,8 @@ test('facade: agentReady listener fires when worker becomes ready', async (t) => relay.addListener('agentReady', (agent) => readyAgents.push(agent)); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: `Ready-${suffix}`, cli: 'cat', channels: ['general'], @@ -128,7 +130,8 @@ test('facade: broadcast sends to all agents', async (t) => { relay.addListener('messageSent', (msg) => sentMessages.push(msg)); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: `Broadcast-${suffix}`, cli: 'cat', channels: ['general'], @@ -162,8 +165,8 @@ test('facade: waitForAny returns first agent to exit', async (t) => { try { const [a, b] = await Promise.all([ - relay.spawnPty({ name: `WaitA-${suffix}`, cli: 'cat', channels: ['general'] }), - relay.spawnPty({ name: `WaitB-${suffix}`, cli: 'cat', channels: ['general'] }), + relay.spawnAgent({ runtime: 'pty', name: `WaitA-${suffix}`, cli: 'cat', channels: ['general'] }), + relay.spawnAgent({ runtime: 'pty', name: `WaitB-${suffix}`, cli: 'cat', channels: ['general'] }), ]); // Release agent A — it should be the first to exit @@ -190,7 +193,8 @@ test('facade: waitForAny respects timeout', async (t) => { const relay = makeRelay(bin); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: `Timeout-${suffix}`, cli: 'cat', channels: ['general'], @@ -218,7 +222,8 @@ test('facade: agentExited listener populates exitCode and exitSignal', async (t) relay.addListener('agentExited', (agent) => exitedAgents.push(agent)); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: `Exit-${suffix}`, cli: 'cat', channels: ['general'], @@ -255,7 +260,8 @@ test('facade: getLogs returns log content for agent', async (t) => { const relay = makeRelay(bin); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: `Logs-${suffix}`, cli: 'cat', channels: ['general'], diff --git a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts index 4ec8ec2f5..61b6cb9a9 100644 --- a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +++ b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts @@ -466,12 +466,13 @@ describe('AgentRelayClient orchestration payloads', () => { }); describe('AgentRelay orchestration handles', () => { - it('spawnPty forwards agentToken to the client', async () => { + it('spawnAgent forwards pty agentToken to the client', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); try { - await relay.spawnPty({ + await relay.spawnAgent({ + runtime: 'pty', name: 'token-pty', cli: 'claude', channels: ['general'], @@ -490,12 +491,16 @@ describe('AgentRelay orchestration handles', () => { } }); - it('spawn forwards agentToken through the facade wrapper', async () => { + it('spawnAgent forwards task and agentToken through the facade wrapper', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); try { - await relay.spawn('token-wrapper', 'claude', 'Do work', { + await relay.spawnAgent({ + runtime: 'pty', + name: 'token-wrapper', + cli: 'claude', + task: 'Do work', agentToken: 'agent-token-wrapper', }); @@ -512,18 +517,22 @@ describe('AgentRelay orchestration handles', () => { } }); - it('property spawners forward agentToken for pty and headless runtimes', async () => { + it('spawnAgent forwards agentToken for pty and headless runtimes', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); try { - await relay.codex.spawn({ + await relay.spawnAgent({ + runtime: 'pty', name: 'codex-token', + cli: 'codex', channels: ['general'], agentToken: 'agent-token-codex', }); - await relay.opencode.spawn({ + await relay.spawnAgent({ + runtime: 'headless', name: 'opencode-token', + cli: 'opencode', channels: ['general'], agentToken: 'agent-token-opencode', }); @@ -547,7 +556,7 @@ describe('AgentRelay orchestration handles', () => { } }); - it('spawnHeadless preserves facade lifecycle hooks and custom headless harness config', async () => { + it('spawnAgent preserves facade lifecycle hooks and custom headless harness config', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); const onStart = vi.fn(); @@ -561,7 +570,8 @@ describe('AgentRelay orchestration handles', () => { const jsonSchema = { type: 'object', properties: { ok: { type: 'boolean' } } }; try { - const agent = await relay.spawnHeadless<{ ok: boolean }>({ + const agent = await relay.spawnAgent<{ ok: boolean }>({ + runtime: 'headless', name: 'headless-facade', cli: 'custom-app', channels: ['reviews'], @@ -607,7 +617,7 @@ describe('AgentRelay orchestration handles', () => { } }); - it('spawnHeadless stores result contracts under the finalized broker name', async () => { + it('spawnAgent stores headless result contracts under the finalized broker name', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); const resultContracts = (relay as any).resultContracts as Map; @@ -621,7 +631,8 @@ describe('AgentRelay orchestration handles', () => { }); try { - const agent = await relay.spawnHeadless<{ ok: boolean }>({ + const agent = await relay.spawnAgent<{ ok: boolean }>({ + runtime: 'headless', name: 'requested-headless', cli: 'custom-app', channels: ['reviews'], @@ -646,10 +657,11 @@ describe('AgentRelay orchestration handles', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'ready-agent', cli: 'claude', channels: ['general'], @@ -673,7 +685,8 @@ describe('AgentRelay orchestration handles', () => { relay.addListener('agentResult', (result) => globalResults.push(result)); try { - const agent = await relay.spawnPty<{ ok: boolean }>({ + const agent = await relay.spawnAgent<{ ok: boolean }>({ + runtime: 'pty', name: 'result-agent', cli: 'claude', channels: ['general'], @@ -740,7 +753,8 @@ describe('AgentRelay orchestration handles', () => { const relay = createWiredRelay(client); try { - const agent = await relay.spawnPty<{ ok: boolean }>({ + const agent = await relay.spawnAgent<{ ok: boolean }>({ + runtime: 'pty', name: 'reused-result-agent', cli: 'claude', result: { jsonSchema: true }, @@ -750,7 +764,8 @@ describe('AgentRelay orchestration handles', () => { (error) => error as Error ); - await relay.spawnPty<{ ok: boolean }>({ + await relay.spawnAgent<{ ok: boolean }>({ + runtime: 'pty', name: 'reused-result-agent', cli: 'claude', result: { jsonSchema: true }, @@ -770,14 +785,10 @@ describe('AgentRelay orchestration handles', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - await relay.spawnPty({ - name: 'msg-agent', - cli: 'claude', - channels: ['general'], - }); + await relay.spawnAgent({ runtime: 'pty', name: 'msg-agent', cli: 'claude', channels: ['general'] }); const waitPromise = relay.waitForAgentMessage('msg-agent', 1_000); let resolved = false; @@ -803,28 +814,29 @@ describe('AgentRelay orchestration handles', () => { } }); - it('spawnAndWait can wait for first agent message', async () => { + it('spawnAgent callers can wait explicitly for first agent message', async () => { const { client, mock, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const spawnWaitPromise = relay.spawnAndWait('spawn-msg', 'claude', 'Do the task', { - waitForMessage: true, - timeoutMs: 1_000, + const agent = await relay.spawnAgent({ + runtime: 'pty', + name: 'spawn-msg', + cli: 'claude', + task: 'Do the task', }); - await vi.waitFor(() => { - expect(mock.spawnPty).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'spawn-msg', - cli: 'claude', - task: 'Do the task', - }) - ); - }); + expect(mock.spawnPty).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'spawn-msg', + cli: 'claude', + task: 'Do the task', + }) + ); + const waitPromise = relay.waitForAgentMessage(agent.name, 1_000); emit({ kind: 'worker_ready', name: 'spawn-msg', runtime: 'pty' }); emit({ kind: 'relay_inbound', @@ -834,36 +846,38 @@ describe('AgentRelay orchestration handles', () => { body: 'initialized', }); - await expect(spawnWaitPromise).resolves.toMatchObject({ name: 'spawn-msg' }); + await expect(waitPromise).resolves.toMatchObject({ name: 'spawn-msg' }); } finally { await relay.shutdown(); } }); - it('spawnAndWait falls back to worker_ready when waitForMessage is false', async () => { + it('spawnAgent callers can wait explicitly for worker_ready', async () => { const { client, mock, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const spawnWaitPromise = relay.spawnAndWait('spawn-ready', 'claude', 'Do the task', { - timeoutMs: 1_000, + const agent = await relay.spawnAgent({ + runtime: 'pty', + name: 'spawn-ready', + cli: 'claude', + task: 'Do the task', }); - await vi.waitFor(() => { - expect(mock.spawnPty).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'spawn-ready', - cli: 'claude', - task: 'Do the task', - }) - ); - }); + expect(mock.spawnPty).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'spawn-ready', + cli: 'claude', + task: 'Do the task', + }) + ); + const waitPromise = agent.waitForReady(1_000); emit({ kind: 'worker_ready', name: 'spawn-ready', runtime: 'pty' }); - await expect(spawnWaitPromise).resolves.toMatchObject({ name: 'spawn-ready' }); + await expect(waitPromise).resolves.toBeUndefined(); } finally { await relay.shutdown(); } @@ -880,7 +894,7 @@ describe('AgentRelay orchestration handles', () => { ]); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { const [agent] = await relay.listAgents(); @@ -899,14 +913,18 @@ describe('AgentRelay orchestration handles', () => { const { client } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); const callOrder: string[] = []; const onStart = vi.fn(() => callOrder.push('start')); const onSuccess = vi.fn(() => callOrder.push('success')); const onError = vi.fn(() => callOrder.push('error')); try { - const agent = await relay.spawn('hook-agent', 'claude', 'do work', { + const agent = await relay.spawnAgent({ + runtime: 'pty', + name: 'hook-agent', + cli: 'claude', + task: 'do work', channels: ['general'], onStart, onSuccess, @@ -938,12 +956,16 @@ describe('AgentRelay orchestration handles', () => { const { client } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); let startDone = false; let successDone = false; try { - await relay.spawn('async-hook-agent', 'claude', 'do work', { + await relay.spawnAgent({ + runtime: 'pty', + name: 'async-hook-agent', + cli: 'claude', + task: 'do work', channels: ['general'], onStart: async () => { await new Promise((resolve) => setTimeout(resolve, 5)); @@ -967,13 +989,14 @@ describe('AgentRelay orchestration handles', () => { vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); mock.spawnPty.mockRejectedValueOnce(new Error('spawn failed')); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); const onStart = vi.fn(); const onError = vi.fn(); try { await expect( - relay.spawnPty({ + relay.spawnAgent({ + runtime: 'pty', name: 'hook-agent-fail', cli: 'claude', channels: ['general'], @@ -1005,10 +1028,11 @@ describe('AgentRelay orchestration handles', () => { const { client, mock } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'reason-agent', cli: 'claude', channels: ['general'], @@ -1026,14 +1050,15 @@ describe('AgentRelay orchestration handles', () => { const { client, mock } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); const callOrder: string[] = []; const onStart = vi.fn(() => callOrder.push('start')); const onSuccess = vi.fn(() => callOrder.push('success')); const onError = vi.fn(() => callOrder.push('error')); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'release-hook-agent', cli: 'claude', channels: ['general'], @@ -1066,10 +1091,11 @@ describe('AgentRelay orchestration handles', () => { const { client, mock, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'release-after-exit', cli: 'claude', channels: ['general'], @@ -1095,10 +1121,11 @@ describe('AgentRelay orchestration handles', () => { }) ); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'release-idempotent-race', cli: 'claude', channels: ['general'], @@ -1116,12 +1143,13 @@ describe('AgentRelay orchestration handles', () => { vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); mock.release.mockRejectedValueOnce(new Error('release failed')); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); const onStart = vi.fn(); const onError = vi.fn(); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'release-hook-fail', cli: 'claude', channels: ['general'], @@ -1155,11 +1183,12 @@ describe('AgentRelay orchestration handles', () => { const { client } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); let successDone = false; try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'release-async-hook-agent', cli: 'claude', channels: ['general'], @@ -1183,12 +1212,13 @@ describe('AgentRelay orchestration handles', () => { const { client } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); const onStart = vi.fn(); const onError = vi.fn(); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'release-startup-fail-agent', cli: 'claude', channels: ['general'], @@ -1215,7 +1245,7 @@ describe('AgentRelay orchestration handles', () => { const { client, mock } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { const system = relay.system(); @@ -1241,7 +1271,7 @@ describe('AgentRelay orchestration handles', () => { const { client, mock, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { type DeliveryResult = Awaited>; expectTypeOf().toEqualTypeOf<{ @@ -1289,7 +1319,7 @@ describe('AgentRelay orchestration handles', () => { const { client, mock, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { const wait = relay.sendAndWaitForDelivery({ to: 'worker', @@ -1322,7 +1352,7 @@ describe('AgentRelay orchestration handles', () => { const { client, mock, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { const wait = relay.sendAndWaitForDelivery({ to: 'worker', @@ -1362,7 +1392,7 @@ describe('AgentRelay orchestration handles', () => { const { client, mock, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { const wait = relay.sendAndWaitForDelivery({ to: 'worker', @@ -1409,7 +1439,7 @@ describe('AgentRelay orchestration handles', () => { targets: [timeoutFixture.target], }); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { const result = await relay.sendAndWaitForDelivery( { to: timeoutFixture.target, text: 'timeout contract probe' }, @@ -1447,7 +1477,7 @@ describe('AgentRelay orchestration handles', () => { cases: Array<{ input: string; normalized: string }>; }>('broker-identity-normalization.json'); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); const seenFrom: string[] = []; relay.addListener('messageReceived', (message) => { seenFrom.push(message.from); @@ -1478,7 +1508,7 @@ describe('AgentRelay orchestration handles', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { await relay.listAgents(); @@ -1558,9 +1588,10 @@ describe('Agent.status computed getter', () => { const { client } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'status-agent', cli: 'claude', channels: ['general'], @@ -1576,9 +1607,10 @@ describe('Agent.status computed getter', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'status-ready', cli: 'claude', channels: ['general'], @@ -1596,9 +1628,10 @@ describe('Agent.status computed getter', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'status-idle', cli: 'claude', channels: ['general'], @@ -1618,9 +1651,10 @@ describe('Agent.status computed getter', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'status-exited', cli: 'claude', channels: ['general'], @@ -1639,15 +1673,11 @@ describe('Agent.status computed getter', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); const exitedReasons: Array = []; relay.addListener('agentExited', (agent) => exitedReasons.push(agent.exitReason)); try { - await relay.spawnPty({ - name: 'reason-exited', - cli: 'claude', - channels: ['general'], - }); + await relay.spawnAgent({ runtime: 'pty', name: 'reason-exited', cli: 'claude', channels: ['general'] }); emit({ kind: 'agent_exited', @@ -1667,9 +1697,10 @@ describe('Agent.status computed getter', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'status-resume', cli: 'claude', channels: ['general'], @@ -1692,9 +1723,10 @@ describe('Agent.onOutput', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'output-agent', cli: 'claude', channels: ['general'], @@ -1716,9 +1748,10 @@ describe('Agent.onOutput', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'my-agent', cli: 'claude', channels: ['general'], @@ -1740,9 +1773,10 @@ describe('Agent.onOutput', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'unsub-agent', cli: 'claude', channels: ['general'], @@ -1765,9 +1799,10 @@ describe('Agent.onOutput', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'stream-filter-agent', cli: 'claude', channels: ['general'], @@ -1790,9 +1825,10 @@ describe('Agent.onOutput', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'all-streams-agent', cli: 'claude', channels: ['general'], @@ -1814,9 +1850,10 @@ describe('Agent.onOutput', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'stderr-filter-agent', cli: 'claude', channels: ['general'], @@ -1838,9 +1875,10 @@ describe('Agent.onOutput', () => { const { client, emit } = createMockFacadeClient(); vi.spyOn(AgentRelayClient, 'start').mockResolvedValue(client); - const relay = new AgentRelay(); + const relay = new AgentRelay({ env: { RELAY_API_KEY: TEST_RELAY_API_KEY } }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: 'explicit-mode-agent', cli: 'claude', channels: ['general'], diff --git a/packages/sdk/src/__tests__/quickstart.test.ts b/packages/sdk/src/__tests__/quickstart.test.ts index 0431ba359..3c935aa79 100644 --- a/packages/sdk/src/__tests__/quickstart.test.ts +++ b/packages/sdk/src/__tests__/quickstart.test.ts @@ -69,8 +69,9 @@ test('facade: spawn → message → list → release → shutdown', async (t) => try { // Spawn two agents in parallel const [codex, worker1] = await Promise.all([ - relay.codex.spawn({ name: `Codex-${suffix}` }), - relay.spawnPty({ + relay.spawnAgent({ name: `Codex-${suffix}`, cli: 'codex', runtime: 'pty' }), + relay.spawnAgent({ + runtime: 'pty', name: `Worker1-${suffix}`, cli: 'cat', channels: ['general'], @@ -142,8 +143,8 @@ test('facade: agent.sendMessage sends from the agent identity', async (t) => { try { const [a, b] = await Promise.all([ - relay.spawnPty({ name: `A-${suffix}`, cli: 'cat', channels: ['general'] }), - relay.spawnPty({ name: `B-${suffix}`, cli: 'cat', channels: ['general'] }), + relay.spawnAgent({ runtime: 'pty', name: `A-${suffix}`, cli: 'cat', channels: ['general'] }), + relay.spawnAgent({ runtime: 'pty', name: `B-${suffix}`, cli: 'cat', channels: ['general'] }), ]); const msg = await a.sendMessage({ to: b.name, text: 'ping' }); @@ -174,7 +175,8 @@ test('facade: message threading with threadId', async (t) => { }); try { - const agent = await relay.spawnPty({ + const agent = await relay.spawnAgent({ + runtime: 'pty', name: `Thread-${suffix}`, cli: 'cat', channels: ['general'], diff --git a/packages/sdk/src/__tests__/spawn-token.test.ts b/packages/sdk/src/__tests__/spawn-token.test.ts index 18ff5ddcd..10c79a1b9 100644 --- a/packages/sdk/src/__tests__/spawn-token.test.ts +++ b/packages/sdk/src/__tests__/spawn-token.test.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest'; -import type { SpawnHeadlessAgentInput } from '../relay.js'; +import type { SpawnAgentConfig, SpawnHeadlessAgentConfig } from '../relay.js'; import type { SpawnCliInput, SpawnHeadlessInput, SpawnPtyInput } from '../types.js'; describe('spawn input agentToken types', () => { @@ -58,10 +58,11 @@ describe('spawn input agentToken types', () => { expectTypeOf().toEqualTypeOf(); }); - it('SpawnHeadlessAgentInput uses cli for the high-level facade', () => { + it('SpawnAgentConfig uses runtime and cli for the high-level facade', () => { const withHeadlessHarness = { name: 'headless-facade', cli: 'custom-app-server', + runtime: 'headless', agentToken: 'jwt-token', harnessConfig: { runtime: 'headless', @@ -69,9 +70,10 @@ describe('spawn input agentToken types', () => { endpoint: 'http://127.0.0.1:4099', sessionId: 'session-headless', }, - } satisfies SpawnHeadlessAgentInput; + } satisfies SpawnHeadlessAgentConfig; - expectTypeOf(withHeadlessHarness).toMatchTypeOf(); + expectTypeOf(withHeadlessHarness).toMatchTypeOf(); + expectTypeOf(withHeadlessHarness).toMatchTypeOf(); expectTypeOf(withHeadlessHarness.cli).toMatchTypeOf(); }); }); diff --git a/packages/sdk/src/__tests__/unit.test.ts b/packages/sdk/src/__tests__/unit.test.ts index 4445c9d54..364c6dfc2 100644 --- a/packages/sdk/src/__tests__/unit.test.ts +++ b/packages/sdk/src/__tests__/unit.test.ts @@ -361,14 +361,11 @@ test('waitForIdle: idle resolves before timeout', async () => { const result = await promise; assert.equal(result, 'idle'); }); -// ── shorthand spawners ─────────────────────────────────────────────────────── +// ── spawnAgent facade ─────────────────────────────────────────────────────── -test('AgentRelay: has shorthand spawners for major CLIs', () => { +test('AgentRelay: has a single high-level spawnAgent facade', () => { const relay = new AgentRelay({ channels: ['general'] }); - assert.ok(relay.claude, 'relay.claude should be defined'); - assert.ok(relay.codex, 'relay.codex should be defined'); - assert.ok(relay.gemini, 'relay.gemini should be defined'); - assert.ok(relay.opencode, 'relay.opencode should be defined'); + assert.equal(typeof relay.spawnAgent, 'function'); }); // ── agent.status ──────────────────────────────────────────────────────────── diff --git a/packages/sdk/src/examples/demo.ts b/packages/sdk/src/examples/demo.ts index b433ca7f2..a8b0bbf60 100644 --- a/packages/sdk/src/examples/demo.ts +++ b/packages/sdk/src/examples/demo.ts @@ -36,8 +36,20 @@ relay.addListener('agentExited', (agent) => { console.log('\n─── Spawning agents ───\n'); const [agentA, agentB] = await Promise.all([ - relay.spawnPty({ name: 'AgentA', cli: 'claude', args: ['--print'], channels: ['general'] }), - relay.spawnPty({ name: 'AgentB', cli: 'claude', args: ['--print'], channels: ['general'] }), + relay.spawnAgent({ + name: 'AgentA', + cli: 'claude', + runtime: 'pty', + args: ['--print'], + channels: ['general'], + }), + relay.spawnAgent({ + name: 'AgentB', + cli: 'claude', + runtime: 'pty', + args: ['--print'], + channels: ['general'], + }), ]); // ── Send messages ─────────────────────────────────────────────────────────── diff --git a/packages/sdk/src/examples/quickstart.ts b/packages/sdk/src/examples/quickstart.ts index 45bbc7d87..b52168c8a 100644 --- a/packages/sdk/src/examples/quickstart.ts +++ b/packages/sdk/src/examples/quickstart.ts @@ -39,16 +39,17 @@ relay.addListener('agentExited', (agent) => { // ── Create agents with sane defaults, running locally ─────────────────────── const [codex, claude, gemini] = await Promise.all([ - relay.codex.spawn(), - relay.claude.spawn(), - relay.gemini.spawn(), + relay.spawnAgent({ name: 'Codex', cli: 'codex', runtime: 'pty' }), + relay.spawnAgent({ name: 'Claude', cli: 'claude', runtime: 'pty' }), + relay.spawnAgent({ name: 'Gemini', cli: 'gemini', runtime: 'pty' }), ]); // ── Configure messaging with custom CLI agents ───────────────────────────── -const worker1 = await relay.spawnPty({ +const worker1 = await relay.spawnAgent({ name: 'Worker1', cli: 'codex', + runtime: 'pty', args: ['--model', 'gpt-5'], channels: ['general'], }); diff --git a/packages/sdk/src/examples/ralph-loop.ts b/packages/sdk/src/examples/ralph-loop.ts index cfc5f45bb..459b85941 100644 --- a/packages/sdk/src/examples/ralph-loop.ts +++ b/packages/sdk/src/examples/ralph-loop.ts @@ -212,12 +212,16 @@ while (iteration < MAX_ITERATIONS) { console.log(` ⚡ Spawning Claude (architect) + Codex (builder) — round: ${roundLabel}`); const [architect, builder] = await Promise.all([ - relay.claude.spawn({ + relay.spawnAgent({ name: `Architect-${story.id}-${roundLabel}`, + cli: 'claude', + runtime: 'pty', channels: ['general'], }), - relay.codex.spawn({ + relay.spawnAgent({ name: `Builder-${story.id}-${roundLabel}`, + cli: 'codex', + runtime: 'pty', args: ['--full-auto'], channels: ['general'], }), diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index 31eb81c4e..d26b05915 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -1,7 +1,7 @@ /** * High-level facade for the Agent Relay SDK. * - * Provides a clean, property-based API on top of the lower-level + * Provides a clean, handle-oriented API on top of the lower-level * {@link AgentRelayClient} protocol client. * * @example @@ -13,7 +13,7 @@ * relay.addListener('messageReceived', (message) => console.log(message)); * relay.addListener('agentSpawned', (agent) => console.log("spawned", agent.name)); * - * const codex = await relay.codex.spawn(); + * const codex = await relay.spawnAgent({ name: "Codex", cli: "codex", runtime: "pty" }); * const human = relay.human({ name: "System" }); * await human.sendMessage({ to: codex.name, text: "Hello!" }); * @@ -200,8 +200,19 @@ export interface AgentResultOptions { type SpawnWithLifecycle = TInput & SpawnLifecycleHooks & { result?: AgentResultOptions }; -export type SpawnHeadlessAgentInput = ClientSpawnHeadlessInput & - SpawnLifecycleHooks & { result?: AgentResultOptions }; +export type SpawnPtyAgentConfig = SpawnWithLifecycle< + SpawnPtyInput & { runtime: 'pty' }, + TAgentResult +>; + +export type SpawnHeadlessAgentConfig = SpawnWithLifecycle< + ClientSpawnHeadlessInput & { runtime: 'headless' }, + TAgentResult +>; + +export type SpawnAgentConfig = + | SpawnPtyAgentConfig + | SpawnHeadlessAgentConfig; export type AgentStatus = 'spawning' | 'ready' | 'idle' | 'exited'; export type DeliveryWaitStatus = 'ack' | 'failed' | 'timeout'; @@ -281,40 +292,6 @@ export interface ReleaseOptions extends ReleaseLifecycleHooks { reason?: string; } -export interface SpawnOptions extends SpawnLifecycleHooks { - args?: string[]; - channels?: string[]; - model?: string; - cwd?: string; - team?: string; - shadowOf?: string; - shadowMode?: string; - idleThresholdSecs?: number; - restartPolicy?: RestartPolicy; - harnessConfig?: ResolvedHarnessConfig; - /** Optional pre-minted relaycast agent token (`at_live_`, from - * `registerAgent(workspaceKey, name)` in `@agent-relay/sdk/http`). The - * broker plumbs this as `RELAY_AGENT_TOKEN`, which the relaycast MCP - * authenticates with. When omitted, the relaycast MCP auto-mints a token - * using `RELAY_API_KEY` + the spawn name; that is the recommended path. - * Note: this is a relaycast credential, NOT a relayfile/relayauth token — - * override `env.RELAYFILE_TOKEN` on the constructor for relayfile auth. */ - agentToken?: string; - /** When true, skip injecting the relay MCP configuration and protocol prompt into the spawned agent. - * Useful for minor tasks where relay messaging is not needed, saving tokens. */ - skipRelayPrompt?: boolean; - /** - * Enables a structured-result MCP tool for the spawned agent and validates - * submissions on the SDK side. - */ - result?: AgentResultOptions; -} - -export interface SpawnAndWaitOptions extends SpawnOptions { - timeoutMs?: number; - waitForMessage?: boolean; -} - type AgentOutputPayload = { stream: string; chunk: string }; type AgentOutputCallback = ((chunk: string) => void) | ((data: AgentOutputPayload) => void); @@ -378,33 +355,6 @@ export interface HumanHandle { }): Promise; } -export interface AgentSpawner { - spawn(options?: SpawnerSpawnOptions): Promise>; -} - -export interface SpawnerSpawnOptions extends SpawnLifecycleHooks { - name?: string; - args?: string[]; - channels?: string[]; - task?: string; - model?: string; - cwd?: string; - idleThresholdSecs?: number; - harnessConfig?: ResolvedHarnessConfig; - /** Optional pre-minted relaycast agent token (`at_live_`, from - * `registerAgent(workspaceKey, name)` in `@agent-relay/sdk/http`). The - * broker plumbs this as `RELAY_AGENT_TOKEN`, which the relaycast MCP - * authenticates with. When omitted, the relaycast MCP auto-mints a token - * using `RELAY_API_KEY` + the spawn name; that is the recommended path. - * Note: this is a relaycast credential, NOT a relayfile/relayauth token — - * override `env.RELAYFILE_TOKEN` on the constructor for relayfile auth. */ - agentToken?: string; - /** When true, skip injecting the relay MCP configuration and protocol prompt into the spawned agent. - * Useful for minor tasks where relay messaging is not needed, saving tokens. */ - skipRelayPrompt?: boolean; - result?: AgentResultOptions; -} - export interface AgentRelayOptions { binaryPath?: string; binaryArgs?: AgentRelayBrokerInitArgs; @@ -552,12 +502,6 @@ export class AgentRelay { return `https://agentrelay.com/observer?key=${this.relayApiKey}`; } - // Shorthand spawners - readonly codex: AgentSpawner; - readonly claude: AgentSpawner; - readonly gemini: AgentSpawner; - readonly opencode: AgentSpawner; - private readonly clientOptions: AgentRelaySpawnOptions; private readonly defaultChannels: string[]; private readonly requestedWorkspaceId?: string; @@ -615,11 +559,6 @@ export class AgentRelay { env: options.env, requestTimeoutMs: options.requestTimeoutMs, }; - - this.codex = this.createSpawner('codex', 'Codex', 'pty'); - this.claude = this.createSpawner('claude', 'Claude', 'pty'); - this.gemini = this.createSpawner('gemini', 'Gemini', 'pty'); - this.opencode = this.createSpawner('opencode', 'OpenCode', 'headless'); } private getWorkspaceRegistryPath(): string { @@ -793,195 +732,74 @@ export class AgentRelay { // ── Spawning ──────────────────────────────────────────────────────────── - async spawnPty( - input: SpawnWithLifecycle + async spawnAgent( + config: SpawnPtyAgentConfig + ): Promise>; + async spawnAgent( + config: SpawnHeadlessAgentConfig + ): Promise>; + async spawnAgent( + config: SpawnAgentConfig ): Promise> { const client = await this.ensureStarted(); - if (!input.channels || input.channels.length === 0) { + if (!config.channels || config.channels.length === 0) { console.warn( - `[AgentRelay] spawnPty("${input.name}"): no channels specified, defaulting to "general". ` + + `[AgentRelay] spawnAgent("${config.name}"): no channels specified, defaulting to "general". ` + 'Set explicit channels for workflow isolation.' ); } - const channels = input.channels ?? ['general']; + const channels = config.channels ?? ['general']; const lifecycleContext: SpawnLifecycleContext = { - name: input.name, - cli: input.cli, + name: config.name, + cli: config.cli, channels, - task: input.task, + task: config.task, }; - await this.invokeLifecycleHook(input.onStart, lifecycleContext, `spawnPty("${input.name}") onStart`); + await this.invokeLifecycleHook(config.onStart, lifecycleContext, `spawnAgent("${config.name}") onStart`); let result: SpawnAgentResult; - const resultContract = this.prepareAgentResultContract(input.result); - if (resultContract) { - this.resultContracts.set(input.name, resultContract as InternalAgentResultContract); - } - try { - const harnessConfig = this.resolveHarnessConfig(input); - result = await client.spawnPty({ - name: input.name, - cli: input.cli, - args: input.args, - channels, - task: input.task, - model: input.model, - cwd: input.cwd, - team: input.team, - agentToken: input.agentToken, - shadowOf: input.shadowOf, - shadowMode: input.shadowMode, - continueFrom: input.continueFrom, - idleThresholdSecs: input.idleThresholdSecs, - restartPolicy: input.restartPolicy, - harnessConfig, - skipRelayPrompt: input.skipRelayPrompt, - agentResultSchema: resultContract?.jsonSchema ?? input.agentResultSchema, - }); - } catch (error) { - if (resultContract) { - this.resultContracts.delete(input.name); - } - await this.invokeLifecycleHook( - input.onError, - { - ...lifecycleContext, - error, - }, - `spawnPty("${input.name}") onError` - ); - throw error; - } - this.resetAgentLifecycleState(result.name); - if (result.name !== input.name && resultContract) { - this.resultContracts.delete(input.name); - this.resultContracts.set(result.name, resultContract as InternalAgentResultContract); - } - const agent = this.ensureAgentHandle( - result.name, - result.runtime, - channels, - result - ) as Agent; - await this.invokeLifecycleHook( - input.onSuccess, - { - ...lifecycleContext, - name: result.name, - runtime: result.runtime, - sessionId: result.sessionId, - }, - `spawnPty("${input.name}") onSuccess` - ); - return agent; - } - - async spawnHeadless( - input: SpawnHeadlessAgentInput - ): Promise> { - return this.spawnHeadlessWithLifecycle('spawnHeadless', input); - } - - async spawn( - name: string, - cli: string, - task?: string, - options?: SpawnOptions - ): Promise> { - return this.spawnPty({ - name, - cli, - task, - args: options?.args, - channels: options?.channels, - model: options?.model, - cwd: options?.cwd, - team: options?.team, - agentToken: options?.agentToken, - shadowOf: options?.shadowOf, - shadowMode: options?.shadowMode, - idleThresholdSecs: options?.idleThresholdSecs, - restartPolicy: options?.restartPolicy, - harnessConfig: options?.harnessConfig, - skipRelayPrompt: options?.skipRelayPrompt, - result: options?.result, - onStart: options?.onStart, - onSuccess: options?.onSuccess, - onError: options?.onError, - }); - } - - async spawnAndWait( - name: string, - cli: string, - task: string, - options?: SpawnAndWaitOptions - ): Promise> { - const { timeoutMs, waitForMessage, ...spawnOptions } = options ?? {}; - await this.spawn(name, cli, task, spawnOptions); - if (waitForMessage) { - return this.waitForAgentMessage(name, timeoutMs ?? 60_000) as Promise>; - } - return this.waitForAgentReady(name, timeoutMs ?? 60_000) as Promise>; - } - - private async spawnHeadlessWithLifecycle( - methodName: 'spawn' | 'spawnHeadless', - input: SpawnHeadlessAgentInput - ): Promise> { - const client = await this.ensureStarted(); - if (!input.channels || input.channels.length === 0) { - console.warn( - `[AgentRelay] ${methodName}("${input.name}"): no channels specified, defaulting to "general". ` + - 'Set explicit channels for workflow isolation.' - ); - } - const channels = input.channels ?? ['general']; - const lifecycleContext: SpawnLifecycleContext = { - name: input.name, - cli: input.cli, - channels, - task: input.task, - }; - await this.invokeLifecycleHook(input.onStart, lifecycleContext, `${methodName}("${input.name}") onStart`); - let result: SpawnAgentResult; - const resultContract = this.prepareAgentResultContract(input.result); + const resultContract = this.prepareAgentResultContract(config.result); try { const harnessConfig = this.resolveHarnessConfig({ - name: input.name, - cli: input.cli, - args: input.args, - task: input.task, - model: input.model, - cwd: input.cwd, - harnessConfig: input.harnessConfig, + name: config.name, + cli: config.cli, + args: config.args, + task: config.task, + model: config.model, + cwd: config.cwd, + harnessConfig: config.harnessConfig, }); - result = await client.spawnHeadless({ - name: input.name, - cli: input.cli, - args: input.args, + const spawnInput = { + name: config.name, + cli: config.cli, + args: config.args, channels, - task: input.task, - model: input.model, - cwd: input.cwd, - team: input.team, - agentToken: input.agentToken, - shadowOf: input.shadowOf, - shadowMode: input.shadowMode, - idleThresholdSecs: input.idleThresholdSecs, - restartPolicy: input.restartPolicy, - continueFrom: input.continueFrom, + task: config.task, + model: config.model, + cwd: config.cwd, + team: config.team, + agentToken: config.agentToken, + shadowOf: config.shadowOf, + shadowMode: config.shadowMode, + idleThresholdSecs: config.idleThresholdSecs, + restartPolicy: config.restartPolicy, + continueFrom: config.continueFrom, harnessConfig, - skipRelayPrompt: input.skipRelayPrompt, - agentResultSchema: resultContract?.jsonSchema ?? input.agentResultSchema, - }); + skipRelayPrompt: config.skipRelayPrompt, + agentResultSchema: resultContract?.jsonSchema ?? config.agentResultSchema, + }; + + result = + config.runtime === 'headless' + ? await client.spawnHeadless(spawnInput) + : await client.spawnPty(spawnInput); } catch (error) { await this.invokeLifecycleHook( - input.onError, + config.onError, { ...lifecycleContext, error, }, - `${methodName}("${input.name}") onError` + `spawnAgent("${config.name}") onError` ); throw error; } @@ -996,14 +814,14 @@ export class AgentRelay { result ) as Agent; await this.invokeLifecycleHook( - input.onSuccess, + config.onSuccess, { ...lifecycleContext, name: result.name, runtime: result.runtime, sessionId: result.sessionId, }, - `${methodName}("${input.name}") onSuccess` + `spawnAgent("${config.name}") onSuccess` ); return agent; } @@ -2298,55 +2116,6 @@ export class AgentRelay { return agent; } - private createSpawner(cli: string, defaultName: string, runtime: AgentRuntime): AgentSpawner { - return { - spawn: async (options?: SpawnerSpawnOptions) => { - const name = options?.name ?? defaultName; - const channels = options?.channels ?? ['general']; - const args = options?.args ?? []; - - const task = options?.task; - if (runtime === 'pty') { - return this.spawnPty({ - name, - cli, - args, - channels, - task, - model: options?.model, - cwd: options?.cwd, - idleThresholdSecs: options?.idleThresholdSecs, - harnessConfig: options?.harnessConfig, - agentToken: options?.agentToken, - skipRelayPrompt: options?.skipRelayPrompt, - result: options?.result, - onStart: options?.onStart, - onSuccess: options?.onSuccess, - onError: options?.onError, - }); - } - - return this.spawnHeadlessWithLifecycle('spawn', { - name, - cli, - args, - channels, - task, - model: options?.model, - cwd: options?.cwd, - harnessConfig: options?.harnessConfig, - idleThresholdSecs: options?.idleThresholdSecs, - agentToken: options?.agentToken, - skipRelayPrompt: options?.skipRelayPrompt, - result: options?.result, - onStart: options?.onStart, - onSuccess: options?.onSuccess, - onError: options?.onError, - }); - }, - }; - } - private async invokeLifecycleHook( hook: ((context: T) => void | Promise) | undefined, context: T, diff --git a/packages/sdk/src/spawn-from-env.ts b/packages/sdk/src/spawn-from-env.ts index e520f05a3..167583585 100644 --- a/packages/sdk/src/spawn-from-env.ts +++ b/packages/sdk/src/spawn-from-env.ts @@ -221,9 +221,10 @@ export async function spawnFromEnv(options: SpawnFromEnvOptions = {}): Promise Date: Wed, 27 May 2026 14:48:29 +0000 Subject: [PATCH 11/15] style: auto-format with Prettier --- .../completed/2026-05/traj_new9hq87ca49/summary.md | 5 ++++- .../completed/2026-05/traj_new9hq87ca49/trajectory.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md index 032d6b184..9615b8c0c 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md +++ b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md @@ -19,10 +19,12 @@ Simplified the high-level AgentRelay spawn facade to a single overloaded spawnAg ## Key Decisions ### Use AgentRelay.spawnAgent as the only high-level spawn facade + - **Chose:** Use AgentRelay.spawnAgent as the only high-level spawn facade - **Reasoning:** The user asked to simplify the high-level SDK and avoid separate pty/headless/named CLI entry points. A single overloaded spawnAgent(config) keeps one public spelling while preserving runtime-specific typing through a pty/headless config discriminant. ### Recommend narrowing Agent Relay around communication core + - **Chose:** Recommend narrowing Agent Relay around communication core - **Reasoning:** The repo's public promise is real-time agent-to-agent communication, but the default CLI and SDK also expose cloud runtime, proactive agents, drive/relayfile, memory/policy/hooks/trajectory, workflow primitives, GitHub/Slack/browser primitives, personas, web/brand, and multiple bridge surfaces. Keep broker, messaging, spawning, MCP, lifecycle, logs, and minimal SDK as core; move higher-level orchestration and integrations behind extension packages or separate workspaces. @@ -31,7 +33,8 @@ Simplified the high-level AgentRelay spawn facade to a single overloaded spawnAg ## Chapters ### 1. Work -*Agent: default* + +_Agent: default_ - Use AgentRelay.spawnAgent as the only high-level spawn facade: Use AgentRelay.spawnAgent as the only high-level spawn facade - Recommend narrowing Agent Relay around communication core: Recommend narrowing Agent Relay around communication core diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json index 47cd2d4e3..4f483455a 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/trajectory.json @@ -66,4 +66,4 @@ "startRef": "b9a68bbbb2d028b9169422bf1d344f08d009b57a", "endRef": "b9a68bbbb2d028b9169422bf1d344f08d009b57a" } -} \ No newline at end of file +} From f82858623e83678d76a2018e3640bb5ea2b46a9a Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 10:52:46 -0400 Subject: [PATCH 12/15] docs: update spawnAgent examples --- .../2026-05/traj_eowdep73c8oz/summary.md | 15 +++++ .../2026-05/traj_eowdep73c8oz/trajectory.json | 29 ++++++++ README.md | 8 ++- packages/cli/README.md | 8 ++- web/components/SdkCodeExample.tsx | 8 ++- web/content/docs/channels.mdx | 4 +- web/content/docs/event-handlers.mdx | 2 +- web/content/docs/harness-runtime-config.mdx | 7 +- web/content/docs/harnesses.mdx | 18 ++++- web/content/docs/quickstart.mdx | 12 +++- web/content/docs/sending-messages.mdx | 12 +++- web/content/docs/spawning-an-agent.mdx | 56 ++++++++++------ web/content/docs/typescript-sdk.mdx | 66 ++++++++++--------- 13 files changed, 176 insertions(+), 69 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/summary.md new file mode 100644 index 000000000..5d3888644 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/summary.md @@ -0,0 +1,15 @@ +# Trajectory: Update web docs for spawnAgent facade + +> **Status:** ✅ Completed +> **Task:** PR-1003 +> **Confidence:** 90% +> **Started:** May 27, 2026 at 10:47 AM +> **Completed:** May 27, 2026 at 10:52 AM + +--- + +## Summary + +Updated web and README examples to use the TypeScript spawnAgent facade while leaving Python SDK examples on the Python API. Validated formatting, diff whitespace, and web build. + +**Approach:** Standard approach diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json new file mode 100644 index 000000000..c22e7f8f9 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json @@ -0,0 +1,29 @@ +{ + "id": "traj_eowdep73c8oz", + "version": 1, + "task": { + "title": "Update web docs for spawnAgent facade", + "source": { + "system": "plain", + "id": "PR-1003" + } + }, + "status": "completed", + "startedAt": "2026-05-27T14:47:50.892Z", + "completedAt": "2026-05-27T14:52:12.473Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Updated web and README examples to use the TypeScript spawnAgent facade while leaving Python SDK examples on the Python API. Validated formatting, diff whitespace, and web build.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "9adfd4c12bba63a108062d862ae84a8e7e06ef24", + "endRef": "9adfd4c12bba63a108062d862ae84a8e7e06ef24" + } +} \ No newline at end of file diff --git a/README.md b/README.md index e7ea93a5e..091b2d88b 100644 --- a/README.md +++ b/README.md @@ -108,15 +108,19 @@ relay.onMessageReceived = (msg) => { const channels = ['tic-tac-toe']; -const x = await relay.claude.spawn({ +const x = await relay.spawnAgent({ name: 'PlayerX', + cli: 'claude', + runtime: 'pty', model: Models.Claude.SONNET, channels, task: 'Play tic-tac-toe as X against PlayerO. You go first.', }); -const o = await relay.codex.spawn({ +const o = await relay.spawnAgent({ name: 'PlayerO', + cli: 'codex', + runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX_SPARK, channels, task: 'Play tic-tac-toe as O against PlayerX.', diff --git a/packages/cli/README.md b/packages/cli/README.md index 3b21852cb..e1f2fb7f4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -74,15 +74,19 @@ relay.onMessageReceived = (msg) => { const channels = ['tic-tac-toe']; -const x = await relay.claude.spawn({ +const x = await relay.spawnAgent({ name: 'PlayerX', + cli: 'claude', + runtime: 'pty', model: Models.Claude.SONNET, channels, task: 'Play tic-tac-toe as X against PlayerO. You go first.', }); -const o = await relay.codex.spawn({ +const o = await relay.spawnAgent({ name: 'PlayerO', + cli: 'codex', + runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX_SPARK, channels, task: 'Play tic-tac-toe as O against PlayerX.', diff --git a/web/components/SdkCodeExample.tsx b/web/components/SdkCodeExample.tsx index 02165ca94..a2c8fb21e 100644 --- a/web/components/SdkCodeExample.tsx +++ b/web/components/SdkCodeExample.tsx @@ -8,13 +8,17 @@ const TS_CODE = `import { AgentRelay } from "@agent-relay/sdk"; const relay = new AgentRelay({ channels: ["quantum-error-correction"] }); -await relay.claude.spawn({ +await relay.spawnAgent({ name: "Research", + cli: "claude", + runtime: "pty", task: "Discuss quantum error correction approaches with Build.", }); -await relay.codex.spawn({ +await relay.spawnAgent({ name: "Build", + cli: "codex", + runtime: "pty", task: "Debate implementation strategies with Research.", }); diff --git a/web/content/docs/channels.mdx b/web/content/docs/channels.mdx index 63ee95775..17d9bc685 100644 --- a/web/content/docs/channels.mdx +++ b/web/content/docs/channels.mdx @@ -11,8 +11,10 @@ Channels give agents a shared room. They are useful when several workers need th ```typescript TypeScript const relay = new AgentRelay({ channels: ['general'] }); -await relay.codex.spawn({ +await relay.spawnAgent({ name: 'Coder', + cli: 'codex', + runtime: 'pty', channels: ['dev', 'reviews'], task: 'Implement the patch and post updates in the team channels.', }); diff --git a/web/content/docs/event-handlers.mdx b/web/content/docs/event-handlers.mdx index 10520b975..c78756829 100644 --- a/web/content/docs/event-handlers.mdx +++ b/web/content/docs/event-handlers.mdx @@ -227,7 +227,7 @@ Migration rules: - `relay.onXxx = null;` → either call the unsubscribe function returned from `addListener`, or use `relay.removeListener('xxx', handler)`. - `relay.onChannelSubscribed = (agent, channels) => ...` and `relay.onChannelUnsubscribed = ...` now receive a single `{ agent, channels }` object instead of positional args. -Per-call option callbacks like `spawnPty({ onStart, onSuccess, onError })` are unchanged — those are scoped to a single invocation, not global hooks. +Per-call option callbacks like `spawnAgent({ onStart, onSuccess, onError })` are unchanged — those are scoped to a single invocation, not global hooks. ## Good uses for listeners diff --git a/web/content/docs/harness-runtime-config.mdx b/web/content/docs/harness-runtime-config.mdx index e8b91aee8..cb9efe368 100644 --- a/web/content/docs/harness-runtime-config.mdx +++ b/web/content/docs/harness-runtime-config.mdx @@ -149,7 +149,11 @@ Codex session: ```typescript const sessionId = await createCodexSession({ cwd, task }); -await relay.spawn('CodexReviewer', 'codex', task, { +await relay.spawnAgent({ + name: 'CodexReviewer', + cli: 'codex', + runtime: 'pty', + task, harnessConfig: { runtime: 'pty', command: 'codex', @@ -185,4 +189,3 @@ allowlists. The broker rejects `harnessId`. Relaycast spawns that need custom behavior should also send a full inline `harnessConfig`, which keeps each spawn self-contained across local, remote, and multi-broker deployments. - diff --git a/web/content/docs/harnesses.mdx b/web/content/docs/harnesses.mdx index cf18bafb6..c4c9b27a8 100644 --- a/web/content/docs/harnesses.mdx +++ b/web/content/docs/harnesses.mdx @@ -56,7 +56,11 @@ const relay = new AgentRelay({ }, }); -await relay.spawn('ClaudeReviewer', 'company-claude', 'Review the current diff.', { +await relay.spawnAgent({ + name: 'ClaudeReviewer', + cli: 'company-claude', + runtime: 'pty', + task: 'Review the current diff.', model: 'opus', args: ['--verbose'], }); @@ -92,7 +96,11 @@ const cwd = process.cwd(); const task = 'Review the current diff.'; const sessionId = await createCodexSession({ cwd, task }); -await relay.spawn('CodexReviewer', 'codex', task, { +await relay.spawnAgent({ + name: 'CodexReviewer', + cli: 'codex', + runtime: 'pty', + task, cwd, harnessConfig: codexResume(sessionId, cwd), }); @@ -126,7 +134,11 @@ function opencodeSession(input: { const relay = new AgentRelay(); -await relay.spawn('OpenCodeWorker', 'opencode', 'Inspect the repo.', { +await relay.spawnAgent({ + name: 'OpenCodeWorker', + cli: 'opencode', + runtime: 'headless', + task: 'Inspect the repo.', harnessConfig: opencodeSession({ endpoint: 'http://127.0.0.1:4096', sessionId: 'ses_123', diff --git a/web/content/docs/quickstart.mdx b/web/content/docs/quickstart.mdx index 241c384b4..0c30cc1fb 100644 --- a/web/content/docs/quickstart.mdx +++ b/web/content/docs/quickstart.mdx @@ -41,16 +41,22 @@ relay.onMessageReceived = (msg) => { }; // Spawn three agents. -const planner = await relay.claude.spawn({ +const planner = await relay.spawnAgent({ name: 'Planner', + cli: 'claude', + runtime: 'pty', model: Models.Claude.OPUS, }); -const coder = await relay.codex.spawn({ +const coder = await relay.spawnAgent({ name: 'Coder', + cli: 'codex', + runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX, }); -const reviewer = await relay.opencode.spawn({ +const reviewer = await relay.spawnAgent({ name: 'Reviewer', + cli: 'opencode', + runtime: 'headless', model: Models.Opencode.OPENAI_GPT_5_2, }); diff --git a/web/content/docs/sending-messages.mdx b/web/content/docs/sending-messages.mdx index c3f4e6d21..4afeac3e7 100644 --- a/web/content/docs/sending-messages.mdx +++ b/web/content/docs/sending-messages.mdx @@ -9,8 +9,16 @@ Once agents are running, the main control loop is simple: listen for messages, s ```typescript TypeScript -const planner = await relay.claude.spawn({ name: 'Planner' }); -const coder = await relay.codex.spawn({ name: 'Coder' }); +const planner = await relay.spawnAgent({ + name: 'Planner', + cli: 'claude', + runtime: 'pty', +}); +const coder = await relay.spawnAgent({ + name: 'Coder', + cli: 'codex', + runtime: 'pty', +}); await planner.sendMessage({ to: 'Coder', diff --git a/web/content/docs/spawning-an-agent.mdx b/web/content/docs/spawning-an-agent.mdx index 48e9dc1eb..fe04c8608 100644 --- a/web/content/docs/spawning-an-agent.mdx +++ b/web/content/docs/spawning-an-agent.mdx @@ -12,8 +12,10 @@ import { AgentRelay, Models } from '@agent-relay/sdk'; const relay = new AgentRelay({ channels: ['dev'] }); -const planner = await relay.claude.spawn({ +const planner = await relay.spawnAgent({ name: 'Planner', + cli: 'claude', + runtime: 'pty', model: Models.Claude.SONNET, channels: ['dev'], task: 'Break the work into 3 implementation steps.', @@ -36,8 +38,7 @@ planner = await relay.claude.spawn( ``` -Spawned agents are either headless (Claude Code, OpenCode) or PTY-backed (Codex, Gemini all others). Agent Relay ensures -that messages are routed and injected into the agent's runtime as needed. +Spawned agents are either PTY-backed terminal sessions or headless app-server sessions. Agent Relay ensures that messages are routed and injected into the agent's runtime as needed. Agents can also spawn other agents via the CLI or MCP. @@ -49,16 +50,15 @@ Agents can also spawn other agents via the CLI or MCP. const cli = 'claude'; -const reviewer = await relay.spawn( - 'Reviewer', +const reviewer = await relay.spawnAgent({ + name: 'Reviewer', cli, - 'Review the migration plan and list the highest-risk steps.', - { - channels: ['review'], - model: 'sonnet', - cwd: '/repo', - } -); + runtime: 'pty', + task: 'Review the migration plan and list the highest-risk steps.', + channels: ['review'], + model: 'sonnet', + cwd: '/repo', +}); ``` ```python Python file="dynamic_spawn.py" @@ -79,14 +79,30 @@ reviewer = await relay.spawn( ``` -### Named Spawn +### Common configs ```typescript TypeScript file="spawners.ts" -const claudeWorker = await relay.claude.spawn({ name: 'Planner' }); -const codexWorker = await relay.codex.spawn({ name: 'Coder' }); -const geminiWorker = await relay.gemini.spawn({ name: 'Researcher' }); -const opencodeWorker = await relay.opencode.spawn({ name: 'Reviewer' }); +const claudeWorker = await relay.spawnAgent({ + name: 'Planner', + cli: 'claude', + runtime: 'pty', +}); +const codexWorker = await relay.spawnAgent({ + name: 'Coder', + cli: 'codex', + runtime: 'pty', +}); +const geminiWorker = await relay.spawnAgent({ + name: 'Researcher', + cli: 'gemini', + runtime: 'pty', +}); +const opencodeWorker = await relay.spawnAgent({ + name: 'Reviewer', + cli: 'opencode', + runtime: 'headless', +}); ``` ```python Python file="spawners.py" @@ -105,7 +121,7 @@ These options control how the local broker/client is started before any agents a ## Per-agent spawn options -These options are available on the shorthand helpers and on `relay.spawn(...)`: +These options are available on TypeScript `relay.spawnAgent(...)` configs and Python spawn helpers: @@ -121,8 +137,10 @@ const Result = z.object({ notes: z.array(z.string()), }); -const reviewer = await relay.claude.spawn({ +const reviewer = await relay.spawnAgent({ name: 'Reviewer', + cli: 'claude', + runtime: 'pty', task: 'Review the change and submit your decision as structured JSON.', result: { schema: Result }, }); diff --git a/web/content/docs/typescript-sdk.mdx b/web/content/docs/typescript-sdk.mdx index c4ce2808a..a3f5694b7 100644 --- a/web/content/docs/typescript-sdk.mdx +++ b/web/content/docs/typescript-sdk.mdx @@ -37,14 +37,26 @@ const relay = new AgentRelay(options?: AgentRelayOptions); ## Spawning Agents -### Shorthand Spawners +### `relay.spawnAgent(config)` ```typescript -// Spawn by CLI type -const agent = await relay.claude.spawn(options?) -const agent = await relay.codex.spawn(options?) -const agent = await relay.gemini.spawn(options?) -const agent = await relay.opencode.spawn(options?) +const coder = await relay.spawnAgent({ + name: 'Coder', + cli: 'codex', + runtime: 'pty', + model: Models.Codex.GPT_5_3_CODEX, + channels: ['dev'], + task: 'Implement the approved plan.', +}); + +const reviewer = await relay.spawnAgent({ + name: 'Reviewer', + cli: 'opencode', + runtime: 'headless', + model: Models.Opencode.OPENAI_GPT_5_2, + channels: ['reviews'], + task: 'Review the branch and summarize risks.', +}); ``` **Spawn options:** @@ -52,6 +64,8 @@ const agent = await relay.opencode.spawn(options?) | Property | Type | Description | | ----------- | ---------- | ------------------------------------------------ | | `name` | `string` | Agent name (defaults to CLI name) | +| `cli` | `string` | CLI or named harness to spawn | +| `runtime` | `'pty' \| 'headless'` | Runtime category for the agent | | `model` | `string` | Model to use (see Models below) | | `task` | `string` | Initial task / prompt | | `channels` | `string[]` | Channels to join | @@ -61,33 +75,15 @@ const agent = await relay.opencode.spawn(options?) | `onSuccess` | `function` | Sync/async callback after spawn succeeds | | `onError` | `function` | Sync/async callback when spawn fails | -### `relay.spawn(name, cli, task?, options?)` - -Spawn any CLI by name: - -```typescript -const agent = await relay.spawn('Worker', 'claude', 'Help with refactoring', { - model: Models.Claude.SONNET, - channels: ['team'], -}); -``` - -### `relay.spawnAndWait(name, cli, task, options?)` +The returned `Agent` handle exposes readiness, idle, exit, output, messaging, release, and structured result helpers. To wait for the agent to become ready before continuing, call `await agent.waitForReady(timeoutMs)`. -Spawn and wait for the agent to be ready before returning: - -```typescript -const agent = await relay.spawnAndWait('Worker', 'claude', 'Analyze the codebase', { - timeoutMs: 30000, - waitForMessage: false, // true = wait for first message, false = wait for process ready -}); -``` +`runtime: 'pty'` is for terminal-backed CLIs such as Claude Code, Codex, and Gemini. `runtime: 'headless'` is for app-server sessions such as OpenCode. --- ## Agent -All spawn methods return an `Agent`: +`spawnAgent` returns an `Agent`: ```typescript interface Agent { @@ -131,8 +127,10 @@ const Summary = z.object({ findings: z.array(z.string()), }); -const reviewer = await relay.claude.spawn({ +const reviewer = await relay.spawnAgent({ name: 'Reviewer', + cli: 'claude', + runtime: 'pty', task: 'Review the branch and submit the final JSON result.', result: { schema: Summary, @@ -197,7 +195,7 @@ const client = await AgentRelayClient.spawn({ cwd: '/my/project' }); | Method | Broker route | Description | | ------ | ------------ | ----------- | | `client.spawnPty(input)` | `POST /api/spawn` | Spawn a PTY-backed worker. | -| `client.spawnProvider(input)` | `POST /api/spawn` | Spawn by provider and transport. | +| `client.spawnCli(input)` | `POST /api/spawn` | Spawn by CLI and transport. | | `client.spawnClaude(input)` | `POST /api/spawn` | Spawn a Claude worker. | | `client.spawnOpencode(input)` | `POST /api/spawn` | Spawn an OpenCode worker. | | `client.release(name, reason?)` | `DELETE /api/spawned/{name}` | Release a worker. | @@ -411,14 +409,18 @@ relay.addListener('agentSpawned', (agent) => { }); // Spawn agents -const planner = await relay.claude.spawn({ +const planner = await relay.spawnAgent({ name: 'Planner', + cli: 'claude', + runtime: 'pty', model: Models.Claude.OPUS, task: 'Plan the feature implementation', }); -const coder = await relay.codex.spawn({ +const coder = await relay.spawnAgent({ name: 'Coder', + cli: 'codex', + runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX, task: 'Implement the plan', }); @@ -469,7 +471,7 @@ Models.Opencode.OPENCODE_GPT_5_NANO; // 'opencode/gpt-5-nano' import { AgentRelayProtocolError, AgentRelayProcessError } from '@agent-relay/sdk'; try { - await relay.claude.spawn({ name: 'Worker' }); + await relay.spawnAgent({ name: 'Worker', cli: 'claude', runtime: 'pty' }); } catch (err) { if (err instanceof AgentRelayProtocolError) { // Broker returned an error response (err.code available) From c937deda336623cdf9d2a6aee50b8af243c21b95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 14:58:40 +0000 Subject: [PATCH 13/15] style: auto-format with Prettier --- .../completed/2026-05/traj_eowdep73c8oz/trajectory.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json index c22e7f8f9..f1a2943b0 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-05/traj_eowdep73c8oz/trajectory.json @@ -26,4 +26,4 @@ "startRef": "9adfd4c12bba63a108062d862ae84a8e7e06ef24", "endRef": "9adfd4c12bba63a108062d862ae84a8e7e06ef24" } -} \ No newline at end of file +} From d58f5d1f875c59d60c13a6bf6a02329baa013b13 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 27 May 2026 11:03:46 -0400 Subject: [PATCH 14/15] feat(sdk): default spawnAgent name and runtime --- .../2026-05/traj_ydxl2ktml22s/summary.md | 15 ++++++ .../2026-05/traj_ydxl2ktml22s/trajectory.json | 29 ++++++++++ CHANGELOG.md | 8 +-- README.md | 2 - packages/cli/README.md | 2 - packages/sdk/README.md | 4 +- .../__tests__/orchestration-upgrades.test.ts | 25 +++++++++ .../sdk/src/__tests__/spawn-token.test.ts | 12 +++-- packages/sdk/src/examples/demo.ts | 2 - packages/sdk/src/examples/quickstart.ts | 7 ++- packages/sdk/src/examples/ralph-loop.ts | 2 - packages/sdk/src/relay.ts | 54 ++++++++++++++----- packages/sdk/src/spawn-from-env.ts | 1 - web/components/SdkCodeExample.tsx | 2 - web/content/docs/channels.mdx | 1 - web/content/docs/harness-runtime-config.mdx | 1 - web/content/docs/harnesses.mdx | 2 - web/content/docs/quickstart.mdx | 2 - web/content/docs/sending-messages.mdx | 2 - web/content/docs/spawning-an-agent.mdx | 22 ++------ web/content/docs/typescript-sdk.mdx | 15 ++---- web/lib/spawn-options-table.ts | 16 +++++- 22 files changed, 150 insertions(+), 76 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/summary.md create mode 100644 .agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/summary.md b/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/summary.md new file mode 100644 index 000000000..91c5c301c --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/summary.md @@ -0,0 +1,15 @@ +# Trajectory: Default spawnAgent name and runtime + +> **Status:** ✅ Completed +> **Task:** PR-1003 +> **Confidence:** 90% +> **Started:** May 27, 2026 at 10:55 AM +> **Completed:** May 27, 2026 at 11:03 AM + +--- + +## Summary + +Made AgentRelay.spawnAgent default name from cli and default runtime to pty, updated docs/examples/changelog, and validated SDK plus web builds. + +**Approach:** Standard approach diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json new file mode 100644 index 000000000..609a08178 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json @@ -0,0 +1,29 @@ +{ + "id": "traj_ydxl2ktml22s", + "version": 1, + "task": { + "title": "Default spawnAgent name and runtime", + "source": { + "system": "plain", + "id": "PR-1003" + } + }, + "status": "completed", + "startedAt": "2026-05-27T14:55:27.608Z", + "completedAt": "2026-05-27T15:03:14.940Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Made AgentRelay.spawnAgent default name from cli and default runtime to pty, updated docs/examples/changelog, and validated SDK plus web builds.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "f82858623e83678d76a2018e3640bb5ea2b46a9a", + "endRef": "f82858623e83678d76a2018e3640bb5ea2b46a9a" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf777537..d99f28ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `@agent-relay/sdk` swaps `@agentworkforce/harness-kit` + `@agentworkforce/workload-router` for `@agentworkforce/persona-kit@^3`. The persona tier system, the `tier` option on `spawnPersona`, the legacy relay-side `PersonaFile` / `PersonaTier` / `PersonaTierSpec` / `ResolvedPersona` / `PersonaSpawnSpec` / `MaterializedConfigFile` types, and the `buildPersonaSpawnSpec` / `materializePersonaConfigFiles` / `restorePersonaConfigFiles` helpers are removed. `loadPersona` now returns the canonical `PersonaSpec`, and `spawnPersona({ persona })` takes a `PersonaSpec` instead of a resolved persona. - `@agent-relay/sdk` removes persona support from the SDK surface: the `./personas` subpath, persona helper/type exports, `AgentRelay.spawnPersona()`, `AgentRelay.getPersonaSpawnPlan()`, and `AgentRelayOptions.personaDirs` are gone. The SDK no longer depends on `@agentworkforce/persona-kit`. - `@agent-relay/sdk` renames the raw client spawn surface from provider terminology to CLI terminology: `AgentRelayClient.spawnProvider()` is now `spawnCli()`, `SpawnProviderInput` is now `SpawnCliInput`, and `SpawnHeadlessInput.provider` is now `SpawnHeadlessInput.cli`. -- `@agent-relay/sdk` removes the high-level `AgentRelay.spawnPty()`, `AgentRelay.spawnHeadless()`, positional `AgentRelay.spawn()`, `AgentRelay.spawnAndWait()`, and shorthand CLI spawners such as `relay.claude.spawn()`. Use `AgentRelay.spawnAgent({ runtime: "pty" | "headless", cli, ... })`. +- `@agent-relay/sdk` removes the high-level `AgentRelay.spawnPty()`, `AgentRelay.spawnHeadless()`, positional `AgentRelay.spawn()`, `AgentRelay.spawnAndWait()`, and shorthand CLI spawners such as `relay.claude.spawn()`. Use `AgentRelay.spawnAgent({ cli, ... })`; `runtime` defaults to `"pty"` and `name` defaults from `cli`. - `agent-relay-broker`'s public Rust protocol types now require typed ID newtypes (`WorkerName`, `DeliveryId`, `EventId`, `WorkspaceId`, `WorkspaceAlias`, `ThreadId`, `AgentId`, `RequestId`, `ChannelName`, `MessageTarget`) on every protocol struct and enum variant in `protocol.rs`, `types.rs`, and `listen_api.rs::ListenApiRequest`. The new wrappers live in `crates/broker/src/lib.rs` under `pub mod ids`. JSON wire format is unchanged because every wrapper is `#[serde(transparent)]`, so the broker ↔ SDK channel and on-disk persisted state remain byte-compatible. - `agent-relay spawn` and SDK spawn calls now return harness `sessionId` metadata for resumable Claude and Codex PTY sessions. - `sdk-swift`: renamed the broker client class `RelayCast` → `AgentRelayClient`. @@ -39,9 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Migration Guidance - Personas relying on `tiers.*` need to be flattened to a single top-level `harness` / `model` / `systemPrompt`. The shape that persona-kit (and the `agentworkforce` CLI) consumes is now the only supported shape. -- Callers that previously used `spawnPersona` to "just launch the harness" — without persona-kit's skill / mount / sidecar side effects — should use `AgentRelay.getPersonaSpawnPlan(id)` to inspect the plan and call `spawnAgent({ runtime: "pty", ... })` with the plan's `cli` + `args` themselves. -- Launch personas through the owning CLI or package and pass the resulting command to `relay.spawnAgent({ runtime: "pty", ... })` or `relay.spawnAgent({ runtime: "headless", ... })`; for AgentWorkforce personas, use `npx agentworkforce persona run ` once available so persona side effects remain CLI-owned. -- Replace high-level `relay.spawnPty(...)`, `relay.spawnHeadless(...)`, `relay.spawn(...)`, `relay.spawnAndWait(...)`, and `relay..spawn(...)` calls with `relay.spawnAgent({ runtime: "pty" | "headless", cli, ... })`; wait explicitly with the returned agent handle when needed. +- Callers that previously used `spawnPersona` to "just launch the harness" — without persona-kit's skill / mount / sidecar side effects — should use `AgentRelay.getPersonaSpawnPlan(id)` to inspect the plan and call `spawnAgent({ cli, args })` themselves. +- Launch personas through the owning CLI or package and pass the resulting command to `relay.spawnAgent({ cli, ... })` or `relay.spawnAgent({ runtime: "headless", cli, ... })`; for AgentWorkforce personas, use `npx agentworkforce persona run ` once available so persona side effects remain CLI-owned. +- Replace high-level `relay.spawnPty(...)`, `relay.spawnHeadless(...)`, `relay.spawn(...)`, `relay.spawnAndWait(...)`, and `relay..spawn(...)` calls with `relay.spawnAgent({ cli, ... })`; add `runtime: "headless"` only for headless app-server sessions, and wait explicitly with the returned agent handle when needed. - Replace `client.spawnProvider({ provider, ... })` with `client.spawnCli({ cli, ... })`; replace `client.spawnHeadless({ provider, ... })` with `client.spawnHeadless({ cli, ... })`. - Downstream Rust callers must construct identifiers via `relay_broker::ids::{WorkerName, DeliveryId, EventId, MessageTarget, …}` instead of `String`. Each newtype impls `From` / `From<&str>` and `Deref`, so most string-handling code keeps compiling; only construction sites (`HashMap` keys, struct literals, channel sends) need updates. - Replace ad-hoc target discrimination (`target.starts_with('#')`, `target == "thread"`) with `MessageTarget::kind()` and match on `MessageTargetKind::{Channel, Thread, DirectMessage, Conversation, Worker}`. diff --git a/README.md b/README.md index 091b2d88b..129f8453b 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,6 @@ const channels = ['tic-tac-toe']; const x = await relay.spawnAgent({ name: 'PlayerX', cli: 'claude', - runtime: 'pty', model: Models.Claude.SONNET, channels, task: 'Play tic-tac-toe as X against PlayerO. You go first.', @@ -120,7 +119,6 @@ const x = await relay.spawnAgent({ const o = await relay.spawnAgent({ name: 'PlayerO', cli: 'codex', - runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX_SPARK, channels, task: 'Play tic-tac-toe as O against PlayerX.', diff --git a/packages/cli/README.md b/packages/cli/README.md index e1f2fb7f4..3c76035fe 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -77,7 +77,6 @@ const channels = ['tic-tac-toe']; const x = await relay.spawnAgent({ name: 'PlayerX', cli: 'claude', - runtime: 'pty', model: Models.Claude.SONNET, channels, task: 'Play tic-tac-toe as X against PlayerO. You go first.', @@ -86,7 +85,6 @@ const x = await relay.spawnAgent({ const o = await relay.spawnAgent({ name: 'PlayerO', cli: 'codex', - runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX_SPARK, channels, task: 'Play tic-tac-toe as O against PlayerX.', diff --git a/packages/sdk/README.md b/packages/sdk/README.md index d9853f083..22bd866d6 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -46,7 +46,6 @@ relay.onAgentActivityChanged = ({ name, active, pendingDeliveries }) => { const worker = await relay.spawnAgent({ name: 'Worker1', cli: 'claude', - runtime: 'pty', channels: ['general'], // Lifecycle hooks can be sync or async functions. onStart: ({ name }) => console.log(`spawning ${name}`), @@ -57,12 +56,13 @@ const worker = await relay.spawnAgent({ const agent = await relay.spawnAgent({ name: 'Worker2', cli: 'codex', - runtime: 'pty', task: 'Build the API', channels: ['dev'], model: 'gpt-4o', }); +const codex = await relay.spawnAgent({ cli: 'codex' }); // name: Codex, runtime: pty + // Headless harnesses keep the same Agent handle surface const reviewer = await relay.spawnAgent({ name: 'HeadlessReviewer', diff --git a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts index 61b6cb9a9..3edd63199 100644 --- a/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +++ b/packages/sdk/src/__tests__/orchestration-upgrades.test.ts @@ -466,6 +466,31 @@ describe('AgentRelayClient orchestration payloads', () => { }); describe('AgentRelay orchestration handles', () => { + it('spawnAgent defaults missing name and runtime to a PTY spawn', async () => { + const { client, mock } = createMockFacadeClient(); + const relay = createWiredRelay(client); + + try { + const agent = await relay.spawnAgent({ + cli: 'codex', + channels: ['general'], + }); + + expect(agent.name).toBe('Codex'); + expect(agent.runtime).toBe('pty'); + expect(mock.spawnPty).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'Codex', + cli: 'codex', + channels: ['general'], + }) + ); + expect(mock.spawnHeadless).not.toHaveBeenCalled(); + } finally { + await relay.shutdown(); + } + }); + it('spawnAgent forwards pty agentToken to the client', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); diff --git a/packages/sdk/src/__tests__/spawn-token.test.ts b/packages/sdk/src/__tests__/spawn-token.test.ts index 10c79a1b9..b75e8871a 100644 --- a/packages/sdk/src/__tests__/spawn-token.test.ts +++ b/packages/sdk/src/__tests__/spawn-token.test.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest'; -import type { SpawnAgentConfig, SpawnHeadlessAgentConfig } from '../relay.js'; +import type { SpawnAgentConfig, SpawnHeadlessAgentConfig, SpawnPtyAgentConfig } from '../relay.js'; import type { SpawnCliInput, SpawnHeadlessInput, SpawnPtyInput } from '../types.js'; describe('spawn input agentToken types', () => { @@ -58,9 +58,15 @@ describe('spawn input agentToken types', () => { expectTypeOf().toEqualTypeOf(); }); - it('SpawnAgentConfig uses runtime and cli for the high-level facade', () => { + it('SpawnAgentConfig defaults name and runtime for the high-level facade', () => { + const minimalPty = { + cli: 'codex', + } satisfies SpawnPtyAgentConfig; + + expectTypeOf(minimalPty).toMatchTypeOf(); + expectTypeOf(minimalPty).toMatchTypeOf(); + const withHeadlessHarness = { - name: 'headless-facade', cli: 'custom-app-server', runtime: 'headless', agentToken: 'jwt-token', diff --git a/packages/sdk/src/examples/demo.ts b/packages/sdk/src/examples/demo.ts index a8b0bbf60..15c5bb136 100644 --- a/packages/sdk/src/examples/demo.ts +++ b/packages/sdk/src/examples/demo.ts @@ -39,14 +39,12 @@ const [agentA, agentB] = await Promise.all([ relay.spawnAgent({ name: 'AgentA', cli: 'claude', - runtime: 'pty', args: ['--print'], channels: ['general'], }), relay.spawnAgent({ name: 'AgentB', cli: 'claude', - runtime: 'pty', args: ['--print'], channels: ['general'], }), diff --git a/packages/sdk/src/examples/quickstart.ts b/packages/sdk/src/examples/quickstart.ts index b52168c8a..0a27fe716 100644 --- a/packages/sdk/src/examples/quickstart.ts +++ b/packages/sdk/src/examples/quickstart.ts @@ -39,9 +39,9 @@ relay.addListener('agentExited', (agent) => { // ── Create agents with sane defaults, running locally ─────────────────────── const [codex, claude, gemini] = await Promise.all([ - relay.spawnAgent({ name: 'Codex', cli: 'codex', runtime: 'pty' }), - relay.spawnAgent({ name: 'Claude', cli: 'claude', runtime: 'pty' }), - relay.spawnAgent({ name: 'Gemini', cli: 'gemini', runtime: 'pty' }), + relay.spawnAgent({ cli: 'codex' }), + relay.spawnAgent({ cli: 'claude' }), + relay.spawnAgent({ cli: 'gemini' }), ]); // ── Configure messaging with custom CLI agents ───────────────────────────── @@ -49,7 +49,6 @@ const [codex, claude, gemini] = await Promise.all([ const worker1 = await relay.spawnAgent({ name: 'Worker1', cli: 'codex', - runtime: 'pty', args: ['--model', 'gpt-5'], channels: ['general'], }); diff --git a/packages/sdk/src/examples/ralph-loop.ts b/packages/sdk/src/examples/ralph-loop.ts index 459b85941..12477a90a 100644 --- a/packages/sdk/src/examples/ralph-loop.ts +++ b/packages/sdk/src/examples/ralph-loop.ts @@ -215,13 +215,11 @@ while (iteration < MAX_ITERATIONS) { relay.spawnAgent({ name: `Architect-${story.id}-${roundLabel}`, cli: 'claude', - runtime: 'pty', channels: ['general'], }), relay.spawnAgent({ name: `Builder-${story.id}-${roundLabel}`, cli: 'codex', - runtime: 'pty', args: ['--full-auto'], channels: ['general'], }), diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index d26b05915..2643c760a 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -13,7 +13,7 @@ * relay.addListener('messageReceived', (message) => console.log(message)); * relay.addListener('agentSpawned', (agent) => console.log("spawned", agent.name)); * - * const codex = await relay.spawnAgent({ name: "Codex", cli: "codex", runtime: "pty" }); + * const codex = await relay.spawnAgent({ cli: "codex" }); * const human = relay.human({ name: "System" }); * await human.sendMessage({ to: codex.name, text: "Hello!" }); * @@ -123,6 +123,32 @@ function generateWorkspaceId(): string { return `${WORKSPACE_ID_PREFIX}${suffix}`; } +const DEFAULT_AGENT_NAMES: Record = { + aider: 'Aider', + agent: 'Agent', + claude: 'Claude', + codex: 'Codex', + cursor: 'Cursor', + 'cursor-agent': 'CursorAgent', + droid: 'Droid', + gemini: 'Gemini', + goose: 'Goose', + opencode: 'OpenCode', +}; + +function defaultAgentNameForCli(cli: string): string { + const trimmed = cli.trim(); + const firstToken = trimmed.split(/\s+/)[0] ?? trimmed; + const base = firstToken.split(':')[0] ?? firstToken; + const knownName = DEFAULT_AGENT_NAMES[base.toLowerCase()]; + if (knownName) return knownName; + + const parts = base.split(/[^a-zA-Z0-9]+/).filter((part) => part.length > 0); + if (parts.length === 0) return 'Agent'; + + return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(''); +} + function toWorkspaceRegistryEntry(value: unknown): WorkspaceRegistryEntry { if (!value || typeof value !== 'object') { return {}; @@ -201,12 +227,12 @@ type SpawnWithLifecycle = TInput & SpawnLifecycleHooks & { result?: AgentResultOptions }; export type SpawnPtyAgentConfig = SpawnWithLifecycle< - SpawnPtyInput & { runtime: 'pty' }, + Omit & { name?: string; runtime?: 'pty' }, TAgentResult >; export type SpawnHeadlessAgentConfig = SpawnWithLifecycle< - ClientSpawnHeadlessInput & { runtime: 'headless' }, + Omit & { name?: string; runtime: 'headless' }, TAgentResult >; @@ -742,25 +768,29 @@ export class AgentRelay { config: SpawnAgentConfig ): Promise> { const client = await this.ensureStarted(); + const name = config.name && config.name.trim() ? config.name : defaultAgentNameForCli(config.cli); + const runtime = config.runtime ?? 'pty'; + const spawnOperation = `spawnAgent("${name}")`; + if (!config.channels || config.channels.length === 0) { console.warn( - `[AgentRelay] spawnAgent("${config.name}"): no channels specified, defaulting to "general". ` + + `[AgentRelay] ${spawnOperation}: no channels specified, defaulting to "general". ` + 'Set explicit channels for workflow isolation.' ); } const channels = config.channels ?? ['general']; const lifecycleContext: SpawnLifecycleContext = { - name: config.name, + name, cli: config.cli, channels, task: config.task, }; - await this.invokeLifecycleHook(config.onStart, lifecycleContext, `spawnAgent("${config.name}") onStart`); + await this.invokeLifecycleHook(config.onStart, lifecycleContext, `${spawnOperation} onStart`); let result: SpawnAgentResult; const resultContract = this.prepareAgentResultContract(config.result); try { const harnessConfig = this.resolveHarnessConfig({ - name: config.name, + name, cli: config.cli, args: config.args, task: config.task, @@ -769,7 +799,7 @@ export class AgentRelay { harnessConfig: config.harnessConfig, }); const spawnInput = { - name: config.name, + name, cli: config.cli, args: config.args, channels, @@ -789,9 +819,7 @@ export class AgentRelay { }; result = - config.runtime === 'headless' - ? await client.spawnHeadless(spawnInput) - : await client.spawnPty(spawnInput); + runtime === 'headless' ? await client.spawnHeadless(spawnInput) : await client.spawnPty(spawnInput); } catch (error) { await this.invokeLifecycleHook( config.onError, @@ -799,7 +827,7 @@ export class AgentRelay { ...lifecycleContext, error, }, - `spawnAgent("${config.name}") onError` + `${spawnOperation} onError` ); throw error; } @@ -821,7 +849,7 @@ export class AgentRelay { runtime: result.runtime, sessionId: result.sessionId, }, - `spawnAgent("${config.name}") onSuccess` + `${spawnOperation} onSuccess` ); return agent; } diff --git a/packages/sdk/src/spawn-from-env.ts b/packages/sdk/src/spawn-from-env.ts index 167583585..880f70c75 100644 --- a/packages/sdk/src/spawn-from-env.ts +++ b/packages/sdk/src/spawn-from-env.ts @@ -224,7 +224,6 @@ export async function spawnFromEnv(options: SpawnFromEnvOptions = {}): Promise { const planner = await relay.spawnAgent({ name: 'Planner', cli: 'claude', - runtime: 'pty', model: Models.Claude.OPUS, }); const coder = await relay.spawnAgent({ name: 'Coder', cli: 'codex', - runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX, }); const reviewer = await relay.spawnAgent({ diff --git a/web/content/docs/sending-messages.mdx b/web/content/docs/sending-messages.mdx index 4afeac3e7..bd279f15a 100644 --- a/web/content/docs/sending-messages.mdx +++ b/web/content/docs/sending-messages.mdx @@ -12,12 +12,10 @@ Once agents are running, the main control loop is simple: listen for messages, s const planner = await relay.spawnAgent({ name: 'Planner', cli: 'claude', - runtime: 'pty', }); const coder = await relay.spawnAgent({ name: 'Coder', cli: 'codex', - runtime: 'pty', }); await planner.sendMessage({ diff --git a/web/content/docs/spawning-an-agent.mdx b/web/content/docs/spawning-an-agent.mdx index fe04c8608..56e86a68b 100644 --- a/web/content/docs/spawning-an-agent.mdx +++ b/web/content/docs/spawning-an-agent.mdx @@ -15,7 +15,6 @@ const relay = new AgentRelay({ channels: ['dev'] }); const planner = await relay.spawnAgent({ name: 'Planner', cli: 'claude', - runtime: 'pty', model: Models.Claude.SONNET, channels: ['dev'], task: 'Break the work into 3 implementation steps.', @@ -53,7 +52,6 @@ const cli = 'claude'; const reviewer = await relay.spawnAgent({ name: 'Reviewer', cli, - runtime: 'pty', task: 'Review the migration plan and list the highest-risk steps.', channels: ['review'], model: 'sonnet', @@ -83,23 +81,10 @@ reviewer = await relay.spawn( ```typescript TypeScript file="spawners.ts" -const claudeWorker = await relay.spawnAgent({ - name: 'Planner', - cli: 'claude', - runtime: 'pty', -}); -const codexWorker = await relay.spawnAgent({ - name: 'Coder', - cli: 'codex', - runtime: 'pty', -}); -const geminiWorker = await relay.spawnAgent({ - name: 'Researcher', - cli: 'gemini', - runtime: 'pty', -}); +const claudeWorker = await relay.spawnAgent({ cli: 'claude' }); +const codexWorker = await relay.spawnAgent({ cli: 'codex' }); +const geminiWorker = await relay.spawnAgent({ cli: 'gemini' }); const opencodeWorker = await relay.spawnAgent({ - name: 'Reviewer', cli: 'opencode', runtime: 'headless', }); @@ -140,7 +125,6 @@ const Result = z.object({ const reviewer = await relay.spawnAgent({ name: 'Reviewer', cli: 'claude', - runtime: 'pty', task: 'Review the change and submit your decision as structured JSON.', result: { schema: Result }, }); diff --git a/web/content/docs/typescript-sdk.mdx b/web/content/docs/typescript-sdk.mdx index a3f5694b7..3bfaab5e8 100644 --- a/web/content/docs/typescript-sdk.mdx +++ b/web/content/docs/typescript-sdk.mdx @@ -40,10 +40,8 @@ const relay = new AgentRelay(options?: AgentRelayOptions); ### `relay.spawnAgent(config)` ```typescript -const coder = await relay.spawnAgent({ - name: 'Coder', +const codex = await relay.spawnAgent({ cli: 'codex', - runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX, channels: ['dev'], task: 'Implement the approved plan.', @@ -63,9 +61,9 @@ const reviewer = await relay.spawnAgent({ | Property | Type | Description | | ----------- | ---------- | ------------------------------------------------ | -| `name` | `string` | Agent name (defaults to CLI name) | +| `name` | `string` | Agent name (defaults from `cli`, e.g. `Codex`) | | `cli` | `string` | CLI or named harness to spawn | -| `runtime` | `'pty' \| 'headless'` | Runtime category for the agent | +| `runtime` | `'pty' \| 'headless'` | Runtime category for the agent (default: `'pty'`) | | `model` | `string` | Model to use (see Models below) | | `task` | `string` | Initial task / prompt | | `channels` | `string[]` | Channels to join | @@ -77,7 +75,7 @@ const reviewer = await relay.spawnAgent({ The returned `Agent` handle exposes readiness, idle, exit, output, messaging, release, and structured result helpers. To wait for the agent to become ready before continuing, call `await agent.waitForReady(timeoutMs)`. -`runtime: 'pty'` is for terminal-backed CLIs such as Claude Code, Codex, and Gemini. `runtime: 'headless'` is for app-server sessions such as OpenCode. +Omit `runtime` for terminal-backed CLIs such as Claude Code, Codex, and Gemini. Use `runtime: 'headless'` for app-server sessions such as OpenCode. --- @@ -130,7 +128,6 @@ const Summary = z.object({ const reviewer = await relay.spawnAgent({ name: 'Reviewer', cli: 'claude', - runtime: 'pty', task: 'Review the branch and submit the final JSON result.', result: { schema: Summary, @@ -412,7 +409,6 @@ relay.addListener('agentSpawned', (agent) => { const planner = await relay.spawnAgent({ name: 'Planner', cli: 'claude', - runtime: 'pty', model: Models.Claude.OPUS, task: 'Plan the feature implementation', }); @@ -420,7 +416,6 @@ const planner = await relay.spawnAgent({ const coder = await relay.spawnAgent({ name: 'Coder', cli: 'codex', - runtime: 'pty', model: Models.Codex.GPT_5_3_CODEX, task: 'Implement the plan', }); @@ -471,7 +466,7 @@ Models.Opencode.OPENCODE_GPT_5_NANO; // 'opencode/gpt-5-nano' import { AgentRelayProtocolError, AgentRelayProcessError } from '@agent-relay/sdk'; try { - await relay.spawnAgent({ name: 'Worker', cli: 'claude', runtime: 'pty' }); + await relay.spawnAgent({ cli: 'claude' }); } catch (err) { if (err instanceof AgentRelayProtocolError) { // Broker returned an error response (err.code available) diff --git a/web/lib/spawn-options-table.ts b/web/lib/spawn-options-table.ts index 423404c10..df4613329 100644 --- a/web/lib/spawn-options-table.ts +++ b/web/lib/spawn-options-table.ts @@ -60,8 +60,20 @@ const RELAY_STARTUP_ROWS: SpawnOptionRow[] = [ ]; const COMMON_ROWS: SpawnOptionRow[] = [ - { typescript: ['name'], python: ['name'], description: 'Stable identity other agents can message' }, - { typescript: ['model'], python: ['model'], description: 'Model string or enum for that provider' }, + { + typescript: ['name'], + python: ['name'], + description: { + typescript: 'Stable identity other agents can message. Defaults from `cli`.', + python: 'Stable identity other agents can message', + }, + }, + { typescript: ['cli'], description: 'CLI or named harness to spawn' }, + { + typescript: ['runtime'], + description: 'Runtime category. Defaults to `pty`; set `headless` for app-server sessions', + }, + { typescript: ['model'], python: ['model'], description: 'Model string or enum for the selected CLI' }, { typescript: ['task'], python: ['task'], description: 'Initial prompt for autonomous startup' }, { typescript: ['channels'], python: ['channels'], description: 'Rooms the agent joins on spawn' }, { typescript: ['args'], python: ['args'], description: 'Extra CLI arguments' }, From bc0a2357d566b8f20e1ce58cb70550b14e640134 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 15:05:44 +0000 Subject: [PATCH 15/15] style: auto-format with Prettier --- .../completed/2026-05/traj_ydxl2ktml22s/trajectory.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json b/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json index 609a08178..c6e8cd279 100644 --- a/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ydxl2ktml22s/trajectory.json @@ -26,4 +26,4 @@ "startRef": "f82858623e83678d76a2018e3640bb5ea2b46a9a", "endRef": "f82858623e83678d76a2018e3640bb5ea2b46a9a" } -} \ No newline at end of file +}