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. The sandbox is created externally and passed in; the agent does not create or dispose it. Skills are declared separately on the context with skills(availableSkills).
Sandbox Options
| Approach | When to use | Docker? |
|---|---|---|
createVirtualSandbox() + createBashTool() | Local dev, tests, lightweight tasks | No |
createDockerSandbox() + createBashTool() | Real binaries in isolated Docker containers | Yes |
createDaytonaSandbox(client, options) + createBashTool() | Managed Daytona sandboxes | No |
createBashTool({ sandbox: e2bSandbox }) | Other cloud sandboxes via adapter | No |
createBashTool is owned by @deepagents/context; it adds AI SDK bash and file tools to an explicit backend and returns an AgentSandbox. It has no skills-specific behavior.
import { InMemoryFs } from 'just-bash';
import { createBashTool, createVirtualSandbox } from '@deepagents/context';
// Simplest: just-bash virtual sandbox (no Docker needed)
const sandbox = await createBashTool({
sandbox: await createVirtualSandbox({ fs: new InMemoryFs() }),
});import { createBashTool, createDockerSandbox, pkg } from '@deepagents/context';
// Pick the backend, then layer the bash tool.
const backend = await createDockerSandbox({
installers: [pkg(['curl', 'jq'])],
});
const sandbox = await createBashTool({ sandbox: backend });
// Don't forget: await sandbox.sandbox.dispose() when doneExposing 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.js → sql). 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 } from '@daytona/sdk';
import {
DAYTONA_DEFAULT_DESTINATION,
createBashTool,
createDaytonaSandbox,
} from '@deepagents/context';
const client = new Daytona({
apiKey: process.env.DAYTONA_API_KEY!,
});
// Pass a caller-owned Daytona client plus either a stable `name`
// (get-or-create) or `sandboxId` (attach). Authentication, base URL, and
// other Daytona config live on `new Daytona(...)`, not on
// `createDaytonaSandbox(...)`. The sandbox lifecycle stays with that client.
const backend = await createDaytonaSandbox(client, {
name: 'agent-backend',
image: 'your-registry/agent-image:latest',
});
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 donedispose() detaches local handles but does not delete a Daytona sandbox. Reuse
stable name values when you want get-or-create semantics across runs, or pass
sandboxId to attach to a specific sandbox directly. An unnamed sandbox cannot
be reclaimed, so createDaytonaSandbox(...) requires one of those two
identifiers.
The agent merges sandbox.tools into its toolset automatically. Any tools you pass override sandbox tools on name conflict.
Host-only Tool Metadata
Any object-returning tool can include a top-level meta field for information
the application should retain without sending it back to the model. No
per-tool toModelOutput is required; the agent applies the projection to its
entire toolset for generation, streaming, and persisted-history replay.
import { tool } from 'ai';
import { z } from 'zod';
import { agent } from '@deepagents/context';
const inspect = tool({
description: 'Inspect a resource',
inputSchema: z.object({ id: z.string() }),
execute: async ({ id }) => ({
status: 'ready',
meta: {
requestId: `request-${id}`,
durationMs: 18,
},
}),
});
const assistant = agent({
name: 'inspector',
sandbox,
context,
model,
tools: { inspect },
});Host-side tool results and UI messages retain both status and meta. The
model sees only { status: 'ready' }. A tool that defines its own
toModelOutput keeps that custom projection.
Host-only means hidden from the model, not secret: metadata can still be
streamed, persisted, logged, or sent to a client. Keep sensitive values in a
server-side store and put only an opaque reference in meta.
Creating an Agent
import { groq } from '@ai-sdk/groq';
import { InMemoryFs } from 'just-bash';
import {
ContextEngine,
SqliteContextStore,
agent,
createBashTool,
createVirtualSandbox,
role,
} from '@deepagents/context';
const sandbox = await createBashTool({
sandbox: await createVirtualSandbox({ fs: new InMemoryFs() }),
});
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
| Option | Type | Default | Description |
|---|---|---|---|
name | string | required | Identifier for logging and error messages |
sandbox | AgentSandbox | required | Sandbox providing bash, readFile, and writeFile tools — created via createBashTool() (optionally chained on top of createDockerSandbox() or createDaytonaSandbox(client, options)) from @deepagents/context |
context | ContextEngine | undefined | Engine that resolves system prompt and messages |
model | LanguageModelV4 | undefined | AI SDK model to use for generation |
tools | ToolSet | {} | Additional AI SDK tools — merged with sandbox tools (user tools override sandbox tools on name conflict) |
toolChoice | ToolChoice | undefined | Controls how the model selects tools |
providerOptions | object | undefined | Provider-specific options forwarded to the AI SDK |
telemetry | object | undefined | AI SDK telemetry settings forwarded unchanged to generateText and streamText |
stopWhen | StopCondition | isStepCount(200) | Ends the agent loop. Raise it for a long agentic run that would otherwise stop mid-task; lower it to bound cost on an untrusted prompt |
guardrails | Guardrail[] | [] | Guardrails applied during streaming |
maxGuardrailRetries | number | 3 | Max retry attempts when a guardrail fails |
logging | boolean | undefined | Enable debug logging |
Instance Properties
The agent exposes these readonly properties after creation:
| Property | Type | Description |
|---|---|---|
tools | ToolSet | Merged toolset (sandbox tools + user tools) |
context | ContextEngine | undefined | The context engine bound to this agent |
model | LanguageModelV4 | undefined | The 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:
| Argument | Type | Description |
|---|---|---|
contextVariables | CIn | Passed as AI SDK runtimeContext and as each tool's context |
config.abortSignal | AbortSignal | Cancel 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:
| Argument | Type | Description |
|---|---|---|
contextVariables | CIn | Passed as AI SDK runtimeContext and as each tool's context |
config.abortSignal | AbortSignal | Cancel the request |
config.transform | StreamTextTransform | StreamTextTransform[] | Custom stream transforms (defaults to smoothStream()) |
config.maxRetries | number | Override 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:
- Forks the current
ContextEngine - Appends the tool input as a
user(...)message in that fork - Runs a one-shot
generate()call with the child context - 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
| Option | Type | Description |
|---|---|---|
toolDescription | string | Override 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
| Option | Type | Description |
|---|---|---|
maxUses | number | Hard cap on total tool invocations for this advisor instance |
maxConversationUses | number | Cap on successful advisor responses before the tool returns max_uses_exceeded |
maxOutputTokens | number | Maximum output tokens for the advisor response |
concise | boolean | string | Use 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 { groq } from '@ai-sdk/groq';
import z from 'zod';
import {
ContextEngine,
SqliteContextStore,
role,
structuredOutput,
user,
} from '@deepagents/context';
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
| Option | Type | Default | Description |
|---|---|---|---|
context | ContextEngine | required | Engine that resolves system prompt and messages |
model | LanguageModelV4 | required | AI SDK model |
schema | z.ZodType | required | Zod schema for the output type |
providerOptions | object | undefined | Provider-specific options |
telemetry | object | undefined | AI SDK telemetry settings forwarded unchanged to generateText and streamText |
tools | ToolSet | undefined | Tools 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
- Context Engine - Full ContextEngine API reference
- Fragments - Building context with fragments
- Renderers - How context is rendered for the model
- Checkpoints - Named restore points for branching