
An agent that goes straight to the goal.
Minimal TypeScript agent loop built with Bun.
Hook into every step of the agent’s execution using hookable.
Built to be embedded in other projects easily, extended through providers, harnesses, and execution contexts.
# Install
bun install
# Authenticate with Anthropic OAuth (Claude Pro/Max)
bun run auth
# Run
bun start --prompt "create a hello world express app"
bun start \
--prompt "your task" \ # required
--model claude-opus-4-6 \ # model id (default: claude-opus-4-6)
--provider anthropic \ # anthropic | openrouter | cerebras
--harness basic \ # tool set to use
--system "be concise" \ # system prompt
--thinking off \ # off | minimal | low | medium | high
--context process # process | docker
An execution context defines where the agent’s tools run. The agent gets a context at creation, and all tool operations (shell, filesystem) go through it.
Runs in the same Node/Bun process. No isolation, fastest.
import { createAgent, createProcessContext } from 'zidane'
const agent = createAgent({
harness,
provider,
// execution defaults to createProcessContext()
})
Full container isolation via dockerode. Configurable resource limits.
# CLI
bun start --prompt "run uname -a" --context docker
bun start --prompt "build the app" --context docker --image node:22 --cwd /workspace
import { createAgent, createDockerContext } from 'zidane'
const agent = createAgent({
harness,
provider,
execution: createDockerContext({
image: 'node:22',
cwd: '/workspace',
limits: { memory: 512, cpu: '1.0' },
}),
})
Requires dockerode as a peer dependency: bun add dockerode
Offloads execution to a remote sandbox API. Implement the SandboxProvider interface for your provider (Rivet, E2B, etc.).
import { createAgent, createSandboxContext } from 'zidane'
import type { SandboxProvider } from 'zidane'
const myProvider: SandboxProvider = {
name: 'my-sandbox',
spawn: async (config) => { /* ... */ },
exec: async (id, command) => { /* ... */ },
readFile: async (id, path) => { /* ... */ },
writeFile: async (id, path, content) => { /* ... */ },
listFiles: async (id, path) => { /* ... */ },
destroy: async (id) => { /* ... */ },
}
const agent = createAgent({
harness,
provider,
execution: createSandboxContext(myProvider),
})
All contexts implement the same interface:
interface ExecutionContext {
type: 'process' | 'docker' | 'sandbox'
capabilities: { shell, filesystem, network, gpu }
spawn(config?): Promise<ExecutionHandle>
exec(handle, command, options?): Promise<ExecResult>
readFile(handle, path): Promise<string>
writeFile(handle, path, content): Promise<void>
listFiles(handle, path): Promise<string[]>
destroy(handle): Promise<void>
}
Access the context from a running agent:
agent.execution // ExecutionContext
agent.execution.type // 'process' | 'docker' | 'sandbox'
agent.handle // ExecutionHandle (after first run)
await agent.destroy() // clean up context resources
Direct Anthropic API with OAuth and API key support.
# OAuth (Claude Pro/Max subscription)
bun run auth
# Or API key
ANTHROPIC_API_KEY=sk-ant-... bun start --prompt "hello"
Access 200+ models through OpenRouter’s unified API.
OPENROUTER_API_KEY=sk-or-... bun start \
--provider openrouter \
--model anthropic/claude-sonnet-4-6 \
--prompt "hello"
Ultra-fast inference on Cerebras wafer-scale hardware.
CEREBRAS_API_KEY=csk-... bun start \
--provider cerebras \
--model zai-glm-4.7 \
--prompt "hello"
Extended reasoning for complex tasks. Maps to Anthropic’s thinking API or OpenRouter’s :thinking variant.
bun start --prompt "solve this proof" --thinking high
| Level | Budget |
|---|---|
off |
disabled |
minimal |
1k tokens |
low |
4k tokens |
medium |
10k tokens |
high |
32k tokens |
Tools are grouped into harnesses. The basic harness includes:
| Tool | Description |
|---|---|
shell |
Execute shell commands |
read_file |
Read file contents |
write_file |
Write/create files |
list_files |
List directory contents |
All paths are sandboxed to the working directory.
The agent uses hookable for lifecycle events. Every hook receives a mutable context object.
agent.hooks.hook('system:before', (ctx) => {
// ctx.system — system prompt text
})
agent.hooks.hook('turn:before', (ctx) => {
// ctx.turn — turn number
// ctx.options — StreamOptions being sent to provider
})
agent.hooks.hook('turn:after', (ctx) => {
// ctx.turn, ctx.usage { input, output }
})
agent.hooks.hook('agent:done', (ctx) => {
// ctx.totalIn, ctx.totalOut, ctx.turns, ctx.elapsed
})
agent.hooks.hook('agent:abort', () => {
// fired when agent.abort() is called
})
agent.hooks.hook('stream:text', (ctx) => {
// ctx.delta — new text chunk
// ctx.text — accumulated text so far
})
agent.hooks.hook('stream:end', (ctx) => {
// ctx.text — final complete text
})
agent.hooks.hook('tool:before', (ctx) => {
// ctx.name, ctx.input
})
agent.hooks.hook('tool:after', (ctx) => {
// ctx.name, ctx.input, ctx.result
})
agent.hooks.hook('tool:error', (ctx) => {
// ctx.name, ctx.input, ctx.error
})
Mutate ctx.block = true to prevent a tool from running.
agent.hooks.hook('tool:gate', (ctx) => {
if (ctx.name === 'shell' && String(ctx.input.command).includes('rm -rf')) {
ctx.block = true
ctx.reason = 'dangerous command'
}
})
Mutate ctx.result or ctx.isError to transform tool results before they’re sent back to the model.
agent.hooks.hook('tool:transform', (ctx) => {
if (ctx.result.length > 5000)
ctx.result = ctx.result.slice(0, 5000) + '\n... (truncated)'
})
Mutate ctx.messages before each LLM call for context window management.
agent.hooks.hook('context:transform', (ctx) => {
if (ctx.messages.length > 30)
ctx.messages.splice(2, ctx.messages.length - 30)
})
Inject a message while the agent is working. Delivered between tool calls, skipping remaining tools in the current turn.
agent.hooks.hook('tool:after', () => {
agent.steer('focus only on the tests directory')
})
Queue messages that extend the conversation after the agent finishes.
agent.followUp('now write tests for what you built')
agent.followUp('then update the README')
Execute multiple tool calls from a single turn concurrently.
const agent = createAgent({
harness: 'basic',
provider,
toolExecution: 'parallel', // default: 'sequential'
})
Pass images alongside the prompt.
import { readFileSync } from 'fs'
await agent.run({
prompt: 'describe this screenshot',
images: [{
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: readFileSync('screenshot.png').toString('base64'),
},
}],
})
agent.isRunning // boolean — is a run in progress?
agent.messages // Message[] — conversation history
agent.execution // ExecutionContext — where tools run
agent.handle // ExecutionHandle — spawned context handle
agent.abort() // cancel the current run
agent.reset() // clear messages and queues
await agent.destroy() // clean up execution context resources
await agent.waitForIdle() // wait for current run to complete
src/
types.ts shared types
agent.ts createAgent, state management
loop.ts turn execution loop
start.ts CLI entrypoint
auth.ts Anthropic OAuth flow
contexts/
types.ts ExecutionContext interface, capabilities
process.ts in-process context (default)
docker.ts Docker container context
sandbox.ts remote sandbox context
index.ts barrel exports
tools/
validation.ts tool argument validation
providers/
index.ts Provider interface
openai-compat.ts shared OpenAI-compatible utilities
anthropic.ts Anthropic provider
openrouter.ts OpenRouter provider
cerebras.ts Cerebras provider
harnesses/
index.ts harness registry
basic.ts shell, read, write, list tools
output/
terminal.ts terminal rendering (md4x)
test/
mock-provider.ts mock provider for testing
mock-context.ts mock execution context for testing
agent.test.ts agent loop tests
contexts.test.ts execution context tests
harness.test.ts harness tests
validation.test.ts validation tests
providers.test.ts provider tests
bun test
125 tests with mock provider and mock execution context — no LLM calls or Docker needed.
ISC
We use cookies
We use cookies to analyze traffic and improve your experience. You can accept or reject analytics cookies.