Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Agent Wrapper

Convenience wrapper that combines ContextEngine with the AI SDK into a streamable agent

The agent() function combines a ContextEngine and an AI SDK model into a single object with generate(), stream(), clone(), asTool(), and asAdvisor() methods. It resolves context, repairs tool calls when needed, and can retry streamed output through guardrails without extra orchestration in your app.

Every agent requires a sandbox — an AgentSandbox that provides bash, readFile, and writeFile tools plus a skills array. The sandbox is created externally and passed in; the agent does not create or dispose it.

Sandbox Options

ApproachWhen to useDocker?Supports skills
createBashTool()Local dev, tests, lightweight tasksNoYes
createDockerSandbox() + createBashTool()Real binaries in isolated Docker containersYesYes
createDaytonaSandbox() + createBashTool()Managed Daytona cloud sandboxesNoYes
createBashTool({ sandbox: e2bSandbox })Other cloud sandboxes via adapterNoYes

Import createBashTool from @deepagents/context (not the raw bash-tool package). The @deepagents/context wrapper adds the skills option and always returns an AgentSandbox.

import { createBashTool } from '@deepagents/context';

// Simplest: just-bash virtual sandbox (no Docker needed)
const sandbox = await createBashTool();
import {
  createBashTool,
  createDockerSandbox,
  pkg,
} from '@deepagents/context';

// Docker with skills: pick the backend, then layer the bash tool.
const backend = await createDockerSandbox({
  installers: [pkg(['curl', 'jq'])],
});
const sandbox = await createBashTool({
  sandbox: backend,
  skills: [{ host: './skills', sandbox: '/workspace/skills' }],
});
// Don't forget: await sandbox.sandbox.dispose() when done

Exposing local binaries on PATH

bin() symlinks a binary that already exists inside the container — typically reached via a bind-mounted workspace — onto PATH. The dev loop becomes nx run <pkg>:build → rerun the agent. No docker build, no esbuild bake-in, no re-published package.

import {
  bin,
  createBashTool,
  createDockerSandbox,
} from '@deepagents/context';

const backend = await createDockerSandbox({
  image: 'node:lts-alpine',
  installers: [bin('/workspace/packages/text2sql/dist/bin/sql.js')],
  volumes: [
    {
      type: 'bind',
      hostPath: process.cwd(),
      containerPath: '/workspace',
      readOnly: true,
    },
  ],
});
const sandbox = await createBashTool({ sandbox: backend });

// `sql` is now on PATH inside the container.
await sandbox.sandbox.executeCommand('sql --help');

The command name defaults to the binary basename without its extension (sql.jssql). Override with { name } or { target }:

bin('/opt/tools/cli.mjs', { name: 'tool' });
bin('/opt/tools/cli.mjs', { target: '/opt/bin/tool' });

bin() requires the file to exist at install time — a missing path throws InstallError with source: 'bin' instead of silently creating a dangling symlink. The chmod +x step is skipped when the file is already executable, so read-only bind mounts of pre-built artifacts work without a writable mount.

import {
  DAYTONA_DEFAULT_DESTINATION,
  createBashTool,
  createDaytonaSandbox,
} from '@deepagents/context';

// Daytona reads DAYTONA_* env vars; mkdir the workspace before wiring the bash tool.
const backend = await createDaytonaSandbox();
await backend.executeCommand(`mkdir -p ${DAYTONA_DEFAULT_DESTINATION}`);
const sandbox = await createBashTool({
  sandbox: backend,
  destination: DAYTONA_DEFAULT_DESTINATION,
});
// Don't forget: await sandbox.sandbox.dispose() when done

The agent merges sandbox.tools into its toolset automatically. Any tools you pass override sandbox tools on name conflict.

Creating an Agent

import {
  agent,
  ContextEngine,
  SqliteContextStore,
  createBashTool,
  role,
} from '@deepagents/context';
import { groq } from '@ai-sdk/groq';

const sandbox = await createBashTool();
const store = new SqliteContextStore('./chat.db');
const context = new ContextEngine({
  store,
  chatId: 'chat-001',
  userId: 'user-001',
}).set(role('You are a helpful coding assistant.'));

