[PR #287] feat: Programmatic Tool Calling & Subagent Spawning for All Sandbox Providers #328

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

📋 Pull Request Information

Original PR: https://github.com/langchain-ai/deepagentsjs/pull/287
Author: @christian-bromann
Created: 3/8/2026
Status: 🔄 Open

Base: mainHead: cb/ptc


📝 Commits (7)

📊 Changes

26 files changed (+3812 additions, -1009 deletions)

View changed files

📝 .gitignore (+2 -1)
📝 examples/package.json (+3 -3)
examples/sandbox/sandbox-ptc-node.ts (+118 -0)
examples/sandbox/sandbox-ptc-python.ts (+118 -0)
examples/sandbox/sandbox-ptc.ts (+177 -0)
examples/sandbox/skills/employee-classifier/SKILL.md (+92 -0)
examples/sandbox/utils/sandbox-ptc.ts (+129 -0)
📝 libs/deepagents/package.json (+6 -6)
📝 libs/deepagents/src/agent.test-d.ts (+2 -2)
📝 libs/deepagents/src/backends/index.ts (+1 -0)
📝 libs/deepagents/src/backends/protocol.ts (+36 -0)
📝 libs/deepagents/src/index.ts (+11 -0)
📝 libs/deepagents/src/middleware/index.ts (+6 -0)
libs/deepagents/src/sandbox-ptc/engine.int.test.ts (+303 -0)
libs/deepagents/src/sandbox-ptc/engine.test.ts (+354 -0)
libs/deepagents/src/sandbox-ptc/engine.ts (+306 -0)
libs/deepagents/src/sandbox-ptc/index.ts (+30 -0)
libs/deepagents/src/sandbox-ptc/middleware.ts (+188 -0)
libs/deepagents/src/sandbox-ptc/prompt.ts (+192 -0)
libs/deepagents/src/sandbox-ptc/runtimes.ts (+189 -0)

...and 6 more files

📄 Description

This PR extends Programmatic Tool Calling (PTC) beyond QuickJS to every sandbox provider (Deno, Modal, Daytona, Node-VFS). Agents can now write bash scripts that call tools and spawn subagents in parallel directly from within the sandbox — enabling Recursive Language Model (RLM) patterns across all execution environments.

This is demonstrated by the example in examples/sandbox/sandbox-ptc.ts:

  • a Deep Agent is tasked to classify (via tool call) and analyse (via subagent call) a CSV file with 100 employees
  • the script finishes within 50s and downloads the entire analysis from the sandbox
  • the agent calls a script containing tool and subagent call (see example pulled from trace)
  • the trace shows all 100 tool calls and subsequent subagents:
Screenshot 2026-03-08 at 12 36 58 AM

Motivation

The QuickJS provider already supports PTC through its WASM FFI bridge — tools are injected as JavaScript functions into the QuickJS context, and the agent calls them via tools.webSearch({...}) inside js_eval. This works because QuickJS runs in-process; the host and guest share an address space.

Shell-based sandboxes (Deno, Modal, Daytona, Node-VFS) had no equivalent. Code running inside these sandboxes could not call tools or spawn subagents during execution — the execute tool was fire-and-forget.

Approach: QuickJS vs Sandbox PTC

QuickJS PTC Sandbox PTC (this PR)
Runtime WASM interpreter (in-process) Any shell sandbox (container, VM, local)
Communication Synchronous FFI — host functions injected into QuickJS context IPC via stderr markers + file-based responses
Tool injection context.newFunction() on the QuickJS C API Bash runtime library uploaded via execute() + heredoc
Tool invocation await tools.webSearch({query: "..."}) (JS) result=$(tool_call web_search '{"query":"..."}') (bash)
Subagent spawning await tools.task({description: "..."}) (JS) result=$(spawn_agent "Research X" "analyst") (bash)
Parallelism Promise.all([...]) Bash background jobs (&) + wait
Language support JavaScript/TypeScript only Bash, Python, Node.js (runtime libraries for each)
Latency per call <1ms (in-process FFI) ~5-50ms (file I/O + polling)
Provider support QuickJS only All sandbox providers

Architecture

Orchestrator (Host)                         Sandbox (Guest)
┌─────────────────────┐                     ┌──────────────────────────────┐
│ PtcExecutionEngine   │                     │ source /tmp/.da_runtime.sh   │
│                      │                     │                              │
│ 1. Install runtime   │──── execute() ────> │ Defines: tool_call,          │
│ 2. Spawn interactive │──── spawnInteractive │          spawn_agent         │
│ 3. Scan stderr for   │<─── stderr ──────── │                              │
│    __DA_REQ__ markers │                     │ result=$(tool_call foo '{}') │
│ 4. Invoke host tool  │                     │   ├─ writes marker to stderr │
│ 5. Write response    │──── writeFile() ──> │   ├─ polls /tmp/.da_ipc/res/ │
│    to sandbox FS     │                     │   └─ reads response, returns │
└─────────────────────┘                     └──────────────────────────────┘

IPC Protocol:

  • Request: Script writes __DA_REQ__<uuid> <json> to stderr (single line, atomic under PIPE_BUF)
  • Response: Engine writes to /tmp/.da_ipc/res/<uuid> (status line + payload)
  • Why stderr? So markers aren't captured by bash $(...) command substitution

What's included

Core PTC engine (libs/deepagents/src/sandbox-ptc/):

  • engine.tsPtcExecutionEngine with StdoutScanner, IPC loop, tool call tracing
  • runtimes.ts — Bash, Python, and Node.js runtime libraries (embedded strings)
  • middleware.tscreateSandboxPtcMiddleware (opt-in, intercepts execute via wrapToolCall)
  • prompt.ts — Auto-generates system prompt from tool schemas and subagent types
  • types.tsPtcToolCallTrace, PtcExecuteResult, IPC types

Protocol extension (libs/deepagents/src/backends/protocol.ts):

  • InteractiveProcess interface (streaming stdout/stderr + writeFile + waitForExit)
  • spawnInteractive() method on SandboxBackendProtocol (optional, enables PTC)

Provider implementations (each adds spawnInteractive()):

  • @langchain/deno — native ReadableStream from sandbox.spawn()
  • @langchain/modalReadableStream from sandbox.exec()
  • @langchain/node-vfs — Node.js child_process streams
  • @langchain/daytona — background process + polling (SDK has no streaming API)

Example (examples/sandbox/sandbox-ptc.ts):

  • Full createDeepAgent integration with PTC middleware + analyst subagent
  • 100 parallel tool_call classify_record + 100 parallel spawn_agent "analyst"
  • Downloads deliverables (classifications + analyst reports) from sandbox to local disk

Usage

import { createDeepAgent, createSandboxPtcMiddleware } from "deepagents";

const agent = createDeepAgent({
  model: "claude-sonnet-4-5-20250929",
  backend: sandbox,
  subagents: [
    { name: "analyst", description: "...", systemPrompt: "..." },
  ],
  middleware: [
    createSandboxPtcMiddleware({ backend: sandbox, ptc: true }),
  ],
});

The middleware auto-injects a system prompt documenting tool_call and spawn_agent with per-tool API references generated from schemas. No manual prompt engineering needed.

Tool call tracing

Every PTC-instrumented execute result includes a trace summary:

[PTC: 200 tool calls (classify_record=100, task=100), 200 succeeded, 0 failed, 1823ms total]

The engine also returns structured PtcToolCallTrace[] with name, input, result/error, and duration per call.


🔄 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/287 **Author:** [@christian-bromann](https://github.com/christian-bromann) **Created:** 3/8/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `cb/ptc` --- ### 📝 Commits (7) - [`bb4bf52`](https://github.com/langchain-ai/deepagentsjs/commit/bb4bf52a05acaa6b4e3896c537df8542c6337bfa) feat(deepagents): support for PTC in all sandboxes - [`9800a3f`](https://github.com/langchain-ai/deepagentsjs/commit/9800a3fdccaed3f1a3cbedcee0bb26d95f81f364) store traces - [`ff2ea45`](https://github.com/langchain-ai/deepagentsjs/commit/ff2ea4555a975107326edcf3c8367b5aed112111) demo update - [`6098276`](https://github.com/langchain-ai/deepagentsjs/commit/60982762708a449998a17c5088df48d424f81771) improve prompt and example - [`ec7576b`](https://github.com/langchain-ai/deepagentsjs/commit/ec7576ba46d45461b26e22042c0b73a17a03c20b) format + lint - [`de866be`](https://github.com/langchain-ai/deepagentsjs/commit/de866be7d581ab7f6150b9f1efa5145f2d43ae14) make PTC and subagent spawning work via scripts - [`fafd0bd`](https://github.com/langchain-ai/deepagentsjs/commit/fafd0bd63e47590c58516e832b308a80c72ded1a) fix test ### 📊 Changes **26 files changed** (+3812 additions, -1009 deletions) <details> <summary>View changed files</summary> 📝 `.gitignore` (+2 -1) 📝 `examples/package.json` (+3 -3) ➕ `examples/sandbox/sandbox-ptc-node.ts` (+118 -0) ➕ `examples/sandbox/sandbox-ptc-python.ts` (+118 -0) ➕ `examples/sandbox/sandbox-ptc.ts` (+177 -0) ➕ `examples/sandbox/skills/employee-classifier/SKILL.md` (+92 -0) ➕ `examples/sandbox/utils/sandbox-ptc.ts` (+129 -0) 📝 `libs/deepagents/package.json` (+6 -6) 📝 `libs/deepagents/src/agent.test-d.ts` (+2 -2) 📝 `libs/deepagents/src/backends/index.ts` (+1 -0) 📝 `libs/deepagents/src/backends/protocol.ts` (+36 -0) 📝 `libs/deepagents/src/index.ts` (+11 -0) 📝 `libs/deepagents/src/middleware/index.ts` (+6 -0) ➕ `libs/deepagents/src/sandbox-ptc/engine.int.test.ts` (+303 -0) ➕ `libs/deepagents/src/sandbox-ptc/engine.test.ts` (+354 -0) ➕ `libs/deepagents/src/sandbox-ptc/engine.ts` (+306 -0) ➕ `libs/deepagents/src/sandbox-ptc/index.ts` (+30 -0) ➕ `libs/deepagents/src/sandbox-ptc/middleware.ts` (+188 -0) ➕ `libs/deepagents/src/sandbox-ptc/prompt.ts` (+192 -0) ➕ `libs/deepagents/src/sandbox-ptc/runtimes.ts` (+189 -0) _...and 6 more files_ </details> ### 📄 Description This PR extends Programmatic Tool Calling (PTC) beyond QuickJS to **every sandbox provider** (Deno, Modal, Daytona, Node-VFS). Agents can now write bash scripts that call tools and spawn subagents in parallel directly from within the sandbox — enabling Recursive Language Model (RLM) patterns across all execution environments. This is demonstrated by the example in `examples/sandbox/sandbox-ptc.ts`: - a Deep Agent is tasked to classify (via tool call) and analyse (via subagent call) a CSV file with 100 employees - the script finishes within 50s and downloads the entire analysis from the sandbox - the agent calls a script containing tool and subagent call (see [example](https://gist.github.com/christian-bromann/e88f3e0cb74600bb5c8fbafab41a8453) pulled from trace) - the [trace](https://smith.langchain.com/public/4568e776-5576-4314-be39-14a57c3e8f8f/r) shows all 100 tool calls and subsequent subagents: <img width="2102" height="1416" alt="Screenshot 2026-03-08 at 12 36 58 AM" src="https://github.com/user-attachments/assets/93dadd8d-8547-4a76-86a3-589f75b1eba3" /> ### Motivation The QuickJS provider already supports PTC through its WASM FFI bridge — tools are injected as JavaScript functions into the QuickJS context, and the agent calls them via `tools.webSearch({...})` inside `js_eval`. This works because QuickJS runs in-process; the host and guest share an address space. Shell-based sandboxes (Deno, Modal, Daytona, Node-VFS) had no equivalent. Code running inside these sandboxes could not call tools or spawn subagents during execution — the `execute` tool was fire-and-forget. ### Approach: QuickJS vs Sandbox PTC | | QuickJS PTC | Sandbox PTC (this PR) | |---|---|---| | **Runtime** | WASM interpreter (in-process) | Any shell sandbox (container, VM, local) | | **Communication** | Synchronous FFI — host functions injected into QuickJS context | IPC via stderr markers + file-based responses | | **Tool injection** | `context.newFunction()` on the QuickJS C API | Bash runtime library uploaded via `execute()` + heredoc | | **Tool invocation** | `await tools.webSearch({query: "..."})` (JS) | `result=$(tool_call web_search '{"query":"..."}')` (bash) | | **Subagent spawning** | `await tools.task({description: "..."})` (JS) | `result=$(spawn_agent "Research X" "analyst")` (bash) | | **Parallelism** | `Promise.all([...])` | Bash background jobs (`&`) + `wait` | | **Language support** | JavaScript/TypeScript only | Bash, Python, Node.js (runtime libraries for each) | | **Latency per call** | <1ms (in-process FFI) | ~5-50ms (file I/O + polling) | | **Provider support** | QuickJS only | All sandbox providers | ### Architecture ``` Orchestrator (Host) Sandbox (Guest) ┌─────────────────────┐ ┌──────────────────────────────┐ │ PtcExecutionEngine │ │ source /tmp/.da_runtime.sh │ │ │ │ │ │ 1. Install runtime │──── execute() ────> │ Defines: tool_call, │ │ 2. Spawn interactive │──── spawnInteractive │ spawn_agent │ │ 3. Scan stderr for │<─── stderr ──────── │ │ │ __DA_REQ__ markers │ │ result=$(tool_call foo '{}') │ │ 4. Invoke host tool │ │ ├─ writes marker to stderr │ │ 5. Write response │──── writeFile() ──> │ ├─ polls /tmp/.da_ipc/res/ │ │ to sandbox FS │ │ └─ reads response, returns │ └─────────────────────┘ └──────────────────────────────┘ ``` **IPC Protocol:** - **Request:** Script writes `__DA_REQ__<uuid> <json>` to stderr (single line, atomic under `PIPE_BUF`) - **Response:** Engine writes to `/tmp/.da_ipc/res/<uuid>` (status line + payload) - **Why stderr?** So markers aren't captured by bash `$(...)` command substitution ### What's included **Core PTC engine** (`libs/deepagents/src/sandbox-ptc/`): - `engine.ts` — `PtcExecutionEngine` with `StdoutScanner`, IPC loop, tool call tracing - `runtimes.ts` — Bash, Python, and Node.js runtime libraries (embedded strings) - `middleware.ts` — `createSandboxPtcMiddleware` (opt-in, intercepts `execute` via `wrapToolCall`) - `prompt.ts` — Auto-generates system prompt from tool schemas and subagent types - `types.ts` — `PtcToolCallTrace`, `PtcExecuteResult`, IPC types **Protocol extension** (`libs/deepagents/src/backends/protocol.ts`): - `InteractiveProcess` interface (streaming stdout/stderr + `writeFile` + `waitForExit`) - `spawnInteractive()` method on `SandboxBackendProtocol` (optional, enables PTC) **Provider implementations** (each adds `spawnInteractive()`): - `@langchain/deno` — native `ReadableStream` from `sandbox.spawn()` - `@langchain/modal` — `ReadableStream` from `sandbox.exec()` - `@langchain/node-vfs` — Node.js `child_process` streams - `@langchain/daytona` — background process + polling (SDK has no streaming API) **Example** (`examples/sandbox/sandbox-ptc.ts`): - Full `createDeepAgent` integration with PTC middleware + analyst subagent - 100 parallel `tool_call classify_record` + 100 parallel `spawn_agent "analyst"` - Downloads deliverables (classifications + analyst reports) from sandbox to local disk ### Usage ```typescript import { createDeepAgent, createSandboxPtcMiddleware } from "deepagents"; const agent = createDeepAgent({ model: "claude-sonnet-4-5-20250929", backend: sandbox, subagents: [ { name: "analyst", description: "...", systemPrompt: "..." }, ], middleware: [ createSandboxPtcMiddleware({ backend: sandbox, ptc: true }), ], }); ``` The middleware auto-injects a system prompt documenting `tool_call` and `spawn_agent` with per-tool API references generated from schemas. No manual prompt engineering needed. ### Tool call tracing Every PTC-instrumented `execute` result includes a trace summary: ``` [PTC: 200 tool calls (classify_record=100, task=100), 200 succeeded, 0 failed, 1823ms total] ``` The engine also returns structured `PtcToolCallTrace[]` with name, input, result/error, and duration per call. --- <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:41 -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#328