zidane 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +723 -0
- package/dist/agent-DvZm8U14.d.ts +313 -0
- package/dist/chunk-26LIQARN.js +109 -0
- package/dist/chunk-27EP7HB3.js +1005 -0
- package/dist/chunk-34KXKPNN.js +45 -0
- package/dist/chunk-LMSOIIAT.js +274 -0
- package/dist/chunk-LS57GDAV.js +365 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/harnesses.d.ts +7 -0
- package/dist/harnesses.js +13 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +57 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +11 -0
- package/dist/providers.d.ts +65 -0
- package/dist/providers.js +257 -0
- package/dist/session.d.ts +167 -0
- package/dist/session.js +27 -0
- package/dist/spawn-pP2grsVp.d.ts +63 -0
- package/dist/tools.d.ts +28 -0
- package/dist/tools.js +20 -0
- package/dist/types-4CFQ-6Qu.d.ts +94 -0
- package/package.json +69 -6
- package/index.js +0 -1
- package/zidane.jpeg +0 -0
package/README.md
CHANGED
|
@@ -1 +1,724 @@
|
|
|
1
1
|

|
|
2
|
+
|
|
3
|
+
# Zidane
|
|
4
|
+
|
|
5
|
+
An agent that goes straight to the goal.
|
|
6
|
+
|
|
7
|
+
Minimal TypeScript agent loop built with [Bun](https://bun.sh).
|
|
8
|
+
|
|
9
|
+
Hook into every step of the agent's execution using [hookable](https://github.com/unjs/hookable).
|
|
10
|
+
|
|
11
|
+
Built to be embedded in other projects easily, extended through [providers](#providers), [harnesses](#harnesses), and [execution contexts](#execution-contexts).
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Install
|
|
17
|
+
bun install
|
|
18
|
+
|
|
19
|
+
# Authenticate with Anthropic OAuth (Claude Pro/Max)
|
|
20
|
+
bun run auth
|
|
21
|
+
|
|
22
|
+
# Run
|
|
23
|
+
bun start --prompt "create a hello world express app"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## CLI
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun start \
|
|
30
|
+
--prompt "your task" \ # required
|
|
31
|
+
--model claude-opus-4-6 \ # model id (default: claude-opus-4-6)
|
|
32
|
+
--provider anthropic \ # anthropic | openrouter | cerebras
|
|
33
|
+
--harness basic \ # tool set to use
|
|
34
|
+
--system "be concise" \ # system prompt
|
|
35
|
+
--thinking off \ # off | minimal | low | medium | high
|
|
36
|
+
--context process \ # process | docker
|
|
37
|
+
--mcp '{"name":"fs","transport":"stdio","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","."]}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `--mcp` flag accepts a JSON object matching `McpServerConfig`. It can be passed multiple times.
|
|
41
|
+
|
|
42
|
+
## Execution Contexts
|
|
43
|
+
|
|
44
|
+
An execution context defines **where** the agent's tools run. All tool operations (shell, filesystem) go through it.
|
|
45
|
+
|
|
46
|
+
### In-process (default)
|
|
47
|
+
|
|
48
|
+
Runs in the same Node/Bun process. No isolation, fastest.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createAgent, createProcessContext } from 'zidane'
|
|
52
|
+
|
|
53
|
+
const agent = createAgent({
|
|
54
|
+
harness,
|
|
55
|
+
provider,
|
|
56
|
+
// execution defaults to createProcessContext()
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Docker
|
|
61
|
+
|
|
62
|
+
Full container isolation via [dockerode](https://github.com/apocas/dockerode). Configurable resource limits.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# CLI
|
|
66
|
+
bun start --prompt "run uname -a" --context docker
|
|
67
|
+
bun start --prompt "build the app" --context docker --image node:22 --cwd /workspace
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createAgent, createDockerContext } from 'zidane'
|
|
72
|
+
|
|
73
|
+
const agent = createAgent({
|
|
74
|
+
harness,
|
|
75
|
+
provider,
|
|
76
|
+
execution: createDockerContext({
|
|
77
|
+
image: 'node:22',
|
|
78
|
+
cwd: '/workspace',
|
|
79
|
+
limits: { memory: 512, cpu: '1.0' },
|
|
80
|
+
}),
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requires `dockerode` as a peer dependency: `bun add dockerode`
|
|
85
|
+
|
|
86
|
+
### Sandbox (remote)
|
|
87
|
+
|
|
88
|
+
Offloads execution to a remote sandbox API. Implement the `SandboxProvider` interface for your provider (Rivet, E2B, etc.).
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createAgent, createSandboxContext } from 'zidane'
|
|
92
|
+
import type { SandboxProvider } from 'zidane'
|
|
93
|
+
|
|
94
|
+
const myProvider: SandboxProvider = {
|
|
95
|
+
name: 'my-sandbox',
|
|
96
|
+
spawn: async (config) => { /* ... */ },
|
|
97
|
+
exec: async (id, command) => { /* ... */ },
|
|
98
|
+
readFile: async (id, path) => { /* ... */ },
|
|
99
|
+
writeFile: async (id, path, content) => { /* ... */ },
|
|
100
|
+
listFiles: async (id, path) => { /* ... */ },
|
|
101
|
+
destroy: async (id) => { /* ... */ },
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const agent = createAgent({
|
|
105
|
+
harness,
|
|
106
|
+
provider,
|
|
107
|
+
execution: createSandboxContext(myProvider),
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Execution Context Interface
|
|
112
|
+
|
|
113
|
+
All contexts implement the same interface:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
interface ExecutionContext {
|
|
117
|
+
type: 'process' | 'docker' | 'sandbox'
|
|
118
|
+
capabilities: { shell, filesystem, network, gpu }
|
|
119
|
+
spawn(config?): Promise<ExecutionHandle>
|
|
120
|
+
exec(handle, command, options?): Promise<ExecResult>
|
|
121
|
+
readFile(handle, path): Promise<string>
|
|
122
|
+
writeFile(handle, path, content): Promise<void>
|
|
123
|
+
listFiles(handle, path): Promise<string[]>
|
|
124
|
+
destroy(handle): Promise<void>
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Access the context from a running agent:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
agent.execution // ExecutionContext
|
|
132
|
+
agent.execution.type // 'process' | 'docker' | 'sandbox'
|
|
133
|
+
agent.handle // ExecutionHandle (after first run)
|
|
134
|
+
await agent.destroy() // clean up context resources
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Providers
|
|
138
|
+
|
|
139
|
+
### Anthropic
|
|
140
|
+
|
|
141
|
+
Direct Anthropic API with OAuth and API key support.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# OAuth (Claude Pro/Max subscription)
|
|
145
|
+
bun run auth
|
|
146
|
+
|
|
147
|
+
# Or API key
|
|
148
|
+
ANTHROPIC_API_KEY=sk-ant-... bun start --prompt "hello"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### OpenRouter
|
|
152
|
+
|
|
153
|
+
Access 200+ models through OpenRouter's unified API.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
OPENROUTER_API_KEY=sk-or-... bun start \
|
|
157
|
+
--provider openrouter \
|
|
158
|
+
--model anthropic/claude-sonnet-4-6 \
|
|
159
|
+
--prompt "hello"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Cerebras
|
|
163
|
+
|
|
164
|
+
Ultra-fast inference on Cerebras wafer-scale hardware.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
CEREBRAS_API_KEY=csk-... bun start \
|
|
168
|
+
--provider cerebras \
|
|
169
|
+
--model zai-glm-4.7 \
|
|
170
|
+
--prompt "hello"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Thinking
|
|
174
|
+
|
|
175
|
+
Extended reasoning for complex tasks. Maps to Anthropic's thinking API or OpenRouter's `:thinking` variant.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
bun start --prompt "solve this proof" --thinking high
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Level | Budget |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `off` | disabled |
|
|
184
|
+
| `minimal` | 1k tokens |
|
|
185
|
+
| `low` | 4k tokens |
|
|
186
|
+
| `medium` | 10k tokens |
|
|
187
|
+
| `high` | 32k tokens |
|
|
188
|
+
|
|
189
|
+
## Tools (Harnesses)
|
|
190
|
+
|
|
191
|
+
Tools are grouped into **harnesses**. The `basic` harness includes:
|
|
192
|
+
|
|
193
|
+
| Tool | Description |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `shell` | Execute shell commands |
|
|
196
|
+
| `read_file` | Read file contents |
|
|
197
|
+
| `write_file` | Write/create files |
|
|
198
|
+
| `list_files` | List directory contents |
|
|
199
|
+
| `spawn` | Spawn a sub-agent for a task |
|
|
200
|
+
|
|
201
|
+
All paths are sandboxed to the working directory.
|
|
202
|
+
|
|
203
|
+
Define a custom harness with `defineHarness`:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { defineHarness } from 'zidane'
|
|
207
|
+
|
|
208
|
+
const harness = defineHarness({
|
|
209
|
+
name: 'researcher',
|
|
210
|
+
system: 'You are a research assistant.',
|
|
211
|
+
tools: { ...basicTools },
|
|
212
|
+
mcpServers: [
|
|
213
|
+
{ name: 'filesystem', transport: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '.'] },
|
|
214
|
+
],
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Sub-agent Spawning
|
|
219
|
+
|
|
220
|
+
The `spawn` tool lets the agent delegate tasks to child agents. Children run independently and return their result as a tool response.
|
|
221
|
+
|
|
222
|
+
### Static spawn tool
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { spawn, basicTools, defineHarness } from 'zidane'
|
|
226
|
+
|
|
227
|
+
const harness = defineHarness({
|
|
228
|
+
name: 'orchestrator',
|
|
229
|
+
tools: { ...basicTools, spawn },
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Children inherit the parent's harness (and can spawn their own children).
|
|
234
|
+
|
|
235
|
+
### Configurable factory
|
|
236
|
+
|
|
237
|
+
Use `createSpawnTool` when you need custom concurrency limits, model overrides, or lifecycle callbacks.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { createSpawnTool } from 'zidane'
|
|
241
|
+
|
|
242
|
+
const spawnTool = createSpawnTool({
|
|
243
|
+
maxConcurrent: 5,
|
|
244
|
+
model: 'claude-haiku-4-5-20251001',
|
|
245
|
+
system: 'You are a focused sub-agent.',
|
|
246
|
+
thinking: 'low',
|
|
247
|
+
onSpawn: (child) => console.log(`started ${child.id}`),
|
|
248
|
+
onComplete: (child, stats) => console.log(`${child.id} done in ${stats.turns} turns`),
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const harness = defineHarness({
|
|
252
|
+
name: 'orchestrator',
|
|
253
|
+
tools: { spawn: spawnTool },
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## MCP Servers
|
|
258
|
+
|
|
259
|
+
Connect any MCP-compatible tool server. Tools are namespaced as `mcp_{serverName}_{toolName}`.
|
|
260
|
+
|
|
261
|
+
### Agent-level
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const agent = createAgent({
|
|
265
|
+
harness,
|
|
266
|
+
provider,
|
|
267
|
+
mcpServers: [
|
|
268
|
+
{ name: 'filesystem', transport: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '.'] },
|
|
269
|
+
{ name: 'search', transport: 'sse', url: 'http://localhost:3001/sse' },
|
|
270
|
+
{ name: 'api', transport: 'streamable-http', url: 'http://localhost:3002/mcp' },
|
|
271
|
+
],
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Harness-level
|
|
276
|
+
|
|
277
|
+
MCP servers can also be declared on the harness so they're shared across all agents using it.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const harness = defineHarness({
|
|
281
|
+
name: 'with-mcp',
|
|
282
|
+
tools: { ...basicTools },
|
|
283
|
+
mcpServers: [
|
|
284
|
+
{ name: 'db', transport: 'stdio', command: 'node', args: ['db-server.js'] },
|
|
285
|
+
],
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
MCP connections are made lazily on the first `run()` call and reused across subsequent runs. They are closed when `agent.destroy()` is called.
|
|
290
|
+
|
|
291
|
+
## Sessions
|
|
292
|
+
|
|
293
|
+
Sessions give an agent persistent identity, message history, and run metadata across multiple calls or restarts.
|
|
294
|
+
|
|
295
|
+
### Creating a session
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { createSession, createMemoryStore } from 'zidane/session'
|
|
299
|
+
|
|
300
|
+
// In-memory (default, no persistence)
|
|
301
|
+
const session = createSession({ id: 'my-session', agentId: 'my-agent' })
|
|
302
|
+
|
|
303
|
+
// With a store for persistence
|
|
304
|
+
const store = createMemoryStore()
|
|
305
|
+
const session = createSession({ id: 'my-session', store })
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Storage backends
|
|
309
|
+
|
|
310
|
+
Three built-in stores are available:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { createMemoryStore, createSqliteStore, createRemoteStore } from 'zidane/session'
|
|
314
|
+
|
|
315
|
+
// In-memory, fast, no disk I/O, lost on process restart
|
|
316
|
+
const memStore = createMemoryStore()
|
|
317
|
+
|
|
318
|
+
// SQLite, persistent, zero-dependency (uses Bun's built-in SQLite)
|
|
319
|
+
const sqliteStore = createSqliteStore({ path: './sessions.db' })
|
|
320
|
+
|
|
321
|
+
// Remote HTTP, delegates to a custom REST API
|
|
322
|
+
const remoteStore = createRemoteStore({ url: 'https://api.example.com/sessions' })
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Agent integration
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const agent = createAgent({
|
|
329
|
+
harness,
|
|
330
|
+
provider,
|
|
331
|
+
session,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
await agent.run({ prompt: 'hello' })
|
|
335
|
+
await session.save() // persist to store
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Session hooks
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
agent.hooks.hook('session:start', (ctx) => {
|
|
342
|
+
// ctx.sessionId, ctx.runId, ctx.prompt
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
agent.hooks.hook('session:end', (ctx) => {
|
|
346
|
+
// ctx.sessionId, ctx.runId
|
|
347
|
+
// ctx.status: 'completed' | 'aborted' | 'error'
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
agent.hooks.hook('session:messages', (ctx) => {
|
|
351
|
+
// ctx.sessionId, ctx.count
|
|
352
|
+
// fired after each turn (live message sync)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
agent.hooks.hook('session:save', (ctx) => {
|
|
356
|
+
// ctx.sessionId
|
|
357
|
+
// fired after session.save() completes
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
agent.hooks.hook('session:meta', (ctx) => {
|
|
361
|
+
// ctx.sessionId, ctx.key, ctx.value
|
|
362
|
+
// fired when session.setMeta() is called
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Messages are synced to the session after every turn, not just at run start/end. If the agent crashes mid-run, you still have messages up to the last completed turn.
|
|
367
|
+
|
|
368
|
+
### Restoring a session
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { loadSession } from 'zidane/session'
|
|
372
|
+
|
|
373
|
+
const session = await loadSession(store, 'my-session')
|
|
374
|
+
if (session) {
|
|
375
|
+
const agent = createAgent({ harness, provider, session })
|
|
376
|
+
await agent.run({ prompt: 'continue from before' })
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Hooks
|
|
381
|
+
|
|
382
|
+
The agent uses [hookable](https://github.com/unjs/hookable) for lifecycle events. Every hook receives a mutable context object.
|
|
383
|
+
|
|
384
|
+
### Lifecycle
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
agent.hooks.hook('system:before', (ctx) => {
|
|
388
|
+
// ctx.system: system prompt text
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
agent.hooks.hook('turn:before', (ctx) => {
|
|
392
|
+
// ctx.turn: turn number
|
|
393
|
+
// ctx.options: StreamOptions being sent to provider
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
agent.hooks.hook('turn:after', (ctx) => {
|
|
397
|
+
// ctx.turn, ctx.usage { input, output }
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
agent.hooks.hook('agent:done', (ctx) => {
|
|
401
|
+
// ctx.totalIn, ctx.totalOut, ctx.turns, ctx.elapsed, ctx.children?
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
agent.hooks.hook('agent:abort', () => {
|
|
405
|
+
// fired when agent.abort() is called
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Streaming
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
agent.hooks.hook('stream:text', (ctx) => {
|
|
413
|
+
// ctx.delta: new text chunk
|
|
414
|
+
// ctx.text: accumulated text so far
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
agent.hooks.hook('stream:end', (ctx) => {
|
|
418
|
+
// ctx.text: final complete text
|
|
419
|
+
})
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Tool Execution
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
agent.hooks.hook('tool:before', (ctx) => {
|
|
426
|
+
// ctx.name, ctx.input
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
agent.hooks.hook('tool:after', (ctx) => {
|
|
430
|
+
// ctx.name, ctx.input, ctx.result
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
agent.hooks.hook('tool:error', (ctx) => {
|
|
434
|
+
// ctx.name, ctx.input, ctx.error
|
|
435
|
+
})
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Tool Gate: block execution
|
|
439
|
+
|
|
440
|
+
Mutate `ctx.block = true` to prevent a tool from running.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
agent.hooks.hook('tool:gate', (ctx) => {
|
|
444
|
+
if (ctx.name === 'shell' && String(ctx.input.command).includes('rm -rf')) {
|
|
445
|
+
ctx.block = true
|
|
446
|
+
ctx.reason = 'dangerous command'
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Tool Transform: modify output
|
|
452
|
+
|
|
453
|
+
Mutate `ctx.result` or `ctx.isError` to transform tool results before they're sent back to the model.
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
agent.hooks.hook('tool:transform', (ctx) => {
|
|
457
|
+
if (ctx.result.length > 5000)
|
|
458
|
+
ctx.result = ctx.result.slice(0, 5000) + '\n... (truncated)'
|
|
459
|
+
})
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Context Transform: prune messages
|
|
463
|
+
|
|
464
|
+
Mutate `ctx.messages` before each LLM call for context window management.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
agent.hooks.hook('context:transform', (ctx) => {
|
|
468
|
+
if (ctx.messages.length > 30)
|
|
469
|
+
ctx.messages.splice(2, ctx.messages.length - 30)
|
|
470
|
+
})
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Spawn hooks
|
|
474
|
+
|
|
475
|
+
Fired by the `spawn` tool when child agents are created.
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
agent.hooks.hook('spawn:before', (ctx) => {
|
|
479
|
+
// ctx.id: child agent id (e.g. 'child-1')
|
|
480
|
+
// ctx.task: the task prompt given to the child
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
agent.hooks.hook('spawn:complete', (ctx) => {
|
|
484
|
+
// ctx.id, ctx.task
|
|
485
|
+
// ctx.stats: AgentStats from the child run
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
agent.hooks.hook('spawn:error', (ctx) => {
|
|
489
|
+
// ctx.id, ctx.task, ctx.error
|
|
490
|
+
})
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### MCP hooks
|
|
494
|
+
|
|
495
|
+
Fired during MCP server lifecycle.
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
agent.hooks.hook('mcp:connect', (ctx) => {
|
|
499
|
+
// ctx.name: server name
|
|
500
|
+
// ctx.transport: 'stdio' | 'sse' | 'streamable-http'
|
|
501
|
+
// ctx.tools: namespaced tool names discovered on this server
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
agent.hooks.hook('mcp:error', (ctx) => {
|
|
505
|
+
// ctx.name: server name
|
|
506
|
+
// ctx.error: connection error
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
agent.hooks.hook('mcp:close', (ctx) => {
|
|
510
|
+
// ctx.name: server name being closed
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
agent.hooks.hook('mcp:tool:before', (ctx) => {
|
|
514
|
+
// ctx.server: MCP server name
|
|
515
|
+
// ctx.tool: original tool name (not namespaced)
|
|
516
|
+
// ctx.input: tool arguments
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
agent.hooks.hook('mcp:tool:after', (ctx) => {
|
|
520
|
+
// ctx.server, ctx.tool, ctx.input
|
|
521
|
+
// ctx.result: tool output string
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
agent.hooks.hook('mcp:tool:error', (ctx) => {
|
|
525
|
+
// ctx.server, ctx.tool, ctx.input, ctx.error
|
|
526
|
+
})
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Steering inject
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
agent.hooks.hook('steer:inject', (ctx) => {
|
|
533
|
+
// ctx.message: the steering message being injected
|
|
534
|
+
})
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Steering and Follow-up
|
|
538
|
+
|
|
539
|
+
### Steering: interrupt mid-run
|
|
540
|
+
|
|
541
|
+
Inject a message while the agent is working. Delivered between tool calls, skipping remaining tools in the current turn.
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
agent.hooks.hook('tool:after', () => {
|
|
545
|
+
agent.steer('focus only on the tests directory')
|
|
546
|
+
})
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Follow-up, continue after done
|
|
550
|
+
|
|
551
|
+
Queue messages that extend the conversation after the agent finishes.
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
agent.followUp('now write tests for what you built')
|
|
555
|
+
agent.followUp('then update the README')
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Parallel Tool Execution
|
|
559
|
+
|
|
560
|
+
Execute multiple tool calls from a single turn concurrently.
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
const agent = createAgent({
|
|
564
|
+
harness,
|
|
565
|
+
provider,
|
|
566
|
+
toolExecution: 'parallel', // default: 'sequential'
|
|
567
|
+
})
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Image Content
|
|
571
|
+
|
|
572
|
+
Pass images alongside the prompt.
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
import { readFileSync } from 'fs'
|
|
576
|
+
|
|
577
|
+
await agent.run({
|
|
578
|
+
prompt: 'describe this screenshot',
|
|
579
|
+
images: [{
|
|
580
|
+
type: 'image',
|
|
581
|
+
source: {
|
|
582
|
+
type: 'base64',
|
|
583
|
+
media_type: 'image/png',
|
|
584
|
+
data: readFileSync('screenshot.png').toString('base64'),
|
|
585
|
+
},
|
|
586
|
+
}],
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## Message Format
|
|
591
|
+
|
|
592
|
+
All messages in zidane use the canonical `SessionMessage` format, with or without sessions:
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
type SessionContentBlock =
|
|
596
|
+
| { type: 'text', text: string }
|
|
597
|
+
| { type: 'image', mediaType: string, data: string }
|
|
598
|
+
| { type: 'tool_call', id: string, name: string, input: Record<string, unknown> }
|
|
599
|
+
| { type: 'tool_result', callId: string, output: string, isError?: boolean }
|
|
600
|
+
| { type: 'thinking', text: string }
|
|
601
|
+
|
|
602
|
+
interface SessionMessage {
|
|
603
|
+
role: 'user' | 'assistant'
|
|
604
|
+
content: SessionContentBlock[]
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Providers convert to and from native wire formats internally. Converters are available for external interop:
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
import { fromAnthropic, toAnthropic, fromOpenAI, toOpenAI, autoDetectAndConvert } from 'zidane'
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
## Usage Tracking
|
|
615
|
+
|
|
616
|
+
Every turn reports token usage. Provider-specific fields are optional:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
interface TurnUsage {
|
|
620
|
+
input: number
|
|
621
|
+
output: number
|
|
622
|
+
cacheCreation?: number // Anthropic: tokens written to cache
|
|
623
|
+
cacheRead?: number // Anthropic: tokens read from cache
|
|
624
|
+
thinking?: number // thinking tokens used
|
|
625
|
+
cost?: number // USD cost reported by provider (e.g. OpenRouter)
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Per-turn data is available on `AgentStats` and `SessionRun`:
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
const stats = await agent.run({ prompt: 'hello' })
|
|
633
|
+
stats.turnUsage // TurnUsage[] per turn
|
|
634
|
+
stats.cost // total cost (sum of per-turn costs, if reported)
|
|
635
|
+
|
|
636
|
+
// In session runs
|
|
637
|
+
session.runs[0].turnUsage // per-turn breakdown
|
|
638
|
+
session.runs[0].totalUsage // aggregated TurnUsage
|
|
639
|
+
session.runs[0].cost // total cost for this run
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## State Management
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
agent.isRunning // boolean: is a run in progress?
|
|
646
|
+
agent.messages // SessionMessage[]: conversation history
|
|
647
|
+
agent.execution // ExecutionContext: where tools run
|
|
648
|
+
agent.handle // ExecutionHandle: spawned context handle
|
|
649
|
+
agent.abort() // cancel the current run
|
|
650
|
+
agent.reset() // clear messages and queues
|
|
651
|
+
await agent.destroy() // clean up execution context and MCP connections
|
|
652
|
+
await agent.waitForIdle() // wait for current run to complete
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
## Project Structure
|
|
656
|
+
|
|
657
|
+
```
|
|
658
|
+
src/
|
|
659
|
+
types.ts shared types
|
|
660
|
+
agent.ts createAgent, AgentHooks, state management
|
|
661
|
+
loop.ts turn execution loop
|
|
662
|
+
start.ts CLI entrypoint
|
|
663
|
+
auth.ts Anthropic OAuth flow
|
|
664
|
+
index.ts package exports
|
|
665
|
+
contexts/
|
|
666
|
+
types.ts ExecutionContext interface, capabilities
|
|
667
|
+
process.ts in-process context (default)
|
|
668
|
+
docker.ts Docker container context
|
|
669
|
+
sandbox.ts remote sandbox context
|
|
670
|
+
index.ts barrel exports
|
|
671
|
+
tools/
|
|
672
|
+
index.ts tool exports
|
|
673
|
+
validation.ts tool argument validation
|
|
674
|
+
shell.ts shell tool
|
|
675
|
+
read-file.ts read_file tool
|
|
676
|
+
write-file.ts write_file tool
|
|
677
|
+
list-files.ts list_files tool
|
|
678
|
+
spawn.ts spawn tool and createSpawnTool factory
|
|
679
|
+
providers/
|
|
680
|
+
index.ts Provider interface
|
|
681
|
+
openai-compat.ts shared OpenAI-compatible utilities
|
|
682
|
+
anthropic.ts Anthropic provider
|
|
683
|
+
openrouter.ts OpenRouter provider
|
|
684
|
+
cerebras.ts Cerebras provider
|
|
685
|
+
harnesses/
|
|
686
|
+
index.ts HarnessConfig, defineHarness, ToolContext
|
|
687
|
+
basic.ts basic harness (shell, read, write, list, spawn)
|
|
688
|
+
mcp/
|
|
689
|
+
index.ts MCP server connection and tool discovery
|
|
690
|
+
session/
|
|
691
|
+
index.ts Session interface, createSession, loadSession
|
|
692
|
+
messages.ts SessionMessage converters (Anthropic/OpenAI)
|
|
693
|
+
memory.ts in-memory session store
|
|
694
|
+
sqlite.ts SQLite-backed session store
|
|
695
|
+
remote.ts HTTP remote session store
|
|
696
|
+
output/
|
|
697
|
+
terminal.ts terminal rendering (md4x)
|
|
698
|
+
test/
|
|
699
|
+
mock-provider.ts mock provider for testing
|
|
700
|
+
mock-context.ts mock execution context for testing
|
|
701
|
+
agent.test.ts agent loop tests
|
|
702
|
+
contexts.test.ts execution context tests
|
|
703
|
+
harness.test.ts harness tests
|
|
704
|
+
mcp.test.ts MCP connection and hook tests
|
|
705
|
+
spawn.test.ts spawn tool and hook tests
|
|
706
|
+
validation.test.ts validation tests
|
|
707
|
+
providers.test.ts provider tests
|
|
708
|
+
openai-compat.test.ts OpenAI-compat utility tests
|
|
709
|
+
session.test.ts session store and agent integration tests
|
|
710
|
+
session-messages.test.ts SessionMessage converter tests
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
## Testing
|
|
714
|
+
|
|
715
|
+
```bash
|
|
716
|
+
bun test
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
300 tests with mock provider and mock execution context, no LLM calls or Docker needed.
|
|
720
|
+
|
|
721
|
+
## License
|
|
722
|
+
|
|
723
|
+
ISC
|
|
724
|
+
|