const assistant = agent({
  name: 'coding_assistant',
  sandbox,
  context,
  model: groq('gpt-oss-20b'),
});

Configuration

OptionTypeDefaultDescription
namestringrequiredIdentifier for logging and error messages
sandboxAgentSandboxrequiredSandbox providing bash, readFile, writeFile tools plus skills — created via createBashTool() (optionally chained on top of createDockerSandbox() or createDaytonaSandbox()) from @deepagents/context
contextContextEngineundefinedEngine that resolves system prompt and messages
modelLanguageModelV3undefinedAI SDK model to use for generation
toolsToolSet{}Additional AI SDK tools — merged with sandbox tools (user tools override sandbox tools on name conflict)
toolChoiceToolChoiceundefinedControls how the model selects tools
providerOptionsobjectundefinedProvider-specific options forwarded to the AI SDK
experimental_telemetryobjectundefinedAI SDK telemetry settings forwarded unchanged to generateText and streamText
guardrailsGuardrail[][]Guardrails applied during streaming
maxGuardrailRetriesnumber3Max retry attempts when a guardrail fails
loggingbooleanundefinedEnable debug logging

Instance Properties

The agent exposes these readonly properties after creation:

PropertyTypeDescription
toolsToolSetMerged toolset (sandbox tools + user tools)
contextContextEngine | undefinedThe context engine bound to this agent
modelLanguageModelV3 | undefinedThe model configured for this agent

generate()

Resolves the context, calls the model, and returns the full result. Useful when you need the complete response before continuing.

import { user } from '@deepagents/context';

context.set(user('Explain closures in JavaScript'));
await context.save();

const result = await assistant.generate({});
console.log(result.text);

generate() accepts two arguments:

ArgumentTypeDescription
contextVariablesCInPassed as experimental_context to the AI SDK
config.abortSignalAbortSignalCancel the request

config.abortSignal is forwarded to the model call and any tool-call repair attempt, so cancellation stops the full request instead of only the outer wrapper.

stream()

Resolves the context and returns a streaming result. When guardrails are configured, toUIMessageStream() is wrapped to intercept parts and retry on failure.

context.set(user('Write a Python quicksort'));
await context.save();

const stream = await assistant.stream({});

for await (const part of stream.toUIMessageStream()) {
  if (part.type === 'text-delta') {
    process.stdout.write(part.delta);
  }
}

stream() accepts two arguments:

ArgumentTypeDescription
contextVariablesCInPassed as experimental_context to the AI SDK
config.abortSignalAbortSignalCancel the request
config.transformStreamTextTransform | StreamTextTransform[]Custom stream transforms (defaults to smoothStream())
config.maxRetriesnumberOverride maxGuardrailRetries for this call

config.abortSignal is reused across repair attempts and guardrail retries, so cancelling a stream stops the active turn cleanly.

clone()

Creates a new agent with the same options, selectively overridden. Handy for switching models or tools without rebuilding everything.

const creativeAssistant = assistant.clone({
  name: 'creative_assistant',
  providerOptions: { temperature: 0.9 },
});

const result = await creativeAssistant.generate({});

asTool()

Convert an agent into a regular AI SDK tool so another agent can call it for a bounded sub-task. asTool() requires both context and model.

When the tool runs, the wrapper:

  1. Forks the current ContextEngine
  2. Appends the tool input as a user(...) message in that fork
  3. Runs a one-shot generate() call with the child context
  4. Returns either the extracted output or the raw tool results

That keeps the parent agent's persisted thread untouched while reusing the same system fragments and configuration.

const researcher = agent({
  name: 'researcher',
  sandbox,
  context,
  model: groq('gpt-oss-20b'),
});

const writer = agent({
  name: 'writer',
  sandbox,
  context,
  model: groq('gpt-oss-20b'),
  tools: {
    research: researcher.asTool({
      toolDescription: 'Research the topic and return a concise brief',
      outputExtractor: async (result) => result.text,
    }),
  },
});

asTool() Options

OptionTypeDescription
toolDescriptionstringOverride the generated tool description
outputExtractor(result) => string | Promise<string>Transform the sub-agent result before the parent receives it

