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..514069aeb --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_0kqt1gnfi3v8/summary.md @@ -0,0 +1,34 @@ +# 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..68b794114 --- /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" + } +} 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..e33267802 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md @@ -0,0 +1,33 @@ +# 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..95530dee3 --- /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" + } +} 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..5ac14b2a3 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md @@ -0,0 +1,33 @@ +# 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..fcf35cbb2 --- /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" + } +} 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..a02946b05 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_ei1zajpyq584/summary.md @@ -0,0 +1,33 @@ +# 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..69ff56b1a --- /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" + } +} 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..f1a2943b0 --- /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" + } +} 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..9615b8c0c --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-05/traj_new9hq87ca49/summary.md @@ -0,0 +1,40 @@ +# 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..4f483455a --- /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" + } +} 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..c6e8cd279 --- /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" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e5d0e6f..d99f28ce9 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.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 @@ -29,6 +30,8 @@ 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({ 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`. @@ -36,8 +39,10 @@ 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 provider 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({ 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}`. - `sdk-swift`: replace `RelayCast(apiKey:baseURL:)` with `AgentRelayClient(apiKey:baseURL:)`. The public API surface is otherwise unchanged. diff --git a/README.md b/README.md index e7ea93a5e..129f8453b 100644 --- a/README.md +++ b/README.md @@ -108,15 +108,17 @@ relay.onMessageReceived = (msg) => { const channels = ['tic-tac-toe']; -const x = await relay.claude.spawn({ +const x = await relay.spawnAgent({ name: 'PlayerX', + cli: 'claude', 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', 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..3c76035fe 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -74,15 +74,17 @@ relay.onMessageReceived = (msg) => { const channels = ['tic-tac-toe']; -const x = await relay.claude.spawn({ +const x = await relay.spawnAgent({ name: 'PlayerX', + cli: 'claude', 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', model: Models.Codex.GPT_5_3_CODEX_SPARK, channels, task: 'Play tic-tac-toe as O against PlayerX.', 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 92ee91b6c..22bd866d6 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -42,9 +42,10 @@ 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', channels: ['general'], // Lifecycle hooks can be sync or async functions. onStart: ({ name }) => console.log(`spawning ${name}`), @@ -52,12 +53,25 @@ 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', + 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', + cli: 'opencode', + runtime: 'headless', + channels: ['reviews'], + task: 'Review the current branch', +}); + // Wait for agent to finish (go idle or exit) const result = await agent.waitForIdle(120_000); @@ -128,9 +142,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'; @@ -160,10 +174,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__/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__/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..6881c3ccd 100644 --- a/packages/sdk/src/__tests__/lifecycle-hooks.test.ts +++ b/packages/sdk/src/__tests__/lifecycle-hooks.test.ts @@ -210,23 +210,25 @@ 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(); - 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.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(after).toHaveBeenCalledTimes(1); - expect((after.mock.calls[0][0] as AfterAgentSpawnContext).kind).toBe('provider'); + expect(beforeKinds).toEqual(['cli']); + expect(afterKinds).toEqual(['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 +242,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 02a524691..3edd63199 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 }) => ({ + spawnCli: vi.fn(async (input: { name: string }) => ({ + name: input.name, + runtime: 'headless' as const, + })), + spawnHeadless: vi.fn(async (input: { name: string }) => ({ name: input.name, runtime: 'headless' as const, })), @@ -194,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') @@ -202,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', @@ -462,12 +466,38 @@ describe('AgentRelayClient orchestration payloads', () => { }); describe('AgentRelay orchestration handles', () => { - it('spawnPty forwards agentToken to the client', async () => { + it('spawnAgent defaults missing name and runtime to a PTY spawn', async () => { const { client, mock } = createMockFacadeClient(); const relay = createWiredRelay(client); try { - await relay.spawnPty({ + 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); + + try { + await relay.spawnAgent({ + runtime: 'pty', name: 'token-pty', cli: 'claude', channels: ['general'], @@ -486,12 +516,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', }); @@ -508,18 +542,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', }); @@ -531,11 +569,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', + cli: 'opencode', agentToken: 'agent-token-opencode', }) ); @@ -544,14 +581,112 @@ describe('AgentRelay orchestration handles', () => { } }); + it('spawnAgent 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.spawnAgent<{ ok: boolean }>({ + runtime: 'headless', + name: 'headless-facade', + cli: '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', + channels: ['reviews'], + task: 'Review the change', + }) + ); + expect(onSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'headless-facade', + cli: 'custom-app', + runtime: 'headless', + }) + ); + expect(mock.spawnHeadless).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'headless-facade', + cli: 'custom-app', + channels: ['reviews'], + task: 'Review the change', + harnessConfig, + agentResultSchema: jsonSchema, + }) + ); + } finally { + await relay.shutdown(); + } + }); + + 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; + 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.spawnAgent<{ ok: boolean }>({ + runtime: 'headless', + 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); - 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'], @@ -575,7 +710,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'], @@ -642,7 +778,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 }, @@ -652,7 +789,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 }, @@ -672,14 +810,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; @@ -705,28 +839,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', @@ -736,36 +871,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(); } @@ -782,7 +919,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(); @@ -801,14 +938,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, @@ -840,12 +981,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)); @@ -869,13 +1014,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'], @@ -907,10 +1053,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'], @@ -928,14 +1075,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'], @@ -968,10 +1116,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'], @@ -997,10 +1146,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'], @@ -1018,12 +1168,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'], @@ -1057,11 +1208,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'], @@ -1085,12 +1237,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'], @@ -1117,7 +1270,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(); @@ -1143,7 +1296,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<{ @@ -1191,7 +1344,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', @@ -1224,7 +1377,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', @@ -1264,7 +1417,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', @@ -1311,7 +1464,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' }, @@ -1349,7 +1502,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); @@ -1380,7 +1533,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(); @@ -1460,9 +1613,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'], @@ -1478,9 +1632,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'], @@ -1498,9 +1653,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'], @@ -1520,9 +1676,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'], @@ -1541,15 +1698,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', @@ -1569,9 +1722,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'], @@ -1594,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: 'output-agent', cli: 'claude', channels: ['general'], @@ -1618,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: 'my-agent', cli: 'claude', channels: ['general'], @@ -1642,9 +1798,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'], @@ -1667,9 +1824,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'], @@ -1692,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: 'all-streams-agent', cli: 'claude', channels: ['general'], @@ -1716,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: 'stderr-filter-agent', cli: 'claude', channels: ['general'], @@ -1740,9 +1900,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 0e28703f3..b75e8871a 100644 --- a/packages/sdk/src/__tests__/spawn-token.test.ts +++ b/packages/sdk/src/__tests__/spawn-token.test.ts @@ -1,6 +1,7 @@ import { describe, expectTypeOf, it } from 'vitest'; -import type { SpawnProviderInput, SpawnPtyInput } from '../types.js'; +import type { SpawnAgentConfig, SpawnHeadlessAgentConfig, SpawnPtyAgentConfig } from '../relay.js'; +import type { SpawnCliInput, SpawnHeadlessInput, SpawnPtyInput } from '../types.js'; describe('spawn input agentToken types', () => { it('SpawnPtyInput accepts agentToken when present or omitted', () => { @@ -21,21 +22,64 @@ 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', + 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 SpawnHeadlessInput; + + expectTypeOf(withHeadlessHarness).toMatchTypeOf(); + expectTypeOf(withHeadlessHarness.cli).toMatchTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + 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 = { + cli: 'custom-app-server', + runtime: 'headless', + agentToken: 'jwt-token', + harnessConfig: { + runtime: 'headless', + protocol: 'custom-app-server', + endpoint: 'http://127.0.0.1:4099', + sessionId: 'session-headless', + }, + } satisfies SpawnHeadlessAgentConfig; + + 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/client.ts b/packages/sdk/src/client.ts index a1fea3766..daf1fb179 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 } : {}), @@ -227,6 +224,20 @@ function buildSpawnProviderBody( }; } +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; @@ -337,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 | SpawnProviderInput = { ...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 | SpawnProviderInput; + resolved = applySpawnPatch(resolved, patch); } } catch (err) { console.error('[agent-relay] beforeAgentSpawn listener threw:', err); @@ -606,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, @@ -614,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', @@ -629,31 +644,38 @@ export class AgentRelayClient { } } - async spawnProvider(input: SpawnProviderInput): Promise { - const beforeCtx: BeforeAgentSpawnContext = { - kind: 'provider', + async spawnCli(input: SpawnCliInput): Promise { + const beforeCtx: BeforeAgentSpawnContext = { + 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); 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 +687,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 +737,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/examples/demo.ts b/packages/sdk/src/examples/demo.ts index b433ca7f2..15c5bb136 100644 --- a/packages/sdk/src/examples/demo.ts +++ b/packages/sdk/src/examples/demo.ts @@ -36,8 +36,18 @@ 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', + args: ['--print'], + channels: ['general'], + }), + relay.spawnAgent({ + name: 'AgentB', + cli: 'claude', + args: ['--print'], + channels: ['general'], + }), ]); // ── Send messages ─────────────────────────────────────────────────────────── diff --git a/packages/sdk/src/examples/quickstart.ts b/packages/sdk/src/examples/quickstart.ts index 45bbc7d87..0a27fe716 100644 --- a/packages/sdk/src/examples/quickstart.ts +++ b/packages/sdk/src/examples/quickstart.ts @@ -39,14 +39,14 @@ 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({ cli: 'codex' }), + relay.spawnAgent({ cli: 'claude' }), + relay.spawnAgent({ cli: 'gemini' }), ]); // ── Configure messaging with custom CLI agents ───────────────────────────── -const worker1 = await relay.spawnPty({ +const worker1 = await relay.spawnAgent({ name: 'Worker1', cli: 'codex', args: ['--model', 'gpt-5'], diff --git a/packages/sdk/src/examples/ralph-loop.ts b/packages/sdk/src/examples/ralph-loop.ts index cfc5f45bb..12477a90a 100644 --- a/packages/sdk/src/examples/ralph-loop.ts +++ b/packages/sdk/src/examples/ralph-loop.ts @@ -212,12 +212,14 @@ 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', channels: ['general'], }), - relay.codex.spawn({ + relay.spawnAgent({ name: `Builder-${story.id}-${roundLabel}`, + cli: 'codex', args: ['--full-auto'], channels: ['general'], }), diff --git a/packages/sdk/src/lifecycle-hooks.ts b/packages/sdk/src/lifecycle-hooks.ts index b21e4fabc..d670ccd18 100644 --- a/packages/sdk/src/lifecycle-hooks.ts +++ b/packages/sdk/src/lifecycle-hooks.ts @@ -28,15 +28,17 @@ 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'; + +type SpawnInput = SpawnPtyInput | SpawnCliInput; // ── 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: @@ -47,24 +49,23 @@ import type { SpawnAgentResult, SpawnPtyInput, SpawnProviderInput } from './type * })); * ``` * - * 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< - SpawnPtyInput & SpawnProviderInput, + SpawnPtyInput & SpawnCliInput, 'args' | 'channels' | 'task' | 'model' | 'team' | 'agentToken' | 'harnessConfig' > >; // ── Call-site contexts ───────────────────────────────────────────────────── -export interface BeforeAgentSpawnContext { +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. */ @@ -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 | SpawnProviderInput; + resolvedInput: TInput; /** 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 da3d1c0e6..2643c760a 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({ cli: "codex" }); * const human = relay.human({ name: "System" }); * await human.sendMessage({ to: codex.name, text: "Hello!" }); * @@ -41,12 +41,17 @@ 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 { + JsonSchema, + SendMessageInput, + SpawnAgentResult, + SpawnHeadlessInput as ClientSpawnHeadlessInput, + SpawnPtyInput, +} from './types.js'; import type { AgentRuntime, BrokerEvent, BrokerStatus, - HeadlessProvider, MessageInjectionMode, RestartPolicy, } from './protocol.js'; @@ -118,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 {}; @@ -192,6 +223,23 @@ export interface AgentResultOptions { onResult?: (data: T, meta: AgentResultMeta) => void | Promise; } +type SpawnWithLifecycle = TInput & + SpawnLifecycleHooks & { result?: AgentResultOptions }; + +export type SpawnPtyAgentConfig = SpawnWithLifecycle< + Omit & { name?: string; runtime?: 'pty' }, + TAgentResult +>; + +export type SpawnHeadlessAgentConfig = SpawnWithLifecycle< + Omit & { name?: string; runtime: 'headless' }, + TAgentResult +>; + +export type SpawnAgentConfig = + | SpawnPtyAgentConfig + | SpawnHeadlessAgentConfig; + export type AgentStatus = 'spawning' | 'ready' | 'idle' | 'exited'; export type DeliveryWaitStatus = 'ack' | 'failed' | 'timeout'; export type DeliveryStateStatus = 'queued' | 'injected' | 'active' | 'verified' | 'failed'; @@ -230,6 +278,7 @@ export interface AgentActivityChange { export interface SpawnLifecycleContext { name: string; + /** CLI identifier used to launch the agent. */ cli: string; channels: string[]; task?: string; @@ -269,40 +318,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); @@ -366,33 +381,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; @@ -540,12 +528,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; @@ -603,11 +585,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 { @@ -701,7 +678,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; @@ -781,66 +758,81 @@ export class AgentRelay { // ── Spawning ──────────────────────────────────────────────────────────── - async spawnPty( - input: SpawnPtyInput & SpawnLifecycleHooks & { result?: AgentResultOptions } + 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) { + 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] spawnPty("${input.name}"): no channels specified, defaulting to "general". ` + + `[AgentRelay] ${spawnOperation}: 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, + 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, `${spawnOperation} onStart`); let result: SpawnAgentResult; - const resultContract = this.prepareAgentResultContract(input.result); - if (resultContract) { - this.resultContracts.set(input.name, resultContract as InternalAgentResultContract); - } + const resultContract = this.prepareAgentResultContract(config.result); try { - const harnessConfig = this.resolveHarnessConfig(input); - result = await client.spawnPty({ - name: input.name, - cli: input.cli, - args: input.args, + const harnessConfig = this.resolveHarnessConfig({ + name, + cli: config.cli, + args: config.args, + task: config.task, + model: config.model, + cwd: config.cwd, + harnessConfig: config.harnessConfig, + }); + const spawnInput = { + 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, + 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, - }); + skipRelayPrompt: config.skipRelayPrompt, + agentResultSchema: resultContract?.jsonSchema ?? config.agentResultSchema, + }; + + result = + runtime === 'headless' ? await client.spawnHeadless(spawnInput) : await client.spawnPty(spawnInput); } catch (error) { - if (resultContract) { - this.resultContracts.delete(input.name); - } await this.invokeLifecycleHook( - input.onError, + config.onError, { ...lifecycleContext, error, }, - `spawnPty("${input.name}") onError` + `${spawnOperation} onError` ); 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( @@ -850,61 +842,18 @@ export class AgentRelay { result ) as Agent; await this.invokeLifecycleHook( - input.onSuccess, + config.onSuccess, { ...lifecycleContext, name: result.name, runtime: result.runtime, sessionId: result.sessionId, }, - `spawnPty("${input.name}") onSuccess` + `${spawnOperation} onSuccess` ); return agent; } - 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>; - } - // ── Human source ──────────────────────────────────────────────────────── human(opts: { name: string }): HumanHandle { @@ -2195,113 +2144,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, - }); - } - - 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({ - name, - provider: cli as HeadlessProvider, - transport: harnessConfig?.runtime ?? 'headless', - args, - channels, - task, - model: options?.model, - cwd: options?.cwd, - 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, - }, - `spawn("${name}") onSuccess` - ); - return agent; - }, - }; - } - 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..880f70c75 100644 --- a/packages/sdk/src/spawn-from-env.ts +++ b/packages/sdk/src/spawn-from-env.ts @@ -221,7 +221,7 @@ export async function spawnFromEnv(options: SpawnFromEnvOptions = {}): Promise`, from @@ -67,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[]; diff --git a/web/components/SdkCodeExample.tsx b/web/components/SdkCodeExample.tsx index 02165ca94..77797b9f8 100644 --- a/web/components/SdkCodeExample.tsx +++ b/web/components/SdkCodeExample.tsx @@ -8,13 +8,15 @@ 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", task: "Discuss quantum error correction approaches with Build.", }); -await relay.codex.spawn({ +await relay.spawnAgent({ name: "Build", + cli: "codex", task: "Debate implementation strategies with Research.", }); diff --git a/web/content/docs/channels.mdx b/web/content/docs/channels.mdx index 63ee95775..1734fbab3 100644 --- a/web/content/docs/channels.mdx +++ b/web/content/docs/channels.mdx @@ -11,8 +11,9 @@ 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', 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..2ffb385fa 100644 --- a/web/content/docs/harness-runtime-config.mdx +++ b/web/content/docs/harness-runtime-config.mdx @@ -149,7 +149,10 @@ Codex session: ```typescript const sessionId = await createCodexSession({ cwd, task }); -await relay.spawn('CodexReviewer', 'codex', task, { +await relay.spawnAgent({ + name: 'CodexReviewer', + cli: 'codex', + task, harnessConfig: { runtime: 'pty', command: 'codex', @@ -185,4 +188,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..529c22301 100644 --- a/web/content/docs/harnesses.mdx +++ b/web/content/docs/harnesses.mdx @@ -56,7 +56,10 @@ const relay = new AgentRelay({ }, }); -await relay.spawn('ClaudeReviewer', 'company-claude', 'Review the current diff.', { +await relay.spawnAgent({ + name: 'ClaudeReviewer', + cli: 'company-claude', + task: 'Review the current diff.', model: 'opus', args: ['--verbose'], }); @@ -92,7 +95,10 @@ 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', + task, cwd, harnessConfig: codexResume(sessionId, cwd), }); @@ -126,7 +132,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..466198070 100644 --- a/web/content/docs/quickstart.mdx +++ b/web/content/docs/quickstart.mdx @@ -41,16 +41,20 @@ relay.onMessageReceived = (msg) => { }; // Spawn three agents. -const planner = await relay.claude.spawn({ +const planner = await relay.spawnAgent({ name: 'Planner', + cli: 'claude', model: Models.Claude.OPUS, }); -const coder = await relay.codex.spawn({ +const coder = await relay.spawnAgent({ name: 'Coder', + cli: 'codex', 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..bd279f15a 100644 --- a/web/content/docs/sending-messages.mdx +++ b/web/content/docs/sending-messages.mdx @@ -9,8 +9,14 @@ 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', +}); +const coder = await relay.spawnAgent({ + name: 'Coder', + cli: 'codex', +}); 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..56e86a68b 100644 --- a/web/content/docs/spawning-an-agent.mdx +++ b/web/content/docs/spawning-an-agent.mdx @@ -12,8 +12,9 @@ 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', model: Models.Claude.SONNET, channels: ['dev'], task: 'Break the work into 3 implementation steps.', @@ -36,8 +37,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 +49,14 @@ 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', - } -); + 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 +77,17 @@ 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({ cli: 'claude' }); +const codexWorker = await relay.spawnAgent({ cli: 'codex' }); +const geminiWorker = await relay.spawnAgent({ cli: 'gemini' }); +const opencodeWorker = await relay.spawnAgent({ + cli: 'opencode', + runtime: 'headless', +}); ``` ```python Python file="spawners.py" @@ -105,7 +106,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 +122,9 @@ const Result = z.object({ notes: z.array(z.string()), }); -const reviewer = await relay.claude.spawn({ +const reviewer = await relay.spawnAgent({ name: 'Reviewer', + cli: 'claude', 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..3bfaab5e8 100644 --- a/web/content/docs/typescript-sdk.mdx +++ b/web/content/docs/typescript-sdk.mdx @@ -37,21 +37,33 @@ 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 codex = await relay.spawnAgent({ + cli: 'codex', + 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:** | 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 (default: `'pty'`) | | `model` | `string` | Model to use (see Models below) | | `task` | `string` | Initial task / prompt | | `channels` | `string[]` | Channels to join | @@ -61,33 +73,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: +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)`. -```typescript -const agent = await relay.spawn('Worker', 'claude', 'Help with refactoring', { - model: Models.Claude.SONNET, - channels: ['team'], -}); -``` - -### `relay.spawnAndWait(name, cli, task, options?)` - -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 -}); -``` +Omit `runtime` for terminal-backed CLIs such as Claude Code, Codex, and Gemini. Use `runtime: 'headless'` for app-server sessions such as OpenCode. --- ## Agent -All spawn methods return an `Agent`: +`spawnAgent` returns an `Agent`: ```typescript interface Agent { @@ -131,8 +125,9 @@ const Summary = z.object({ findings: z.array(z.string()), }); -const reviewer = await relay.claude.spawn({ +const reviewer = await relay.spawnAgent({ name: 'Reviewer', + cli: 'claude', task: 'Review the branch and submit the final JSON result.', result: { schema: Summary, @@ -197,7 +192,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 +406,16 @@ relay.addListener('agentSpawned', (agent) => { }); // Spawn agents -const planner = await relay.claude.spawn({ +const planner = await relay.spawnAgent({ name: 'Planner', + cli: 'claude', model: Models.Claude.OPUS, task: 'Plan the feature implementation', }); -const coder = await relay.codex.spawn({ +const coder = await relay.spawnAgent({ name: 'Coder', + cli: 'codex', model: Models.Codex.GPT_5_3_CODEX, task: 'Implement the plan', }); @@ -469,7 +466,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({ 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' },