zidane 1.1.5 → 1.3.1

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 CHANGED
@@ -8,6 +8,8 @@ Minimal TypeScript agent loop built with [Bun](https://bun.sh).
8
8
 
9
9
  Hook into every step of the agent's execution using [hookable](https://github.com/unjs/hookable).
10
10
 
11
+ Built to be embedded in other projects easily, extended through [providers](#providers), [harnesses](#harnesses), and [execution contexts](#execution-contexts).
12
+
11
13
  ## Quickstart
12
14
 
13
15
  ```bash
@@ -30,7 +32,106 @@ bun start \
30
32
  --provider anthropic \ # anthropic | openrouter | cerebras
31
33
  --harness basic \ # tool set to use
32
34
  --system "be concise" \ # system prompt
33
- --thinking off # off | minimal | low | medium | high
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
34
135
  ```
35
136
 
36
137
  ## Providers
@@ -69,8 +170,6 @@ CEREBRAS_API_KEY=csk-... bun start \
69
170
  --prompt "hello"
70
171
  ```
71
172
 
72
- Available models: `zai-glm-4.7`, `gpt-oss-120b`
73
-
74
173
  ## Thinking
75
174
 
76
175
  Extended reasoning for complex tasks. Maps to Anthropic's thinking API or OpenRouter's `:thinking` variant.
@@ -97,9 +196,245 @@ Tools are grouped into **harnesses**. The `basic` harness includes:
97
196
  | `read_file` | Read file contents |
98
197
  | `write_file` | Write/create files |
99
198
  | `list_files` | List directory contents |
199
+ | `spawn` | Spawn a sub-agent for a task |
100
200
 
101
201
  All paths are sandboxed to the working directory.
102
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, turn history, and run metadata across multiple calls or restarts. Each message exchange is a `SessionTurn` with its own UUID, enabling real-time multiplayer streaming.
294
+
295
+ ### SessionTurn
296
+
297
+ Every message in a session is a turn:
298
+
299
+ ```ts
300
+ interface SessionTurn {
301
+ id: string // UUID — generated by store or crypto.randomUUID()
302
+ role: 'user' | 'assistant' | 'system'
303
+ content: SessionContentBlock[] // same format used by providers
304
+ usage?: TurnUsage // token usage (assistant turns only)
305
+ createdAt: number // timestamp
306
+ }
307
+ ```
308
+
309
+ ### Creating a session
310
+
311
+ `createSession` is async — stores can generate IDs server-side (e.g. Supabase).
312
+
313
+ ```ts
314
+ import { createSession, createMemoryStore } from 'zidane/session'
315
+
316
+ // In-memory (default, no persistence)
317
+ const session = await createSession({ id: 'my-session', agentId: 'my-agent' })
318
+
319
+ // With a store for persistence
320
+ const store = createMemoryStore()
321
+ const session = await createSession({ id: 'my-session', store })
322
+ ```
323
+
324
+ ### Storage backends
325
+
326
+ Three built-in stores are available. All implement the full `SessionStore` interface including incremental operations.
327
+
328
+ ```ts
329
+ import { createMemoryStore, createSqliteStore, createRemoteStore } from 'zidane/session'
330
+
331
+ // In-memory, fast, no disk I/O, lost on process restart
332
+ const memStore = createMemoryStore()
333
+
334
+ // SQLite, persistent, zero-dependency (uses Bun's built-in SQLite)
335
+ const sqliteStore = createSqliteStore({ path: './sessions.db' })
336
+
337
+ // Remote HTTP, delegates to a custom REST API
338
+ const remoteStore = createRemoteStore({ url: 'https://api.example.com/sessions' })
339
+ ```
340
+
341
+ ### SessionStore interface
342
+
343
+ ```ts
344
+ interface SessionStore {
345
+ // Optional: server-side ID generation
346
+ generateSessionId?: () => string | Promise<string>
347
+ generateTurnId?: () => string | Promise<string>
348
+
349
+ // Core CRUD
350
+ load: (sessionId: string) => Promise<SessionData | null>
351
+ save: (session: SessionData) => Promise<void>
352
+ delete: (sessionId: string) => Promise<void>
353
+ list: (filter?) => Promise<string[]>
354
+
355
+ // Incremental operations (avoids full re-save)
356
+ appendTurns: (sessionId: string, turns: SessionTurn[]) => Promise<void>
357
+ getTurns: (sessionId: string, from?: number, limit?: number) => Promise<SessionTurn[]>
358
+ updateRun: (sessionId: string, run: SessionRun) => Promise<void>
359
+ updateStatus: (sessionId: string, status: SessionStatus) => Promise<void>
360
+ }
361
+ ```
362
+
363
+ Custom ID generation lets external databases (e.g. Supabase) provide UUIDs server-side, keeping IDs in sync:
364
+
365
+ ```ts
366
+ const store = createRemoteStore({ url: '...' })
367
+ store.generateTurnId = async () => {
368
+ const { data } = await supabase.rpc('gen_random_uuid')
369
+ return data
370
+ }
371
+ ```
372
+
373
+ ### Agent integration
374
+
375
+ ```ts
376
+ const agent = createAgent({
377
+ harness,
378
+ provider,
379
+ session,
380
+ })
381
+
382
+ await agent.run({ prompt: 'hello' })
383
+ await session.save() // persist to store
384
+ ```
385
+
386
+ Turns are persisted incrementally after each agent turn via `appendTurns` — not as a full document save. If the agent crashes mid-run, you still have turns up to the last completed turn.
387
+
388
+ ### Session status
389
+
390
+ Sessions track their status: `'idle' | 'running' | 'completed' | 'error'`. The agent updates it automatically during runs.
391
+
392
+ ```ts
393
+ session.status // 'idle'
394
+ await agent.run({ prompt: 'go' })
395
+ // idle → running → completed (or error)
396
+ ```
397
+
398
+ ### Session hooks
399
+
400
+ ```ts
401
+ agent.hooks.hook('session:start', (ctx) => {
402
+ // ctx.sessionId, ctx.runId, ctx.prompt
403
+ })
404
+
405
+ agent.hooks.hook('session:end', (ctx) => {
406
+ // ctx.sessionId, ctx.runId
407
+ // ctx.status: 'completed' | 'aborted' | 'error'
408
+ })
409
+
410
+ agent.hooks.hook('session:turns', (ctx) => {
411
+ // ctx.sessionId, ctx.count
412
+ // fired after each turn (incremental sync)
413
+ })
414
+
415
+ agent.hooks.hook('session:save', (ctx) => {
416
+ // ctx.sessionId
417
+ // fired after session.save() completes
418
+ })
419
+
420
+ agent.hooks.hook('session:meta', (ctx) => {
421
+ // ctx.sessionId, ctx.key, ctx.value
422
+ // fired when session.setMeta() is called
423
+ })
424
+ ```
425
+
426
+ ### Restoring a session
427
+
428
+ ```ts
429
+ import { loadSession } from 'zidane/session'
430
+
431
+ const session = await loadSession(store, 'my-session')
432
+ if (session) {
433
+ const agent = createAgent({ harness, provider, session })
434
+ await agent.run({ prompt: 'continue from before' })
435
+ }
436
+ ```
437
+
103
438
  ## Hooks
104
439
 
105
440
  The agent uses [hookable](https://github.com/unjs/hookable) for lifecycle events. Every hook receives a mutable context object.
@@ -108,20 +443,21 @@ The agent uses [hookable](https://github.com/unjs/hookable) for lifecycle events
108
443
 
109
444
  ```ts
110
445
  agent.hooks.hook('system:before', (ctx) => {
111
- // ctx.system system prompt text
446
+ // ctx.system: system prompt text
112
447
  })
113
448
 
114
449
  agent.hooks.hook('turn:before', (ctx) => {
115
- // ctx.turn turn number
116
- // ctx.options StreamOptions being sent to provider
450
+ // ctx.turn: turn number
451
+ // ctx.turnId: UUID for this turn (generated before LLM call)
452
+ // ctx.options: StreamOptions being sent to provider
117
453
  })
118
454
 
119
455
  agent.hooks.hook('turn:after', (ctx) => {
120
- // ctx.turn, ctx.usage { input, output }
456
+ // ctx.turn, ctx.turnId, ctx.usage { input, output }
121
457
  })
122
458
 
123
459
  agent.hooks.hook('agent:done', (ctx) => {
124
- // ctx.totalIn, ctx.totalOut, ctx.turns, ctx.elapsed
460
+ // ctx.totalIn, ctx.totalOut, ctx.turns, ctx.elapsed, ctx.children?
125
461
  })
126
462
 
127
463
  agent.hooks.hook('agent:abort', () => {
@@ -133,12 +469,15 @@ agent.hooks.hook('agent:abort', () => {
133
469
 
134
470
  ```ts
135
471
  agent.hooks.hook('stream:text', (ctx) => {
136
- // ctx.delta new text chunk
137
- // ctx.text accumulated text so far
472
+ // ctx.delta: new text chunk
473
+ // ctx.text: accumulated text so far
474
+ // ctx.turnId: UUID of the turn being streamed
475
+ // ctx.blockIndex: content block index within the turn
138
476
  })
139
477
 
140
478
  agent.hooks.hook('stream:end', (ctx) => {
141
- // ctx.text final complete text
479
+ // ctx.text: final complete text
480
+ // ctx.turnId, ctx.blockIndex
142
481
  })
143
482
  ```
144
483
 
@@ -158,7 +497,7 @@ agent.hooks.hook('tool:error', (ctx) => {
158
497
  })
159
498
  ```
160
499
 
161
- ### Tool Gate block execution
500
+ ### Tool Gate: block execution
162
501
 
163
502
  Mutate `ctx.block = true` to prevent a tool from running.
164
503
 
@@ -171,7 +510,7 @@ agent.hooks.hook('tool:gate', (ctx) => {
171
510
  })
172
511
  ```
173
512
 
174
- ### Tool Transform modify output
513
+ ### Tool Transform: modify output
175
514
 
176
515
  Mutate `ctx.result` or `ctx.isError` to transform tool results before they're sent back to the model.
177
516
 
@@ -182,7 +521,7 @@ agent.hooks.hook('tool:transform', (ctx) => {
182
521
  })
183
522
  ```
184
523
 
185
- ### Context Transform prune messages
524
+ ### Context Transform: prune messages
186
525
 
187
526
  Mutate `ctx.messages` before each LLM call for context window management.
188
527
 
@@ -193,9 +532,73 @@ agent.hooks.hook('context:transform', (ctx) => {
193
532
  })
194
533
  ```
195
534
 
196
- ## Steering & Follow-up
535
+ ### Spawn hooks
197
536
 
198
- ### Steering interrupt mid-run
537
+ Fired by the `spawn` tool when child agents are created.
538
+
539
+ ```ts
540
+ agent.hooks.hook('spawn:before', (ctx) => {
541
+ // ctx.id: child agent id (e.g. 'child-1')
542
+ // ctx.task: the task prompt given to the child
543
+ })
544
+
545
+ agent.hooks.hook('spawn:complete', (ctx) => {
546
+ // ctx.id, ctx.task
547
+ // ctx.stats: AgentStats from the child run
548
+ })
549
+
550
+ agent.hooks.hook('spawn:error', (ctx) => {
551
+ // ctx.id, ctx.task, ctx.error
552
+ })
553
+ ```
554
+
555
+ ### MCP hooks
556
+
557
+ Fired during MCP server lifecycle.
558
+
559
+ ```ts
560
+ agent.hooks.hook('mcp:connect', (ctx) => {
561
+ // ctx.name: server name
562
+ // ctx.transport: 'stdio' | 'sse' | 'streamable-http'
563
+ // ctx.tools: namespaced tool names discovered on this server
564
+ })
565
+
566
+ agent.hooks.hook('mcp:error', (ctx) => {
567
+ // ctx.name: server name
568
+ // ctx.error: connection error
569
+ })
570
+
571
+ agent.hooks.hook('mcp:close', (ctx) => {
572
+ // ctx.name: server name being closed
573
+ })
574
+
575
+ agent.hooks.hook('mcp:tool:before', (ctx) => {
576
+ // ctx.server: MCP server name
577
+ // ctx.tool: original tool name (not namespaced)
578
+ // ctx.input: tool arguments
579
+ })
580
+
581
+ agent.hooks.hook('mcp:tool:after', (ctx) => {
582
+ // ctx.server, ctx.tool, ctx.input
583
+ // ctx.result: tool output string
584
+ })
585
+
586
+ agent.hooks.hook('mcp:tool:error', (ctx) => {
587
+ // ctx.server, ctx.tool, ctx.input, ctx.error
588
+ })
589
+ ```
590
+
591
+ ### Steering inject
592
+
593
+ ```ts
594
+ agent.hooks.hook('steer:inject', (ctx) => {
595
+ // ctx.message: the steering message being injected
596
+ })
597
+ ```
598
+
599
+ ## Steering and Follow-up
600
+
601
+ ### Steering: interrupt mid-run
199
602
 
200
603
  Inject a message while the agent is working. Delivered between tool calls, skipping remaining tools in the current turn.
201
604
 
@@ -205,7 +608,7 @@ agent.hooks.hook('tool:after', () => {
205
608
  })
206
609
  ```
207
610
 
208
- ### Follow-up continue after done
611
+ ### Follow-up, continue after done
209
612
 
210
613
  Queue messages that extend the conversation after the agent finishes.
211
614
 
@@ -220,7 +623,7 @@ Execute multiple tool calls from a single turn concurrently.
220
623
 
221
624
  ```ts
222
625
  const agent = createAgent({
223
- harness: 'basic',
626
+ harness,
224
627
  provider,
225
628
  toolExecution: 'parallel', // default: 'sequential'
226
629
  })
@@ -246,13 +649,68 @@ await agent.run({
246
649
  })
247
650
  ```
248
651
 
652
+ ## Message Format
653
+
654
+ All messages in zidane use the canonical `SessionMessage` format, with or without sessions:
655
+
656
+ ```ts
657
+ type SessionContentBlock =
658
+ | { type: 'text', text: string }
659
+ | { type: 'image', mediaType: string, data: string }
660
+ | { type: 'tool_call', id: string, name: string, input: Record<string, unknown> }
661
+ | { type: 'tool_result', callId: string, output: string, isError?: boolean }
662
+ | { type: 'thinking', text: string }
663
+
664
+ interface SessionMessage {
665
+ role: 'user' | 'assistant'
666
+ content: SessionContentBlock[]
667
+ }
668
+ ```
669
+
670
+ Providers convert to and from native wire formats internally. Converters are available for external interop:
671
+
672
+ ```ts
673
+ import { fromAnthropic, toAnthropic, fromOpenAI, toOpenAI, autoDetectAndConvert } from 'zidane'
674
+ ```
675
+
676
+ ## Usage Tracking
677
+
678
+ Every turn reports token usage. Provider-specific fields are optional:
679
+
680
+ ```ts
681
+ interface TurnUsage {
682
+ input: number
683
+ output: number
684
+ cacheCreation?: number // Anthropic: tokens written to cache
685
+ cacheRead?: number // Anthropic: tokens read from cache
686
+ thinking?: number // thinking tokens used
687
+ cost?: number // USD cost reported by provider (e.g. OpenRouter)
688
+ }
689
+ ```
690
+
691
+ Per-turn data is available on `AgentStats` and `SessionRun`:
692
+
693
+ ```ts
694
+ const stats = await agent.run({ prompt: 'hello' })
695
+ stats.turnUsage // TurnUsage[] per turn
696
+ stats.cost // total cost (sum of per-turn costs, if reported)
697
+
698
+ // In session runs
699
+ session.runs[0].turnUsage // per-turn breakdown
700
+ session.runs[0].totalUsage // aggregated TurnUsage
701
+ session.runs[0].cost // total cost for this run
702
+ ```
703
+
249
704
  ## State Management
250
705
 
251
706
  ```ts
252
- agent.isRunning // boolean is a run in progress?
253
- agent.messages // Message[] conversation history
254
- agent.abort() // cancel the current run
255
- agent.reset() // clear messages and queues
707
+ agent.isRunning // boolean: is a run in progress?
708
+ agent.messages // SessionMessage[]: conversation history
709
+ agent.execution // ExecutionContext: where tools run
710
+ agent.handle // ExecutionHandle: spawned context handle
711
+ agent.abort() // cancel the current run
712
+ agent.reset() // clear messages and queues
713
+ await agent.destroy() // clean up execution context and MCP connections
256
714
  await agent.waitForIdle() // wait for current run to complete
257
715
  ```
258
716
 
@@ -261,12 +719,25 @@ await agent.waitForIdle() // wait for current run to complete
261
719
  ```
262
720
  src/
263
721
  types.ts shared types
264
- agent.ts createAgent, state management
722
+ agent.ts createAgent, AgentHooks, state management
265
723
  loop.ts turn execution loop
266
724
  start.ts CLI entrypoint
267
725
  auth.ts Anthropic OAuth flow
726
+ index.ts package exports
727
+ contexts/
728
+ types.ts ExecutionContext interface, capabilities
729
+ process.ts in-process context (default)
730
+ docker.ts Docker container context
731
+ sandbox.ts remote sandbox context
732
+ index.ts barrel exports
268
733
  tools/
734
+ index.ts tool exports
269
735
  validation.ts tool argument validation
736
+ shell.ts shell tool
737
+ read-file.ts read_file tool
738
+ write-file.ts write_file tool
739
+ list-files.ts list_files tool
740
+ spawn.ts spawn tool and createSpawnTool factory
270
741
  providers/
271
742
  index.ts Provider interface
272
743
  openai-compat.ts shared OpenAI-compatible utilities
@@ -274,14 +745,31 @@ src/
274
745
  openrouter.ts OpenRouter provider
275
746
  cerebras.ts Cerebras provider
276
747
  harnesses/
277
- index.ts harness registry
278
- basic.ts shell, read, write, list tools
748
+ index.ts HarnessConfig, defineHarness, ToolContext
749
+ basic.ts basic harness (shell, read, write, list, spawn)
750
+ mcp/
751
+ index.ts MCP server connection and tool discovery
752
+ session/
753
+ index.ts Session interface, createSession, loadSession
754
+ messages.ts SessionMessage converters (Anthropic/OpenAI)
755
+ memory.ts in-memory session store
756
+ sqlite.ts SQLite-backed session store
757
+ remote.ts HTTP remote session store
279
758
  output/
280
759
  terminal.ts terminal rendering (md4x)
281
760
  test/
282
761
  mock-provider.ts mock provider for testing
283
- agent.test.ts agent test suite (30 tests)
762
+ mock-context.ts mock execution context for testing
763
+ agent.test.ts agent loop tests
764
+ contexts.test.ts execution context tests
765
+ harness.test.ts harness tests
766
+ mcp.test.ts MCP connection and hook tests
767
+ spawn.test.ts spawn tool and hook tests
284
768
  validation.test.ts validation tests
769
+ providers.test.ts provider tests
770
+ openai-compat.test.ts OpenAI-compat utility tests
771
+ session.test.ts session store and agent integration tests
772
+ session-messages.test.ts SessionMessage converter tests
285
773
  ```
286
774
 
287
775
  ## Testing
@@ -290,8 +778,9 @@ test/
290
778
  bun test
291
779
  ```
292
780
 
293
- 30 tests with a mock provider no LLM calls needed.
781
+ 300 tests with mock provider and mock execution context, no LLM calls or Docker needed.
294
782
 
295
783
  ## License
296
784
 
297
785
  ISC
786
+