feat(sdk): add async subagent middleware for remote LangGraph servers#1758
Conversation
| if name not in self._async: | ||
| spec = self._agents[name] | ||
| self._async[name] = get_client( | ||
| url=spec["url"], |
There was a problem hiding this comment.
Should this default to None? In most cases we want to use the asgi transport, right?
There was a problem hiding this comment.
url is now NotRequired in AsyncSubAgent and both get_sync and get_async use spec.get("url"), so omitting it falls through to ASGI transport. Does that make sense? Can you confirm?
| return error | ||
| spec = agent_map[subagent_type] | ||
| client = clients.sync(subagent_type) | ||
| thread = client.threads.create() |
There was a problem hiding this comment.
ooc: why do we need sync methods? Can we use the single async client and avoid the ssl cache hit and avoid the fact that sync client doesn't support asgi transport?
There was a problem hiding this comment.
Do you think we can just remove them?
libs/cli/deepagents_cli/main.py
Outdated
|
|
||
| from deepagents_cli.agent import load_async_subagents | ||
|
|
||
| async_subagents = load_async_subagents() or None |
There was a problem hiding this comment.
ooc can we load from the server?
|
Reading this PR to understand the changes and architecture. |
0e3c580 to
ba35fd7
Compare
Introduces AsyncSubAgentMiddleware that uses the LangGraph SDK to launch, monitor, and update background runs on remote LangGraph deployments. Wired into create_deep_agent via a new async_subagents parameter.
The LangGraph Platform server requires the x-auth-scheme header to know which auth flow to use. Without it, requests fail with "Missing agent builder trigger auth scheme, and passthrough headers." Now included by default on all SDK client connections.
Adds load_async_subagents() to read [async_subagents] from config.toml and passes them through create_cli_agent to create_deep_agent. Wired into interactive mode, non-interactive mode, and model hot-swap paths.
The JSON job_id format caused JSONDecodeError when LLMs decomposed the structured data instead of passing it verbatim. Switched to an opaque thread_id::run_id format that LLMs treat as an atomic string. The parser is robust and accepts the canonical format, full launch output, and legacy JSON for backwards compatibility.
The update tool was using update_state which only appended a message without triggering a new run. After the original run finished, checking the job would show "success" with the update message as the result. Now update creates a new run on the same thread so the subagent processes the follow-up with full conversation history. Returns a new job_id for tracking. Also encodes agent name in job_id for correct client routing with multiple agent types.
When update_async_subagent is called while a run is still active, the default "reject" strategy would reject the new run. Now uses "interrupt" so the current run is stopped and the follow-up is processed immediately.
Adds launch_async_subagent and update_async_subagent to the CLI's interrupt_on map, matching the existing behavior for the sync task tool. check_async_subagent is read-only and doesn't require approval.
…returns
Tools now accept ToolRuntime and return Command(update={...}) to write
AsyncSubAgentJob records to the async_subagent_jobs state field, ensuring
job metadata survives context compaction.
d2652ec to
acc9437
Compare
| key = self._cache_key(spec) | ||
| if key not in self._async: | ||
| self._async[key] = get_client( | ||
| url=spec.get("url"), |
There was a problem hiding this comment.
in what circumstances does this work when url is None? if we deploy both the deepagent and a dedicated async subagent through langgraph.json, would this work?
PR #1758 landed CLI code that passes `async_subagents` to `create_deep_agent()`, but the CLI still pins SDK 0.4.x which doesn't accept that kwarg — meaning even `async_subagents=None` raises `TypeError` at runtime. This switches to a conditional `**kwargs` pattern so the kwarg is only passed when async subagents are actually configured, keeping the CLI compatible with the pinned SDK until 0.5.0 ships.
…langchain-ai#1758) Adds `AsyncSubAgentMiddleware` to the SDK, enabling agents to launch background jobs on remote LangGraph deployments via the LangGraph SDK. Unlike synchronous subagents that block until completion, async subagents return a job ID immediately — the main agent can continue working, check progress on demand, send follow-up instructions, or cancel jobs. The CLI reads async subagent definitions from `[async_subagents]` in `~/.deepagents/config.toml` and wires them in automatically. ## Changes - Add `AsyncSubAgentMiddleware` in `deepagents/middleware/async_subagents.py` with five tools: `launch_async_subagent`, `check_async_subagent`, `update_async_subagent`, `cancel_async_subagent`, and `list_async_subagent_jobs` — each with sync + async implementations that return `Command` objects to persist job state - Track jobs via `AsyncSubAgentState` with a `_jobs_reducer` that merges updates into a `dict[str, AsyncSubAgentJob]`, surviving context compaction - Cache LangGraph SDK clients per `(url, headers)` tuple in `_ClientCache` to avoid reconnecting on every tool call - Add `async_subagents` parameter to `create_deep_agent` in `graph.py` — when provided, appends `AsyncSubAgentMiddleware` to the middleware stack - Add `load_async_subagents()` in the CLI to parse `config.toml` and pass specs through to `create_cli_agent` in both interactive and server modes - Wire HITL interrupt config for `launch_async_subagent`, `update_async_subagent`, and `cancel_async_subagent` in the CLI's approval system - Export `AsyncSubAgent`, `AsyncSubAgentJob`, and `AsyncSubAgentMiddleware` from `deepagents` and `deepagents.middleware` --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Colin Francis <colin.francis@langchain.dev> Co-authored-by: Colin Francis <colinfrancis53@gmail.com> Co-authored-by: Mason Daugherty <github@mdrxy.com> Co-authored-by: Mason Daugherty <mason@langchain.dev>
…chain-ai#1931) PR langchain-ai#1758 landed CLI code that passes `async_subagents` to `create_deep_agent()`, but the CLI still pins SDK 0.4.x which doesn't accept that kwarg — meaning even `async_subagents=None` raises `TypeError` at runtime. This switches to a conditional `**kwargs` pattern so the kwarg is only passed when async subagents are actually configured, keeping the CLI compatible with the pinned SDK until 0.5.0 ships.
Adds
AsyncSubAgentMiddlewareto the SDK, enabling agents to launch background jobs on remote LangGraph deployments via the LangGraph SDK. Unlike synchronous subagents that block until completion, async subagents return a job ID immediately — the main agent can continue working, check progress on demand, send follow-up instructions, or cancel jobs.The CLI reads async subagent definitions from
[async_subagents]in~/.deepagents/config.tomland wires them in automatically.Changes
AsyncSubAgentMiddlewareindeepagents/middleware/async_subagents.pywith five tools:launch_async_subagent,check_async_subagent,update_async_subagent,cancel_async_subagent, andlist_async_subagent_jobs— each with sync + async implementations that returnCommandobjects to persist job stateAsyncSubAgentStatewith a_jobs_reducerthat merges updates into adict[str, AsyncSubAgentJob], surviving context compaction(url, headers)tuple in_ClientCacheto avoid reconnecting on every tool callasync_subagentsparameter tocreate_deep_agentingraph.py— when provided, appendsAsyncSubAgentMiddlewareto the middleware stackload_async_subagents()in the CLI to parseconfig.tomland pass specs through tocreate_cli_agentin both interactive and server modeslaunch_async_subagent,update_async_subagent, andcancel_async_subagentin the CLI's approval systemAsyncSubAgent,AsyncSubAgentJob, andAsyncSubAgentMiddlewarefromdeepagentsanddeepagents.middleware