Skip to content

feat(sdk): expose headless spawn facade method#1003

Merged
willwashburn merged 15 commits into
mainfrom
codex/issue-998-spawn-provider-headless
May 27, 2026
Merged

feat(sdk): expose headless spawn facade method#1003
willwashburn merged 15 commits into
mainfrom
codex/issue-998-spawn-provider-headless

Conversation

@willwashburn

@willwashburn willwashburn commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Add AgentRelay.spawnHeadless({ cli, ... }) with the same facade lifecycle, result-contract, channel, and handle wiring as spawnPty().
  • Remove legacy provider naming from the SDK spawn request surface for the major release: AgentRelayClient.spawnProvider() -> spawnCli(), SpawnProviderInput -> SpawnCliInput, and SpawnHeadlessInput.provider -> SpawnHeadlessInput.cli.
  • Widen headless/CLI spawn input metadata for custom headless harnesses, update gateway spawn action typing, docs, changelog migration notes, and focused tests.

Verification

  • npm --prefix packages/sdk run check
  • npm --prefix packages/sdk run build
  • npm --prefix packages/gateway run build
  • npx vitest run --config vitest.config.ts src/__tests__/lifecycle-hooks.test.ts src/__tests__/orchestration-upgrades.test.ts src/__tests__/spawn-token.test.ts -t "spawnCli|cli transport|spawnHeadless|SpawnCliInput|SpawnHeadlessInput|SpawnHeadlessAgentInput"
  • npx prettier --check CHANGELOG.md packages/sdk/README.md packages/sdk/src/client.ts packages/sdk/src/lifecycle-hooks.ts packages/sdk/src/relay.ts packages/sdk/src/types.ts packages/sdk/src/__tests__/lifecycle-hooks.test.ts packages/sdk/src/__tests__/orchestration-upgrades.test.ts packages/sdk/src/__tests__/spawn-token.test.ts packages/sdk/src/__tests__/integration.test.ts packages/gateway/src/types.ts

Note: a full run of src/__tests__/orchestration-upgrades.test.ts in this sandbox hits existing tests that auto-create Relaycast workspaces when no API key is set, so it fails on restricted network DNS for api.relaycast.dev. The added/renamed CLI and headless tests pass in the focused run above.

Closes #998

@willwashburn willwashburn requested a review from khaliqgant as a code owner May 27, 2026 11:26
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces provider-shaped spawn APIs with CLI-shaped inputs, adds AgentRelay.spawnHeadless and shared lifecycle helpers, centralizes headless lifecycle/result handling, refactors AgentRelayClient to spawnCli, updates lifecycle-hook types, and adds tests, docs, changelog, gateway type changes, and trajectory records.

Changes

AgentRelay spawn facade expansion

