[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-serverdeployment), reached over HTTP+SSE. Created withcreateRemoteSubAgentTool; transport isHttpRemoteAgentTransport(generic HTTP) orDOStubTransport(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 usesSessionState.clientToolCallOwnership(on the root session) +rootSessionId, viarouteSubmitToolResult(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 nosubmitToolResultmethod — there's no way to deliver a tool result to a remote child.remote-sub-agent-dispatch.tsproxies the child's stream to the parent but never writesclientToolCallOwnershipon the parent root for the remote child, and never forwardsparentSessionId/rootSessionIdontransport.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 bypendingClientToolCalls), so the parent's dispatch currently can't even distinguish it from a generic stream drop. - No tests cover remote-sub-agent + client-tool.
Design sketch (recommended approach: "parent-side ownership mirror")
- Add
submitToolResulttoRemoteAgentTransport+HttpRemoteAgentTransport(POST/submit-tool-result— endpoint already exists onagent-server) +DOStubTransport. - Forward
parentSessionId/rootSessionIdinRemoteStartRequestfrom every runtime's remote-dispatch site. - Extend
RemoteStatusResponseto carrypendingClientToolCalls(the guard MR already addedawaitingClientTool). Parent's dispatch detects the child's client-tool suspension and mirrors ownership + aremote-tagged pending stub onto the parent's root session, suspending the parent via the existing client-tool machinery. routeSubmitToolResult: on a remote-tagged owner,forwardToOwner→transport.submitToolResult(childSessionId, result); the remoteagent-serverresolves + auto-resumes the child; the parent reconnects the child stream (fromSequence) to proxy the continuation to completion.- Cross-runtime parity: JS, Temporal, DBOS, and Cloudflare DO remote-dispatch call sites (note: the JS runtime's remote path is separate from
executeRemoteSubAgentDispatch). - 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.