[PR #235] [CLOSED] feat(deepagents): async subagent execution by background streaming #290

Closed
opened 2026-06-05 17:22:32 -04:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/langchain-ai/deepagentsjs/pull/235
Author: @hntrl
Created: 2/18/2026
Status: Closed

Base: mainHead: hunter/async-subagents


📝 Commits (6)

📊 Changes

13 files changed (+1242 additions, -130 deletions)

View changed files

examples/async-subagents/agent.ts (+23 -0)
examples/async-subagents/demo.ts (+214 -0)
examples/async-subagents/index.html (+12 -0)
examples/async-subagents/package.json (+27 -0)
examples/async-subagents/server.ts (+34 -0)
examples/async-subagents/task-delegation.eval.ts (+338 -0)
examples/async-subagents/tsconfig.json (+23 -0)
examples/async-subagents/vitest.config.ts (+45 -0)
📝 examples/package.json (+4 -3)
📝 libs/deepagents/src/agent.ts (+10 -10)
📝 libs/deepagents/src/middleware/subagents.ts (+274 -62)
📝 pnpm-lock.yaml (+237 -55)
📝 pnpm-workspace.yaml (+1 -0)

📄 Description

Summary

Instead of blocking on each subagent until it finishes (await subagent.invoke()), the supervisor now fires off subagents as background streams and continues working while they run. Results are delivered back to the supervisor as [Task Result] messages when each subagent completes.

This branch includes two things:

  1. The core middleware change — refactoring createSubAgentMiddleware from synchronous invoke to async streaming
  2. A demo streaming example (examples/async-subagents/) — a React + Hono app with a real supervisor agent dispatching researcher and analyst subagents

1. Supervisor calls the task tool

The model emits a tool call like task({ subagent_type: "researcher", description: "Research LeBron James" }). The task tool:

  • Calls subagent.stream(input, config) to get an IterableReadableStream
  • Wraps it in a SubagentExecution — a class that eagerly consumes the stream in the background
  • Returns a Command that puts the execution into graph state under a tasks map and sends back a ToolMessage("Task initiated")
const stream = await subagent.stream(subagentState, config);
const execution = new SubagentExecution(subagent_type, stream);

return new Command({
  update: {
    messages: [new ToolMessage({ content: "Task initiated", tool_call_id: toolCallId, name: "task" })],
    tasks: { [toolCallId]: { type: "add", execution } },
  },
});

The SubagentExecution constructor starts consuming the stream immediately. The stream drains in the background via a for await loop inside the class.

2. The supervisor continues working

Because the tool returned instantly, the model gets back "Task initiated" and can decide what to do next. It might launch more tasks, call other tools, or start composing a response.

3. afterAgent — waiting for results

When the supervisor's agent loop would normally end (the model stops calling tools), the afterAgent hook fires. It checks: are there still pending tasks?

afterAgent: {
  hook: async (state) => {
    const tasks = state.tasks;
    if (!tasks || Object.keys(tasks).length === 0) return;

    // Block until at least one task finishes
    await Promise.race(Object.values(tasks).map((exec) => exec.result));

    // Collect completed results and inject them as messages
    const result = collectCompletedTasks(tasks);
    return {
      ...result.stateUpdate,
      messages: result.messages,  // [Task Result] messages
      tasks: result.taskUpdates,  // remove completed tasks
      jumpTo: "model",            // loop back to the supervisor
    };
  },
  canJumpTo: ["model"],
}

This is the critical piece: Promise.race resolves as soon as any task finishes (or instantly if one already finished). The hook collects the result, removes the completed task from state, injects a [Task Result] message, and uses jumpTo: "model" to loop the supervisor back for another turn.

Remaining tasks keep draining in the background. On the next iteration, beforeModel and afterAgent pick up any additional completions.

4. beforeModel — catching results between turns

The beforeModel hook runs before every model call and sweeps for tasks that completed while the model was generating its previous response:

beforeModel: async (state) => {
  const result = collectCompletedTasks(state.tasks);
  if (!result) return;
  return {
    ...result.stateUpdate,
    messages: result.messages,
    tasks: result.taskUpdates,
  };
}

This ensures the model always sees the latest results when it starts thinking.

5. Loop terminates naturally

The agent loop ends when the model stops calling tools and there are no pending tasks. The afterAgent hook returns undefined (no jumpTo), so the graph completes.

The demo example - examples/async-subagents/

A complete application you can run and interact with:

  • agent.ts — defines a supervisor with researcher and analyst subagents using createDeepAgent
  • server.ts — a Hono HTTP server that exposes POST /api/stream using the LangGraph SSE streaming protocol
  • vite.config.ts — a Vite dev server with a custom plugin that proxies /api/* requests to the Hono handler in-process
  • src/App.tsx — a React frontend using @langchain/langgraph-sdk/react's useStream hook with filterSubagentMessages and a sidebar showing live subagent status cards

The frontend shows messages in a chat interface, and when the supervisor dispatches subagents, they appear in a sidebar with live status badges (pending → running → complete). When results come back, the supervisor synthesizes them into a final response.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/langchain-ai/deepagentsjs/pull/235 **Author:** [@hntrl](https://github.com/hntrl) **Created:** 2/18/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `hunter/async-subagents` --- ### 📝 Commits (6) - [`167df4e`](https://github.com/langchain-ai/deepagentsjs/commit/167df4e78b2ac749a7c130f891be0e8cc4647a95) feat(deepagents): async subagent execution by background streaming - [`2de167c`](https://github.com/langchain-ai/deepagentsjs/commit/2de167c55dadc53dd7758b496be78de91ead058b) cr - [`39171e1`](https://github.com/langchain-ai/deepagentsjs/commit/39171e1cbb3e36c467d56fd90d87927360dce8ef) cr - [`f9489bd`](https://github.com/langchain-ai/deepagentsjs/commit/f9489bd589f919ba5c91cfb857a5a867d90499bf) cr - [`6d7f114`](https://github.com/langchain-ai/deepagentsjs/commit/6d7f1144a216e20fa0980acd2b84fd2d0fe92a3d) cr - [`c835e4d`](https://github.com/langchain-ai/deepagentsjs/commit/c835e4d7d6f543c33afa32a332ccceccb94a718e) cr ### 📊 Changes **13 files changed** (+1242 additions, -130 deletions) <details> <summary>View changed files</summary> ➕ `examples/async-subagents/agent.ts` (+23 -0) ➕ `examples/async-subagents/demo.ts` (+214 -0) ➕ `examples/async-subagents/index.html` (+12 -0) ➕ `examples/async-subagents/package.json` (+27 -0) ➕ `examples/async-subagents/server.ts` (+34 -0) ➕ `examples/async-subagents/task-delegation.eval.ts` (+338 -0) ➕ `examples/async-subagents/tsconfig.json` (+23 -0) ➕ `examples/async-subagents/vitest.config.ts` (+45 -0) 📝 `examples/package.json` (+4 -3) 📝 `libs/deepagents/src/agent.ts` (+10 -10) 📝 `libs/deepagents/src/middleware/subagents.ts` (+274 -62) 📝 `pnpm-lock.yaml` (+237 -55) 📝 `pnpm-workspace.yaml` (+1 -0) </details> ### 📄 Description ## Summary Instead of blocking on each subagent until it finishes (`await subagent.invoke()`), the supervisor now fires off subagents as background streams and continues working while they run. Results are delivered back to the supervisor as `[Task Result]` messages when each subagent completes. This branch includes two things: 1. **The core middleware change** — refactoring `createSubAgentMiddleware` from synchronous invoke to async streaming 2. **A demo streaming example** (`examples/async-subagents/`) — a React + Hono app with a real supervisor agent dispatching researcher and analyst subagents --- ### 1. Supervisor calls the `task` tool The model emits a tool call like `task({ subagent_type: "researcher", description: "Research LeBron James" })`. The task tool: - Calls `subagent.stream(input, config)` to get an `IterableReadableStream` - Wraps it in a **`SubagentExecution`** — a class that eagerly consumes the stream in the background - Returns a `Command` that puts the execution into graph state under a `tasks` map and sends back a `ToolMessage("Task initiated")` ```typescript const stream = await subagent.stream(subagentState, config); const execution = new SubagentExecution(subagent_type, stream); return new Command({ update: { messages: [new ToolMessage({ content: "Task initiated", tool_call_id: toolCallId, name: "task" })], tasks: { [toolCallId]: { type: "add", execution } }, }, }); ``` The `SubagentExecution` constructor starts consuming the stream **immediately**. The stream drains in the background via a `for await` loop inside the class. ### 2. The supervisor continues working Because the tool returned instantly, the model gets back "Task initiated" and can decide what to do next. It might launch more tasks, call other tools, or start composing a response. ### 3. `afterAgent` — waiting for results When the supervisor's agent loop would normally end (the model stops calling tools), the `afterAgent` hook fires. It checks: are there still pending tasks? ```typescript afterAgent: { hook: async (state) => { const tasks = state.tasks; if (!tasks || Object.keys(tasks).length === 0) return; // Block until at least one task finishes await Promise.race(Object.values(tasks).map((exec) => exec.result)); // Collect completed results and inject them as messages const result = collectCompletedTasks(tasks); return { ...result.stateUpdate, messages: result.messages, // [Task Result] messages tasks: result.taskUpdates, // remove completed tasks jumpTo: "model", // loop back to the supervisor }; }, canJumpTo: ["model"], } ``` This is the critical piece: `Promise.race` resolves as soon as **any** task finishes (or instantly if one already finished). The hook collects the result, removes the completed task from state, injects a `[Task Result]` message, and uses `jumpTo: "model"` to loop the supervisor back for another turn. Remaining tasks keep draining in the background. On the next iteration, `beforeModel` and `afterAgent` pick up any additional completions. ### 4. `beforeModel` — catching results between turns The `beforeModel` hook runs before every model call and sweeps for tasks that completed while the model was generating its previous response: ```typescript beforeModel: async (state) => { const result = collectCompletedTasks(state.tasks); if (!result) return; return { ...result.stateUpdate, messages: result.messages, tasks: result.taskUpdates, }; } ``` This ensures the model always sees the latest results when it starts thinking. ### 5. Loop terminates naturally The agent loop ends when the model stops calling tools **and** there are no pending tasks. The `afterAgent` hook returns `undefined` (no `jumpTo`), so the graph completes. ## The demo example - `examples/async-subagents/` A complete application you can run and interact with: - **`agent.ts`** — defines a supervisor with `researcher` and `analyst` subagents using `createDeepAgent` - **`server.ts`** — a Hono HTTP server that exposes `POST /api/stream` using the LangGraph SSE streaming protocol - **`vite.config.ts`** — a Vite dev server with a custom plugin that proxies `/api/*` requests to the Hono handler in-process - **`src/App.tsx`** — a React frontend using `@langchain/langgraph-sdk/react`'s `useStream` hook with `filterSubagentMessages` and a sidebar showing live subagent status cards The frontend shows messages in a chat interface, and when the supervisor dispatches subagents, they appear in a sidebar with live status badges (pending → running → complete). When results come back, the supervisor synthesizes them into a final response. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-06-05 17:22:32 -04:00
yindo closed this issue 2026-06-05 17:22:32 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#290