asAdvisor()

Expose the same agent as a no-input advisor tool backed by its configured model. The advisor receives the full executor conversation history automatically, so callers use it for strategy checks rather than raw execution work.

const reviewer = agent({
  name: 'reviewer',
  sandbox,
  context,
  model: groq('gpt-oss-20b'),
});

const { tool: advisor, usage } = reviewer.asAdvisor({
  concise: true,
  maxConversationUses: 2,
});

const worker = agent({
  name: 'worker',
  sandbox,
  context,
  model: groq('gpt-oss-20b'),
  tools: { advisor },
});

console.log(usage());

usage() reports successful advisor calls plus aggregated model usage for the returned advisor instance.

asAdvisor() Options

OptionTypeDescription
maxUsesnumberHard cap on total tool invocations for this advisor instance
maxConversationUsesnumberCap on successful advisor responses before the tool returns max_uses_exceeded
maxOutputTokensnumberMaximum output tokens for the advisor response
conciseboolean | stringUse the built-in concise prompt, or provide custom advisor instructions

The tool returns short machine-readable status codes such as too_many_requests, prompt_too_long, or execution_time_exceeded when the advisor request cannot complete cleanly.

Guardrails

Pass guardrails and maxGuardrailRetries to add real-time stream interception. When a guardrail fails, the agent emits a finish step, persists the corrected assistant state, and retries with self-correction feedback.

import { agent, errorRecoveryGuardrail } from '@deepagents/context';

const guarded = agent({
  name: 'guarded_assistant',
  sandbox,
  context,
  model: groq('gpt-oss-20b'),
  guardrails: [errorRecoveryGuardrail],
  maxGuardrailRetries: 3,
});

See the Guardrails reference for the full API — writing custom guardrails, the pass/fail/stop model, GuardrailContext, chaining, and built-in error patterns. For a practical walkthrough, see the Error Recovery recipe.

structuredOutput()

Returns type-safe structured data parsed against a Zod schema. Separate from agent() because it uses Output.object() under the hood.

import { structuredOutput, ContextEngine, SqliteContextStore, role, user } from '@deepagents/context';
import { groq } from '@ai-sdk/groq';
import z from 'zod';

const store = new SqliteContextStore('./chat.db');
const context = new ContextEngine({
  store,
  chatId: 'extract-001',
  userId: 'user-001',
}).set(role('Extract structured data from the user message.'));

const PersonSchema = z.object({
  name: z.string(),
  age: z.number(),
  occupation: z.string(),
});

const extractor = structuredOutput({
  context,
  model: groq('gpt-oss-20b'),
  schema: PersonSchema,
});

context.set(user('John is a 30-year-old software engineer.'));
await context.save();

const person = await extractor.generate({});
// { name: "John", age: 30, occupation: "software engineer" }

structuredOutput Configuration

OptionTypeDefaultDescription
contextContextEnginerequiredEngine that resolves system prompt and messages
modelLanguageModelV3requiredAI SDK model
schemaz.ZodTyperequiredZod schema for the output type
providerOptionsobjectundefinedProvider-specific options
experimental_telemetryobjectundefinedAI SDK telemetry settings forwarded unchanged to generateText and streamText
toolsToolSetundefinedTools available during generation

structuredOutput Methods

generate() resolves context, calls the model with Output.object(), and returns the parsed value directly.

stream() returns the full StreamTextResult with the structured output schema applied. Use this when you need to process stream parts before the final value.

const stream = await extractor.stream({});

for await (const part of stream.toUIMessageStream()) {
  if (part.type === 'text-delta') {
    process.stdout.write(part.delta);
  }
}

How Context Resolution Works

Both agent() and structuredOutput() call context.resolve() with an XmlRenderer internally. The resolved system prompt and messages are forwarded to the AI SDK's generateText or streamText. You don't need to call resolve() yourself when using these wrappers.

agent.generate() / agent.stream()
  └─ context.resolve({ renderer: XmlRenderer })
       └─ { systemPrompt, messages }
            └─ generateText() / streamText()

Next Steps