Skip to content

SDK structured-result feature silently hangs: agentResultSchema / waitForResult never fires when broker is spawned via @agent-relay/sdk #955

Description

@willwashburn

Summary

When a caller uses the agentResultSchema / result / waitForResult APIs from @agent-relay/sdk and spawns the broker via AgentRelayClient.spawn() (i.e. not via the agent-relay CLI), the spawned agent has no way to submit a structured result. The promise from waitForResult never resolves, no agent_result event ever fires, and the caller sees a silent hang (or a fallback-path failure on their end).

This was introduced with PR #932 ("Add structured agent result callbacks", commit 839d0cd). The feature works end-to-end only when the broker is launched via the agent-relay CLI, because that's the only code path that sets up the patched relaycast MCP.

Where the wiring breaks

The structured-result feature has four moving parts:

  1. SDK → broker spawn input. Caller passes agentResultSchema on SpawnPtyInput. SDK forwards it as agent_result_schema to the broker.

  2. Broker → MCP env injection. The broker constructs an AgentResultMcpConfig and injects AGENT_RELAY_RESULT_URL, AGENT_RELAY_RESULT_TOKEN, AGENT_RELAY_RESULT_SCHEMA into the relaycast MCP's environment (not into the agent's environment directly). See crates/broker/src/types.rs:230-239:

    impl AgentResultMcpConfig {
        pub fn env_pairs(&self) -> Vec<(&'static str, String)> {
            let mut pairs = vec![
                (\"AGENT_RELAY_RESULT_URL\", self.callback_url.clone()),
                (\"AGENT_RELAY_RESULT_TOKEN\", self.token.clone()),
            ];
            if let Some(schema) = &self.schema {
                pairs.push((\"AGENT_RELAY_RESULT_SCHEMA\", schema.to_string()));
            }
            pairs
        }
    }
  3. MCP → submit_result tool. A patched relaycast MCP reads those env vars and registers a `submit_result` MCP tool that POSTs to /api/agent-result. This patched MCP lives in src/cli/relaycast-mcp.ts:120-210 (added in commit 839d0cd):

    function registerAgentResultTool(server, config) {
      if (!config) return;
      server.registerTool('submit_result', { ... }, async ({ data, final, metadata }) => {
        const response = await fetch(config.url, {
          method: 'POST',
          headers: { Authorization: \`Bearer \${config.token}\`, ... },
          body: JSON.stringify({ agent: config.agentName, data, final: final ?? true, metadata }),
        });
        ...
      });
    }
  4. Broker → agent_result event. Broker accepts the POST and emits an agent_result BrokerEvent. SDK turns it into the agentResult event / resolves waitForResult.

Steps 2 and 4 work correctly. Step 3 is where things break for SDK consumers.

The actual bug: which MCP gets launched

The broker decides which MCP binary to launch via crates/broker/src/snippets.rs:240-266:

if let Ok(custom_cmd) = std::env::var(\"RELAYCAST_MCP_COMMAND\") {
    // use the custom command (parsed into argv)
} else {
    server.insert(\"command\".into(), Value::String(\"npx\".into()));
    server.insert(\"args\".into(), Value::Array(vec![
        Value::String(\"-y\".into()),
        Value::String(RELAYCAST_MCP_PACKAGE.into()),  // \"@relaycast/mcp\"
    ]));
}
  • The agent-relay CLI sets RELAYCAST_MCP_COMMAND before spawning the broker — see src/cli/lib/broker-lifecycle.ts:454-463 and src/cli/lib/relaycast-mcp-command.ts. It points at the patched relaycast-mcp.js bundled inside agent-relay/dist/src/cli/.

  • AgentRelayClient.spawn() does not set RELAYCAST_MCP_COMMAND. Verified in the installed SDK:

    $ grep -rn \"RELAYCAST_MCP_COMMAND\" node_modules/@agent-relay/sdk/dist/
    # (no matches)

So an SDK-spawned broker falls through to npx -y @relaycast/mcp, which resolves to the unpatched @relaycast/mcp@1.2.0 from the npm registry. Confirmed that package has zero references to submit_result or AGENT_RELAY_RESULT_*:

$ npm pack @relaycast/mcp@1.2.0 && tar xzf relaycast-mcp-1.2.0.tgz
$ grep -rn 'submit_result\\|AGENT_RELAY_RESULT' package/
# (no matches)

$ npm pack agent-relay@7.1.0 && tar xzf agent-relay-7.1.0.tgz
$ grep -c 'submit_result\\|AGENT_RELAY_RESULT' package/dist/src/cli/relaycast-mcp.js
5

So the agent gets the unpatched relaycast MCP, which has no submit_result tool. The agent has no way to call back. agent_result never fires. waitForResult hangs forever.

Reproduction

import { AgentRelayClient } from '@agent-relay/sdk';

const client = await AgentRelayClient.spawn({ cwd: '/some/dir' });
const spawned = await client.spawnPty({
  name: 'commit-draft',
  cli: 'codex',
  task: 'Output a JSON object {\"title\": string, \"body\": string} describing this diff: ...',
  agentResultSchema: {
    type: 'object',
    properties: { title: { type: 'string' }, body: { type: 'string' } },
    required: ['title'],
  },
});

// Wait for agent_result event — hangs forever, even after the agent has output a perfectly valid JSON to stdout.
client.onEvent((event) => {
  if (event.kind === 'agent_result' && event.name === spawned.name) {
    console.log('got result', event.data);
  }
});

Using the AgentRelay facade and agent.waitForResult() has the same failure mode — the underlying broker event never fires, so the promise never resolves.

Expected vs Actual

Expected: SDK consumers calling agentResultSchema / waitForResult get the same end-to-end behavior they'd get if they launched the broker via agent-relay up.

Actual: Silent hang. No agent_result event, no error, no warning logged. The unpatched MCP doesn't even know it's expected to register a submit_result tool, so there's no diagnostic on that side either.

Fix options (in order of preference)

  1. SDK auto-sets RELAYCAST_MCP_COMMAND in AgentRelayClient.spawn(). This requires the SDK to either bundle the patched relaycast-mcp.js itself, or take a dep on agent-relay (or a new sub-package) that provides the script. This is the right fix — the SDK is the public surface for the structured-result API, and it currently silently ships a broken feature.

  2. Publish the patched MCP as @relaycast/mcp@1.3 so the unpatched fallback path also works. This fixes downstream users who never set RELAYCAST_MCP_COMMAND, including non-SDK consumers.

  3. Extract the patched MCP into a published @agent-relay/relaycast-mcp package (the client identifier already used in src/cli/relaycast-mcp.ts:358 hints this was the original plan) and have the SDK reference it via RELAYCAST_MCP_COMMAND=\"npx -y @agent-relay/relaycast-mcp\".

  4. Minimum viable: fail loud. Even without fixing the underlying issue, the SDK should detect that the result feature was requested + no RELAYCAST_MCP_COMMAND was set + the default @relaycast/mcp is the unpatched version, and either warn or throw. Today the failure is silent, which is the worst-of-both for callers.

Repro environment

  • @agent-relay/sdk@7.1.0
  • @relaycast/mcp@1.2.0 (resolved by npx -y @relaycast/mcp inside the broker)
  • Broker binary bundled with @agent-relay/sdk@7.1.0 (agent-relay-broker-* v7.1.0)
  • Node 22, macOS 25.5.0

Notes for the fixer

  • The patched MCP source lives in src/cli/relaycast-mcp.ts of this repo; PR Add structured agent result callbacks #932 (commits 839d0cdd76b0f4) added it.
  • Be careful with the agent-name path: the patched MCP reads agentName from PatchedMcpServerOptions, not from env. If the SDK invokes the MCP without a --name arg, the submit_result POST will be missing the agent field and the broker may drop it. Check the wiring in both the CLI and any SDK-side launcher.
  • Worth adding an end-to-end test that uses AgentRelayClient.spawn() + agentResultSchema + waitForResult against a real codex/claude harness, since the existing test coverage for Add structured agent result callbacks #932 (per the merge stat) is mostly unit-level on the SDK side.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions