[feature] Support client-executed tools inside remote sub-agents

Summary

Client-executed tools (defineTool({ execute: 'client' })) are not supported inside remote sub-agents (createRemoteSubAgentTool / HttpRemoteAgentTransport / DOStubTransport). A remote sub-agent that calls a client tool suspends, and the parent has no way to route the browser's tool result across the HTTP boundary to resume it. This is a never-implemented combination, not a regression.

A fail-fast guard has been added so this now errors clearly with RemoteSubAgentClientToolUnsupportedError instead of hanging silently (see the guard MR that links this issue). This issue tracks implementing actual support as future work.

Background (standalone context)

  • Remote sub-agent: a parent agent delegates to an agent hosted on a separate HTTP service (an @helix-agents/agent-server deployment), reached over HTTP+SSE. Created with createRemoteSubAgentTool; transport is HttpRemoteAgentTransport (generic HTTP) or DOStubTransport (cross-DO).
  • Client-executed tool: a tool whose handler runs in the end-user's browser. The agent pauses mid-run (writes pendingClientToolCalls), the browser POSTs the result to /submit-tool-result, and the run resumes. Routing to the owning session uses SessionState.clientToolCallOwnership (on the root session) + rootSessionId, via routeSubmitToolResult (packages/core/src/client-tool/route-submit.ts).

Why it doesn't work today (confirmed gaps)

  • RemoteAgentTransport (packages/core/src/types/remote-protocol.ts) has no submitToolResult method — there's no way to deliver a tool result to a remote child.
  • remote-sub-agent-dispatch.ts proxies the child's stream to the parent but never writes clientToolCallOwnership on the parent root for the remote child, and never forwards parentSessionId/rootSessionId on transport.start(). So the parent has no routing entry for the child's pending call, and the child/parent live in different state stores.
  • A client-tool suspension keeps SessionStatus === 'active' (signaled by pendingClientToolCalls), so the parent's dispatch currently can't even distinguish it from a generic stream drop.
  • No tests cover remote-sub-agent + client-tool.
  1. Add submitToolResult to RemoteAgentTransport + HttpRemoteAgentTransport (POST /submit-tool-result — endpoint already exists on agent-server) + DOStubTransport.
  2. Forward parentSessionId/rootSessionId in RemoteStartRequest from every runtime's remote-dispatch site.
  3. Extend RemoteStatusResponse to carry pendingClientToolCalls (the guard MR already added awaitingClientTool). Parent's dispatch detects the child's client-tool suspension and mirrors ownership + a remote-tagged pending stub onto the parent's root session, suspending the parent via the existing client-tool machinery.
  4. routeSubmitToolResult: on a remote-tagged owner, forwardToOwnertransport.submitToolResult(childSessionId, result); the remote agent-server resolves + auto-resumes the child; the parent reconnects the child stream (fromSequence) to proxy the continuation to completion.
  5. Cross-runtime parity: JS, Temporal, DBOS, and Cloudflare DO remote-dispatch call sites (note: the JS runtime's remote path is separate from executeRemoteSubAgentDispatch).
  6. Cross-runtime e2e tests: remote sub-agent suspends on a client tool → submit at the parent → child resumes → completes; verify the browser result routes correctly and no unknown-tool-call.

Effort

Large — a distributed-systems feature spanning ~7 packages (core protocol + dispatch + route-submit, agent-server, http-transport, runtime-cloudflare, runtime-temporal, runtime-dbos, runtime-js) plus cross-runtime tests. Deferred deliberately; most apps don't need this niche combination.

References

File/line references as of origin/main @ def0392b9. Identified during an exhaustive audit of the "SessionState projection drops system fields" bug class.