Layer / File(s) Summary
Type system and spawn contracts
packages/sdk/src/types.ts, packages/sdk/src/lifecycle-hooks.ts
Introduce SpawnCliInput; remove SpawnProviderInput; widen SpawnHeadlessInput (model/cwd/team/shadow*/idleThresholdSecs/restartPolicy/continueFrom/harnessConfig); narrow lifecycle hook unions and patchable fields to SpawnPtyInput & SpawnCliInput.
Client: CLI spawn surface and request serialization
packages/sdk/src/client.ts
Add AgentRelayClient.spawnCli and internal spawnCliWithContext; remove spawnProvider; resolve transport from cli/harnessConfig.runtime; add buildSpawnCliBody and applySpawnPatch; generalize runBeforeSpawn and route spawnHeadless/spawnClaude/spawnOpencode through CLI flow.
AgentRelay facade methods and lifecycle handler
packages/sdk/src/relay.ts
Add SpawnWithLifecycle and SpawnHeadlessAgentInput; make findHarnessDefinition public; update spawnPty typing and result-schema selection; add public spawnHeadless delegating to spawnHeadlessWithLifecycle; refactor non-PTY spawner paths.
spawnHeadlessWithLifecycle implementation
packages/sdk/src/relay.ts
New helper centralizes lifecycle hook invocation (onStart/onSuccess/onError), harnessConfig resolution, client.spawnHeadless invocation (including continueFrom and derived agentResultSchema), registration of structured-result contracts after success, handle creation, and lifecycle bookkeeping.
Tests: orchestration, lifecycle, and type assertions
packages/sdk/src/__tests__/*
Extend facade mock with spawnHeadless; update orchestration tests to assert cli usage and lifecycle payloads; add headless orchestration tests verifying handle shape and agentResultSchema forwarding; add type-level Vitest assertions for SpawnCliInput/SpawnHeadlessInput/SpawnHeadlessAgentInput; rename integration test descriptions.
Documentation, examples, gateway types, and trajectories
CHANGELOG.md, packages/sdk/README.md, packages/gateway/src/types.ts, .agentworkforce/trajectories/completed/*
Document spawnHeadless({ cli, ... }), add migration guidance updating providercli, update gateway SpawnAgentAction.agent to SpawnCliInput, and add trajectory JSON/summary artifacts recording the API rename and decision history.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant AgentRelay
  participant spawnHeadlessWithLifecycle
  participant AgentRelayClient
  Caller->>AgentRelay: spawnHeadless(input)
  AgentRelay->>spawnHeadlessWithLifecycle: create lifecycle context & onStart
  spawnHeadlessWithLifecycle->>AgentRelayClient: resolve harnessConfig & client.spawnHeadless(payload)
  AgentRelayClient-->>spawnHeadlessWithLifecycle: spawn response (agent)
  spawnHeadlessWithLifecycle->>AgentRelay: register contracts & create Agent handle
  spawnHeadlessWithLifecycle->>AgentRelay: invoke onSuccess
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • khaliqgant

"🐰 I hopped through types and spawn,
I stitched lifecycle where hooks belong,
Headless agents wake and play,
handles returned to lead the way,
tiny tests applaud the song."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(sdk): expose headless spawn facade method' directly describes the main change: adding AgentRelay.spawnHeadless as a public facade method.
Description check ✅ Passed The description comprehensively covers the summary and includes a detailed verification section with specific test commands and build steps.
Linked Issues check ✅ Passed The PR fully addresses #998 objectives: exposes AgentRelay.spawnHeadless with lifecycle parity to spawnPty, removes legacy provider naming (spawnProvider → spawnCli), and updates all related types and tests.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked objectives: headless spawn facade, provider-to-CLI terminology migration, gateway typing updates, and supporting test/documentation changes. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-998-spawn-provider-headless

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request surfaces high-level AgentRelay.spawnProvider and AgentRelay.spawnHeadless facade methods in the @agent-relay/sdk package, allowing callers to launch provider-backed and headless harness agents with standard lifecycle hooks and handles. It also widens SpawnHeadlessInput to accept additional harness-backed provider metadata and configurations, documents the new APIs in the README and CHANGELOG, and adds comprehensive Vitest unit tests to verify the behavior. I have no feedback to provide as there are no review comments and the implementation is clean and well-tested.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df9e4c5665

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/sdk/src/relay.ts Outdated
result = await invoke(client, {
name: input.name,
provider: input.provider,
transport: input.transport ?? harnessConfig?.runtime ?? defaultTransport,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not freeze harness transport before hooks

When spawnProvider() is called without an explicit transport but with a harness config (or a registered harness), this line resolves and stores the transport before AgentRelayClient.spawnProvider() runs beforeAgentSpawn hooks. Those hooks are allowed to replace harnessConfig, but they cannot patch transport; the client then sees this pre-set transport and will not derive it from the patched harness, so a hook that swaps a headless harness for a pty harness (or vice versa) sends mismatched transport/harnessConfig to the broker and the spawn is rejected. Let the client derive transport from the post-hook input unless the caller explicitly supplied one.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
@.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json:
- Line 47: The committed trajectory.json contains a machine-specific absolute
path in the "projectId" field; replace that value with a neutral, repo-relative
identifier or project slug (e.g., "relay" or "./relay") instead of
"/Users/will/Projects/AgentWorkforce/relay". Update the "projectId" entry in
trajectory.json (look for the "projectId" key) to a non-sensitive identifier,
commit the change, and ensure future generation excludes absolute local paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4af54a17-81b1-472a-9f49-e5fb4627dd22

📥 Commits

Reviewing files that changed from the base of the PR and between f904124 and 61d1bbf.

📒 Files selected for processing (8)
  • .agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/summary.md
  • .agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json
  • CHANGELOG.md
  • packages/sdk/README.md
  • packages/sdk/src/__tests__/orchestration-upgrades.test.ts
  • packages/sdk/src/__tests__/spawn-token.test.ts
  • packages/sdk/src/relay.ts
  • packages/sdk/src/types.ts

},
"commits": [],
"filesChanged": [],
"projectId": "/Users/will/Projects/AgentWorkforce/relay",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove absolute local path from committed trajectory metadata.

projectId currently embeds a developer-local filesystem path (/Users/will/...), which leaks user/environment-identifying data and makes artifacts machine-specific. Prefer a repo-relative identifier or neutral project slug.

Suggested change
-  "projectId": "/Users/will/Projects/AgentWorkforce/relay",
+  "projectId": "AgentWorkforce/relay",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"projectId": "/Users/will/Projects/AgentWorkforce/relay",
"projectId": "AgentWorkforce/relay",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
@.agentworkforce/trajectories/completed/2026-05/traj_bvo77swtj1br/trajectory.json
at line 47, The committed trajectory.json contains a machine-specific absolute
path in the "projectId" field; replace that value with a neutral, repo-relative
identifier or project slug (e.g., "relay" or "./relay") instead of
"/Users/will/Projects/AgentWorkforce/relay". Update the "projectId" entry in
trajectory.json (look for the "projectId" key) to a non-sensitive identifier,
commit the change, and ensure future generation excludes absolute local paths.

@willwashburn willwashburn changed the title feat(sdk): expose provider spawn facade methods feat(sdk): expose headless spawn facade method May 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sdk/src/relay.ts (1)

928-1017: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't register the headless result contract under the requested name before the broker finalizes it.

This helper already expects result.name !== input.name. In that path, pre-populating this.resultContracts with input.name can clobber a live agent that already owns the requested name, and its pending waitForResult() state is never reset because only result.name is lifecycle-reset after the spawn succeeds. Please keep the pre-spawn contract separate from live-agent state, or explicitly tear down the requested-name state before reusing that key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/src/relay.ts` around lines 928 - 1017, The code pre-populates
this.resultContracts with the requested input.name before the broker finalizes
the agent name, which can clobber a live agent when result.name !== input.name;
modify spawnHeadlessWithLifecycle so you do NOT set
this.resultContracts.set(input.name, ...) before the spawn call — instead keep
the prepared resultContract in a local variable and only insert it into
this.resultContracts after the spawn succeeds (using result.name), and ensure
the error path still cleans up any temporary state (remove the pre-population
and rely on the existing cleanup that deletes by result.name when necessary);
key symbols: spawnHeadlessWithLifecycle, prepareAgentResultContract,
resultContract, this.resultContracts, input.name, result.name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/sdk/src/relay.ts`:
- Around line 928-1017: The code pre-populates this.resultContracts with the
requested input.name before the broker finalizes the agent name, which can
clobber a live agent when result.name !== input.name; modify
spawnHeadlessWithLifecycle so you do NOT set
this.resultContracts.set(input.name, ...) before the spawn call — instead keep
the prepared resultContract in a local variable and only insert it into
this.resultContracts after the spawn succeeds (using result.name), and ensure
the error path still cleans up any temporary state (remove the pre-population
and rely on the existing cleanup that deletes by result.name when necessary);
key symbols: spawnHeadlessWithLifecycle, prepareAgentResultContract,
resultContract, this.resultContracts, input.name, result.name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 47e1aa6c-bafe-4a25-b506-874bded77a73

📥 Commits

Reviewing files that changed from the base of the PR and between 61d1bbf and 910adcf.

📒 Files selected for processing (7)
  • .agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md
  • .agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json
  • CHANGELOG.md
  • packages/sdk/README.md
  • packages/sdk/src/__tests__/orchestration-upgrades.test.ts
  • packages/sdk/src/__tests__/spawn-token.test.ts
  • packages/sdk/src/relay.ts
✅ Files skipped from review due to trivial changes (2)
  • .agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/trajectory.json
  • .agentworkforce/trajectories/completed/2026-05/traj_33ykjz5a7avh/summary.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • CHANGELOG.md
  • packages/sdk/README.md
  • packages/sdk/src/tests/spawn-token.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sdk/src/relay.ts (1)

815-818: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delay PTY result-contract registration until the spawn resolves.

spawnPty() still overwrites resultContracts[input.name] before the broker confirms the final agent name, and the error path deletes that key again. If a same-named agent is already running, its in-flight agent_result can be validated/callbacked against the new contract, and a failed spawn can drop the old contract entirely. The new headless helper below already avoids this by registering only after result.name is known.

Proposed fix
     await this.invokeLifecycleHook(input.onStart, lifecycleContext, `spawnPty("${input.name}") onStart`);
     let result: SpawnAgentResult;
     const resultContract = this.prepareAgentResultContract(input.result);
-    if (resultContract) {
-      this.resultContracts.set(input.name, resultContract as InternalAgentResultContract);
-    }
     try {
       const harnessConfig = this.resolveHarnessConfig(input);
       result = await client.spawnPty({
         name: input.name,
         cli: input.cli,
@@
         skipRelayPrompt: input.skipRelayPrompt,
         agentResultSchema: resultContract?.jsonSchema ?? input.agentResultSchema,
       });
     } catch (error) {
-      if (resultContract) {
-        this.resultContracts.delete(input.name);
-      }
       await this.invokeLifecycleHook(
         input.onError,
         {
           ...lifecycleContext,
           error,
@@
       );
       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);
     }

Also applies to: 840-858

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/src/relay.ts` around lines 815 - 818, The current code registers
the prepared result contract into this.resultContracts using input.name before
spawnPty resolves, which can cause race conditions and accidental
overwrites/deletes; change the flow so prepareAgentResultContract is still
called but do NOT set this.resultContracts.set(input.name, ...) until after
spawnPty (or the spawn promise) resolves and you have the final result.name from
the broker; update the spawnPty-related logic (including any code paths in the
same block and the similar logic around lines 840-858) to register the contract
only when the spawn outcome confirms the final agent name (cast to
InternalAgentResultContract when storing), and ensure error/cleanup paths only
remove entries that were actually added after successful spawn resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/sdk/src/relay.ts`:
- Around line 815-818: The current code registers the prepared result contract
into this.resultContracts using input.name before spawnPty resolves, which can
cause race conditions and accidental overwrites/deletes; change the flow so
prepareAgentResultContract is still called but do NOT set
this.resultContracts.set(input.name, ...) until after spawnPty (or the spawn
promise) resolves and you have the final result.name from the broker; update the
spawnPty-related logic (including any code paths in the same block and the
similar logic around lines 840-858) to register the contract only when the spawn
outcome confirms the final agent name (cast to InternalAgentResultContract when
storing), and ensure error/cleanup paths only remove entries that were actually
added after successful spawn resolution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d3e8d96-b4a7-43b2-a64d-02933e0c8b4d

📥 Commits

Reviewing files that changed from the base of the PR and between d1aa908 and 9a709ee.

📒 Files selected for processing (2)
  • packages/sdk/src/__tests__/orchestration-upgrades.test.ts
  • packages/sdk/src/relay.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/sdk/src/tests/orchestration-upgrades.test.ts

@github-actions

Copy link
Copy Markdown
Contributor

Preview deployed!

Environment URL
Web https://d1qu0ath988pe2.cloudfront.net

This preview will be cleaned up when the PR is merged or closed.

@willwashburn willwashburn merged commit d4bc060 into main May 27, 2026
2 checks passed
@willwashburn willwashburn deleted the codex/issue-998-spawn-provider-headless branch May 27, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Surface spawnHeadless/spawnProvider on AgentRelay as siblings to spawnPty

1 participant