Skip to content

Comments

feat(deepagents): async subagent execution by background streaming#235

Open
Hunter Lovell (hntrl) wants to merge 6 commits intomainfrom
hunter/async-subagents
Open

feat(deepagents): async subagent execution by background streaming#235
Hunter Lovell (hntrl) wants to merge 6 commits intomainfrom
hunter/async-subagents

Conversation

@hntrl
Copy link
Member

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.

@changeset-bot
Copy link

changeset-bot bot commented Feb 18, 2026

⚠️ No Changeset found

Latest commit: c835e4d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant