Skip to content

feat(deepagents): async agents experiments#299

Draft
Christian Bromann (christian-bromann) wants to merge 2 commits intomainfrom
cb/async-agents
Draft

feat(deepagents): async agents experiments#299
Christian Bromann (christian-bromann) wants to merge 2 commits intomainfrom
cb/async-agents

Conversation

@christian-bromann

This PR adds an experimental observer/companion stack for long-running Deep Agents.

It introduces a session-scoped activity bus that lets external consumers inspect what a running agent is doing, plus an optional conversational companion agent that can answer questions about the run and queue lightweight steering commands.

Motivation

Long-running agents are hard to work with once they are in flight. Today, users can mostly do one of two things:

  1. Watch the stream passively.
  2. Interrupt the run and restart with new instructions.

This change explores a middle ground:

  • observe the current state of a running agent
  • inspect recent cross-thread activity
  • queue lightweight steering like reminders or todo requests without tearing down the run

The design is intentionally built around a shared session/event model rather than a single read-only observer abstraction, so it can later back UI tails, CLIs, websocket transports, and higher-level agent orchestration patterns.

What’s Included

Observer middleware

Adds createObserverMiddleware() in libs/deepagents/src/middleware/observer.ts.

The middleware:

  • records model_response activity events after model calls
  • records tool_result activity events after tool calls
  • extracts file-touch metadata for common file tools
  • reads queued control commands before model calls
  • injects queued steering commands into the next model step
  • records control_applied events when commands are claimed

This is implemented as best-effort middleware so observation failures do not break the main run.

Session handle

Adds createSessionHandle() in libs/deepagents/src/observer/handle.ts.

The session handle provides the non-LLM control/read API:

  • getSnapshot() for aggregated session/thread state
  • getEvents() for cursor-based activity paging
  • send() for queueing steering commands

This is the core primitive behind the feature.

Companion agent

Adds createCompanionAgent() in libs/deepagents/src/observer/agent.ts.

The companion agent:

  • always gets an observe_agent tool
  • optionally gets a steer_agent tool when allowSteering: true
  • is intended to be a conversational layer over the session handle
  • can answer questions about current work and queue lightweight steering commands

Observer tools

Adds createObserveTool() and createSteerTool() in libs/deepagents/src/observer/tool.ts.

These tools expose:

  • structured observation of session snapshots + activity events
  • lightweight command queueing for reminders, messages, todo requests, and guidance

Types and store helpers

Adds shared types and store helpers under libs/deepagents/src/observer/.

This includes:

  • ActivityEvent
  • ControlCommand
  • SessionSnapshot
  • SessionEventPage
  • SessionHandle
  • event/control namespace helpers
  • event writing + eviction
  • event pagination
  • control-command persistence + claiming

Examples

Adds runnable examples in examples/observer/:

  • basic-companion.ts
  • interactive-companion.ts

These demonstrate:

  • running a main Deep Agent in the background
  • creating a shared session handle
  • attaching a conversational companion
  • asking questions about the run
  • queueing lightweight steering commands interactively

API Overview

Middleware

const observerMiddleware = createObserverMiddleware({
  store,
  sessionId,
  enableControl: true,
});

Session handle

const session = createSessionHandle({
  sessionId,
  store,
  getState: (threadId) =>
    agent.getState({ configurable: { thread_id: threadId } }),
});

const snapshot = await session.getSnapshot({ scope: "all" });
const events = await session.getEvents({ limit: 20 });

await session.send({
  kind: "reminder",
  target: "active",
  payload: { text: "Before continuing, update the docs too." },
});

Companion agent

const companion = createCompanionAgent({
  session,
  checkpointer,
  allowSteering: true,
});

Current Limitations / Follow-ups

This implementation is a strong first slice, but a few areas are still follow-up work rather than fully complete behavior:

  • Subagent activity is not yet automatically instrumented by propagating the observer middleware into spawned subagents.
  • Steering commands are queued and injected as model-visible guidance; they are not yet enforced as structured mutations of todo/guidance state.
  • "active" targeting is currently lightweight and should become more precise once we have stronger active-thread tracking.
  • Exactly-once command claiming across concurrent threads/processes will need stronger store-level claim semantics than the current read/update loop.
  • Thread lifecycle events (thread_started, thread_completed, thread_failed) are defined in the model but not yet fully emitted, so running/idle inference can be improved.

Why this direction

The important architectural choice here is making the primitive session-based and event-driven rather than coupling everything to a single observer agent.

That gives us:

  • a reusable control/read surface for non-LLM consumers
  • a path toward async agent UX
  • a foundation for future UI streaming and websocket transport
  • a cleaner way to reason about agent families instead of only one root thread

Example Use Cases

  • “What is the agent doing right now?”
  • “What files has it touched so far?”
  • “What todos are still pending?”
  • “Before you continue, add a todo to update the docs.”
  • “Remember to add tests before you finish.”

Notes

This should be viewed as an experimental first pass at async observation + lightweight steering for Deep Agents, with the core abstractions now in place for follow-up work on subagent propagation, stronger control semantics, and more robust cross-thread concurrency handling.

@changeset-bot
Copy link

changeset-bot bot commented Mar 11, 2026

⚠️ No Changeset found

Latest commit: d731d81

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

) {
res.statusCode = status;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(body));

Check warning

Code scanning / CodeQL

Information exposure through a stack trace Medium

This information exposed to the user depends on
stack trace information
.
This information exposed to the user depends on
stack trace information
.

Copilot Autofix

AI 4 days ago

In general, to fix information exposure through stack traces you should avoid sending raw exception data (messages, stacks, or entire error objects) back to clients. Instead, log the error server-side (where developers can inspect it) and respond to the client with a generic, non-sensitive error message and possibly a coarse error code.

For this codebase, the best minimal fix is to modify the /api/steer error handling so that it no longer returns error.message (or any other data derived from the original exception) to the client. Instead, it should:

  • Log the full error (including stack) to the server console or logger.
  • Return a fixed generic error payload, such as { error: "Invalid request payload" } or { error: "Unable to process steer request" }, independent of the thrown error.
  • Keep all success responses unchanged and leave sendJson as a generic helper, since the issue here is how it’s used with error data, not JSON.stringify itself.

Concretely, in libs/deepagents-ui/src/vite-server.ts, within the if (req.method === "POST" && url.pathname === "/api/steer") { ... } block, replace the catch block that builds the response from error.message with one that logs the error via console.error and sends a generic message. No new imports are needed, as console is globally available in Node.

Suggested changeset 1
libs/deepagents-ui/src/vite-server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/libs/deepagents-ui/src/vite-server.ts b/libs/deepagents-ui/src/vite-server.ts
--- a/libs/deepagents-ui/src/vite-server.ts
+++ b/libs/deepagents-ui/src/vite-server.ts
@@ -103,8 +103,10 @@
         const result = await attached.steer(input);
         sendJson(res, 200, result);
       } catch (error) {
+        // Log detailed error information on the server, but do not expose it to the client.
+        console.error("Error handling /api/steer request:", error);
         sendJson(res, 400, {
-          error: error instanceof Error ? error.message : String(error),
+          error: "Unable to process steer request.",
         });
       }
       return;
EOF
@@ -103,8 +103,10 @@
const result = await attached.steer(input);
sendJson(res, 200, result);
} catch (error) {
// Log detailed error information on the server, but do not expose it to the client.
console.error("Error handling /api/steer request:", error);
sendJson(res, 400, {
error: error instanceof Error ? error.message : String(error),
error: "Unable to process steer request.",
});
}
return;
Copilot is powered by AI and may make mistakes. Always verify output.
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