zidane 1.0.2 → 1.1.5

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
@@ -1 +1,297 @@
1
1
  ![Zidane](https://github.com/Tahul/zidane/blob/main/zidane.jpeg?raw=true)
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
+ ## Quickstart
12
+
13
+ ```bash
14
+ # Install
15
+ bun install
16
+
17
+ # Authenticate with Anthropic OAuth (Claude Pro/Max)
18
+ bun run auth
19
+
20
+ # Run
21
+ bun start --prompt "create a hello world express app"
22
+ ```
23
+
24
+ ## CLI
25
+
26
+ ```bash
27
+ bun start \
28
+ --prompt "your task" \ # required
29
+ --model claude-opus-4-6 \ # model id (default: claude-opus-4-6)
30
+ --provider anthropic \ # anthropic | openrouter | cerebras
31
+ --harness basic \ # tool set to use
32
+ --system "be concise" \ # system prompt
33
+ --thinking off # off | minimal | low | medium | high
34
+ ```
35
+
36
+ ## Providers
37
+
38
+ ### Anthropic
39
+
40
+ Direct Anthropic API with OAuth and API key support.
41
+
42
+ ```bash
43
+ # OAuth (Claude Pro/Max subscription)
44
+ bun run auth
45
+
46
+ # Or API key
47
+ ANTHROPIC_API_KEY=sk-ant-... bun start --prompt "hello"
48
+ ```
49
+
50
+ ### OpenRouter
51
+
52
+ Access 200+ models through OpenRouter's unified API.
53
+
54
+ ```bash
55
+ OPENROUTER_API_KEY=sk-or-... bun start \
56
+ --provider openrouter \
57
+ --model anthropic/claude-sonnet-4-6 \
58
+ --prompt "hello"
59
+ ```
60
+
61
+ ### Cerebras
62
+
63
+ Ultra-fast inference on Cerebras wafer-scale hardware.
64
+
65
+ ```bash
66
+ CEREBRAS_API_KEY=csk-... bun start \
67
+ --provider cerebras \
68
+ --model zai-glm-4.7 \
69
+ --prompt "hello"
70
+ ```
71
+
72
+ Available models: `zai-glm-4.7`, `gpt-oss-120b`
73
+
74
+ ## Thinking
75
+
76
+ Extended reasoning for complex tasks. Maps to Anthropic's thinking API or OpenRouter's `:thinking` variant.
77
+
78
+ ```bash
79
+ bun start --prompt "solve this proof" --thinking high
80
+ ```
81
+
82
+ | Level | Budget |
83
+ |---|---|
84
+ | `off` | disabled |
85
+ | `minimal` | 1k tokens |
86
+ | `low` | 4k tokens |
87
+ | `medium` | 10k tokens |
88
+ | `high` | 32k tokens |
89
+
90
+ ## Tools (Harnesses)
91
+
92
+ Tools are grouped into **harnesses**. The `basic` harness includes:
93
+
94
+ | Tool | Description |
95
+ |---|---|
96
+ | `shell` | Execute shell commands |
97
+ | `read_file` | Read file contents |
98
+ | `write_file` | Write/create files |
99
+ | `list_files` | List directory contents |
100
+
101
+ All paths are sandboxed to the working directory.
102
+
103
+ ## Hooks
104
+
105
+ The agent uses [hookable](https://github.com/unjs/hookable) for lifecycle events. Every hook receives a mutable context object.
106
+
107
+ ### Lifecycle
108
+
109
+ ```ts
110
+ agent.hooks.hook('system:before', (ctx) => {
111
+ // ctx.system — system prompt text
112
+ })
113
+
114
+ agent.hooks.hook('turn:before', (ctx) => {
115
+ // ctx.turn — turn number
116
+ // ctx.options — StreamOptions being sent to provider
117
+ })
118
+
119
+ agent.hooks.hook('turn:after', (ctx) => {
120
+ // ctx.turn, ctx.usage { input, output }
121
+ })
122
+
123
+ agent.hooks.hook('agent:done', (ctx) => {
124
+ // ctx.totalIn, ctx.totalOut, ctx.turns, ctx.elapsed
125
+ })
126
+
127
+ agent.hooks.hook('agent:abort', () => {
128
+ // fired when agent.abort() is called
129
+ })
130
+ ```
131
+
132
+ ### Streaming
133
+
134
+ ```ts
135
+ agent.hooks.hook('stream:text', (ctx) => {
136
+ // ctx.delta — new text chunk
137
+ // ctx.text — accumulated text so far
138
+ })
139
+
140
+ agent.hooks.hook('stream:end', (ctx) => {
141
+ // ctx.text — final complete text
142
+ })
143
+ ```
144
+
145
+ ### Tool Execution
146
+
147
+ ```ts
148
+ agent.hooks.hook('tool:before', (ctx) => {
149
+ // ctx.name, ctx.input
150
+ })
151
+
152
+ agent.hooks.hook('tool:after', (ctx) => {
153
+ // ctx.name, ctx.input, ctx.result
154
+ })
155
+
156
+ agent.hooks.hook('tool:error', (ctx) => {
157
+ // ctx.name, ctx.input, ctx.error
158
+ })
159
+ ```
160
+
161
+ ### Tool Gate — block execution
162
+
163
+ Mutate `ctx.block = true` to prevent a tool from running.
164
+
165
+ ```ts
166
+ agent.hooks.hook('tool:gate', (ctx) => {
167
+ if (ctx.name === 'shell' && String(ctx.input.command).includes('rm -rf')) {
168
+ ctx.block = true
169
+ ctx.reason = 'dangerous command'
170
+ }
171
+ })
172
+ ```
173
+
174
+ ### Tool Transform — modify output
175
+
176
+ Mutate `ctx.result` or `ctx.isError` to transform tool results before they're sent back to the model.
177
+
178
+ ```ts
179
+ agent.hooks.hook('tool:transform', (ctx) => {
180
+ if (ctx.result.length > 5000)
181
+ ctx.result = ctx.result.slice(0, 5000) + '\n... (truncated)'
182
+ })
183
+ ```
184
+
185
+ ### Context Transform — prune messages
186
+
187
+ Mutate `ctx.messages` before each LLM call for context window management.
188
+
189
+ ```ts
190
+ agent.hooks.hook('context:transform', (ctx) => {
191
+ if (ctx.messages.length > 30)
192
+ ctx.messages.splice(2, ctx.messages.length - 30)
193
+ })
194
+ ```
195
+
196
+ ## Steering & Follow-up
197
+
198
+ ### Steering — interrupt mid-run
199
+
200
+ Inject a message while the agent is working. Delivered between tool calls, skipping remaining tools in the current turn.
201
+
202
+ ```ts
203
+ agent.hooks.hook('tool:after', () => {
204
+ agent.steer('focus only on the tests directory')
205
+ })
206
+ ```
207
+
208
+ ### Follow-up — continue after done
209
+
210
+ Queue messages that extend the conversation after the agent finishes.
211
+
212
+ ```ts
213
+ agent.followUp('now write tests for what you built')
214
+ agent.followUp('then update the README')
215
+ ```
216
+
217
+ ## Parallel Tool Execution
218
+
219
+ Execute multiple tool calls from a single turn concurrently.
220
+
221
+ ```ts
222
+ const agent = createAgent({
223
+ harness: 'basic',
224
+ provider,
225
+ toolExecution: 'parallel', // default: 'sequential'
226
+ })
227
+ ```
228
+
229
+ ## Image Content
230
+
231
+ Pass images alongside the prompt.
232
+
233
+ ```ts
234
+ import { readFileSync } from 'fs'
235
+
236
+ await agent.run({
237
+ prompt: 'describe this screenshot',
238
+ images: [{
239
+ type: 'image',
240
+ source: {
241
+ type: 'base64',
242
+ media_type: 'image/png',
243
+ data: readFileSync('screenshot.png').toString('base64'),
244
+ },
245
+ }],
246
+ })
247
+ ```
248
+
249
+ ## State Management
250
+
251
+ ```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
256
+ await agent.waitForIdle() // wait for current run to complete
257
+ ```
258
+
259
+ ## Project Structure
260
+
261
+ ```
262
+ src/
263
+ types.ts shared types
264
+ agent.ts createAgent, state management
265
+ loop.ts turn execution loop
266
+ start.ts CLI entrypoint
267
+ auth.ts Anthropic OAuth flow
268
+ tools/
269
+ validation.ts tool argument validation
270
+ providers/
271
+ index.ts Provider interface
272
+ openai-compat.ts shared OpenAI-compatible utilities
273
+ anthropic.ts Anthropic provider
274
+ openrouter.ts OpenRouter provider
275
+ cerebras.ts Cerebras provider
276
+ harnesses/
277
+ index.ts harness registry
278
+ basic.ts shell, read, write, list tools
279
+ output/
280
+ terminal.ts terminal rendering (md4x)
281
+ test/
282
+ mock-provider.ts mock provider for testing
283
+ agent.test.ts agent test suite (30 tests)
284
+ validation.test.ts validation tests
285
+ ```
286
+
287
+ ## Testing
288
+
289
+ ```bash
290
+ bun test
291
+ ```
292
+
293
+ 30 tests with a mock provider — no LLM calls needed.
294
+
295
+ ## License
296
+
297
+ ISC
@@ -0,0 +1,125 @@
1
+ // src/harnesses/basic.ts
2
+ import { execSync } from "child_process";
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
4
+ import { dirname, resolve } from "path";
5
+ var cwd = process.cwd();
6
+ function safePath(p) {
7
+ const resolved = resolve(cwd, p);
8
+ if (!resolved.startsWith(cwd))
9
+ throw new Error(`Path escapes working directory: ${p}`);
10
+ return resolved;
11
+ }
12
+ var shell = {
13
+ spec: {
14
+ name: "shell",
15
+ description: "Execute a shell command and return stdout+stderr. Runs in the project root.",
16
+ input_schema: {
17
+ type: "object",
18
+ properties: {
19
+ command: { type: "string", description: "The shell command to run" }
20
+ },
21
+ required: ["command"]
22
+ }
23
+ },
24
+ async execute({ command }) {
25
+ try {
26
+ const out = execSync(command, {
27
+ cwd,
28
+ encoding: "utf-8",
29
+ timeout: 3e4,
30
+ maxBuffer: 1024 * 1024,
31
+ stdio: ["pipe", "pipe", "pipe"]
32
+ });
33
+ return out || "(no output)";
34
+ } catch (err) {
35
+ const stderr = err.stderr?.toString() ?? "";
36
+ const stdout = err.stdout?.toString() ?? "";
37
+ return `Exit code ${err.status ?? 1}
38
+ ${stdout}
39
+ ${stderr}`.trim();
40
+ }
41
+ }
42
+ };
43
+ var readFile = {
44
+ spec: {
45
+ name: "read_file",
46
+ description: "Read the contents of a file at the given path (relative to project root).",
47
+ input_schema: {
48
+ type: "object",
49
+ properties: {
50
+ path: { type: "string", description: "Relative file path" }
51
+ },
52
+ required: ["path"]
53
+ }
54
+ },
55
+ async execute({ path }) {
56
+ const target = safePath(path);
57
+ if (!existsSync(target))
58
+ return `File not found: ${path}`;
59
+ return readFileSync(target, "utf-8");
60
+ }
61
+ };
62
+ var writeFile = {
63
+ spec: {
64
+ name: "write_file",
65
+ description: "Write content to a file. Creates parent directories if needed.",
66
+ input_schema: {
67
+ type: "object",
68
+ properties: {
69
+ path: { type: "string", description: "Relative file path" },
70
+ content: { type: "string", description: "File content to write" }
71
+ },
72
+ required: ["path", "content"]
73
+ }
74
+ },
75
+ async execute({ path, content }) {
76
+ const target = safePath(path);
77
+ mkdirSync(dirname(target), { recursive: true });
78
+ writeFileSync(target, content);
79
+ return `Wrote ${content.length} bytes to ${path}`;
80
+ }
81
+ };
82
+ var listFiles = {
83
+ spec: {
84
+ name: "list_files",
85
+ description: "List files and directories at the given path (relative to project root).",
86
+ input_schema: {
87
+ type: "object",
88
+ properties: {
89
+ path: { type: "string", description: 'Relative directory path (default: ".")' }
90
+ },
91
+ required: []
92
+ }
93
+ },
94
+ async execute({ path }) {
95
+ const target = safePath(path || ".");
96
+ if (!existsSync(target))
97
+ return `Directory not found: ${path}`;
98
+ const entries = readdirSync(target);
99
+ return entries.map((name) => {
100
+ const full = resolve(target, name);
101
+ const isDir = statSync(full).isDirectory();
102
+ return `${isDir ? "\u{1F4C1}" : "\u{1F4C4}"} ${name}`;
103
+ }).join("\n");
104
+ }
105
+ };
106
+ var basic_default = defineHarness({
107
+ name: "basic",
108
+ system: "You are a helpful assistant with access to shell, file reading, file writing, and directory listing tools. Use them to accomplish tasks in the project directory.",
109
+ tools: {
110
+ shell,
111
+ readFile,
112
+ writeFile,
113
+ listFiles
114
+ }
115
+ });
116
+
117
+ // src/harnesses/index.ts
118
+ function defineHarness(config) {
119
+ return config;
120
+ }
121
+
122
+ export {
123
+ basic_default,
124
+ defineHarness
125
+ };
@@ -0,0 +1,24 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+
3
+ declare const _default: HarnessConfig;
4
+
5
+ interface ToolDef {
6
+ spec: Anthropic.Tool;
7
+ execute: (input: Record<string, unknown>) => Promise<string>;
8
+ }
9
+ type ToolMap = Map<string, ToolDef>;
10
+ interface HarnessConfig {
11
+ /** Display name for this harness */
12
+ name: string;
13
+ /** Default system prompt injected when no system is provided at run time */
14
+ system?: string;
15
+ /** Tool definitions available to the agent */
16
+ tools: Record<string, ToolDef>;
17
+ }
18
+ /**
19
+ * Define a harness with a name, optional system prompt, and tools.
20
+ */
21
+ declare function defineHarness(config: HarnessConfig): HarnessConfig;
22
+ type Harness = HarnessConfig;
23
+
24
+ export { type Harness, type HarnessConfig, type ToolDef, type ToolMap, _default as basic, defineHarness };
@@ -0,0 +1,8 @@
1
+ import {
2
+ basic_default,
3
+ defineHarness
4
+ } from "./chunk-ECE5USCO.js";
5
+ export {
6
+ basic_default as basic,
7
+ defineHarness
8
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared types for the agent system.
3
+ */
4
+ type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high';
5
+ type ToolExecutionMode = 'sequential' | 'parallel';
6
+ interface ImageContent {
7
+ type: 'image';
8
+ source: {
9
+ type: 'base64';
10
+ media_type: string;
11
+ data: string;
12
+ };
13
+ }
14
+ type ContentBlock = {
15
+ type: 'text';
16
+ text: string;
17
+ } | ImageContent;
18
+ interface AgentRunOptions {
19
+ model?: string;
20
+ prompt: string;
21
+ system?: string;
22
+ thinking?: ThinkingLevel;
23
+ images?: ImageContent[];
24
+ }
25
+ interface AgentStats {
26
+ totalIn: number;
27
+ totalOut: number;
28
+ turns: number;
29
+ elapsed: number;
30
+ }
31
+
32
+ declare function anthropic(): Provider;
33
+
34
+ declare function cerebras(defaultModel?: string): Provider;
35
+
36
+ declare function openrouter(defaultModel?: string): Provider;
37
+
38
+ interface ToolSpec {
39
+ name: string;
40
+ description: string;
41
+ input_schema: Record<string, unknown>;
42
+ }
43
+ interface ToolCall {
44
+ id: string;
45
+ name: string;
46
+ input: Record<string, unknown>;
47
+ }
48
+ interface ToolResult {
49
+ id: string;
50
+ content: string;
51
+ }
52
+ interface Message {
53
+ role: 'user' | 'assistant';
54
+ content: unknown;
55
+ }
56
+ interface StreamCallbacks {
57
+ onText: (delta: string) => void;
58
+ }
59
+ interface TurnResult {
60
+ /** Full response to push into message history as assistant turn */
61
+ assistantMessage: unknown;
62
+ /** Text content blocks concatenated */
63
+ text: string;
64
+ /** Tool calls requested by the model */
65
+ toolCalls: ToolCall[];
66
+ /** Whether the model wants to stop */
67
+ done: boolean;
68
+ usage: {
69
+ input: number;
70
+ output: number;
71
+ };
72
+ }
73
+ interface StreamOptions {
74
+ model: string;
75
+ system: string;
76
+ tools: unknown[];
77
+ messages: Message[];
78
+ maxTokens: number;
79
+ /** Thinking/reasoning level (optional, default: off) */
80
+ thinking?: ThinkingLevel;
81
+ /** Abort signal for cancellation */
82
+ signal?: AbortSignal;
83
+ }
84
+ interface Provider {
85
+ readonly name: string;
86
+ readonly meta: {
87
+ defaultModel: string;
88
+ } & Record<string, unknown>;
89
+ /** Format tool specs for this provider */
90
+ formatTools: (tools: ToolSpec[]) => unknown[];
91
+ /** Create a user message (text or with images) */
92
+ userMessage: (content: string, images?: ImageContent[]) => Message;
93
+ /** Create an assistant message (for priming) */
94
+ assistantMessage: (content: string) => Message;
95
+ /** Create a tool results message to send back */
96
+ toolResultsMessage: (results: ToolResult[]) => Message;
97
+ /** Stream a turn, calling onText for each text delta */
98
+ stream: (options: StreamOptions, callbacks: StreamCallbacks) => Promise<TurnResult>;
99
+ }
100
+
101
+ export { type AgentStats as A, type ContentBlock as C, type ImageContent as I, type Message as M, type Provider as P, type StreamOptions as S, type ToolExecutionMode as T, type AgentRunOptions as a, type ThinkingLevel as b, type StreamCallbacks as c, type ToolCall as d, type ToolResult as e, type ToolSpec as f, type TurnResult as g, anthropic as h, cerebras as i, openrouter as o };
@@ -0,0 +1,89 @@
1
+ import { Hookable } from 'hookable';
2
+ import { HarnessConfig } from './harnesses.js';
3
+ export { Harness, ToolDef, ToolMap, defineHarness } from './harnesses.js';
4
+ import { S as StreamOptions, M as Message, A as AgentStats, a as AgentRunOptions, P as Provider, T as ToolExecutionMode } from './index-ByJfS-kX.js';
5
+ export { C as ContentBlock, I as ImageContent, b as ThinkingLevel } from './index-ByJfS-kX.js';
6
+ import '@anthropic-ai/sdk';
7
+
8
+ /**
9
+ * Agent creation and state management.
10
+ */
11
+
12
+ interface AgentHooks {
13
+ 'system:before': (ctx: {
14
+ system: string;
15
+ }) => void;
16
+ 'turn:before': (ctx: {
17
+ turn: number;
18
+ options: StreamOptions;
19
+ }) => void;
20
+ 'turn:after': (ctx: {
21
+ turn: number;
22
+ usage: {
23
+ input: number;
24
+ output: number;
25
+ };
26
+ }) => void;
27
+ 'stream:text': (ctx: {
28
+ delta: string;
29
+ text: string;
30
+ }) => void;
31
+ 'stream:end': (ctx: {
32
+ text: string;
33
+ }) => void;
34
+ 'tool:before': (ctx: {
35
+ name: string;
36
+ input: Record<string, unknown>;
37
+ }) => void;
38
+ 'tool:after': (ctx: {
39
+ name: string;
40
+ input: Record<string, unknown>;
41
+ result: string;
42
+ }) => void;
43
+ 'tool:error': (ctx: {
44
+ name: string;
45
+ input: Record<string, unknown>;
46
+ error: Error;
47
+ }) => void;
48
+ 'tool:gate': (ctx: {
49
+ name: string;
50
+ input: Record<string, unknown>;
51
+ block: boolean;
52
+ reason: string;
53
+ }) => void;
54
+ 'tool:transform': (ctx: {
55
+ name: string;
56
+ input: Record<string, unknown>;
57
+ result: string;
58
+ isError: boolean;
59
+ }) => void;
60
+ 'context:transform': (ctx: {
61
+ messages: Message[];
62
+ }) => void;
63
+ 'steer:inject': (ctx: {
64
+ message: string;
65
+ }) => void;
66
+ 'agent:abort': (ctx: object) => void;
67
+ 'agent:done': (ctx: AgentStats) => void;
68
+ }
69
+ interface AgentOptions {
70
+ harness: HarnessConfig;
71
+ provider: Provider;
72
+ /** Tool execution mode: 'sequential' (default) or 'parallel' */
73
+ toolExecution?: ToolExecutionMode;
74
+ }
75
+ interface Agent {
76
+ hooks: Hookable<AgentHooks>;
77
+ run: (options: AgentRunOptions) => Promise<AgentStats>;
78
+ abort: () => void;
79
+ steer: (message: string) => void;
80
+ followUp: (message: string) => void;
81
+ waitForIdle: () => Promise<void>;
82
+ reset: () => void;
83
+ readonly isRunning: boolean;
84
+ readonly messages: Message[];
85
+ meta: Record<string, unknown>;
86
+ }
87
+ declare function createAgent({ harness, provider, toolExecution }: AgentOptions): Agent;
88
+
89
+ export { type Agent, type AgentHooks, type AgentOptions, AgentRunOptions, AgentStats, HarnessConfig, ToolExecutionMode, createAgent };
package/dist/index.js ADDED
@@ -0,0 +1,261 @@
1
+ import {
2
+ defineHarness
3
+ } from "./chunk-ECE5USCO.js";
4
+
5
+ // src/agent.ts
6
+ import { createHooks } from "hookable";
7
+
8
+ // src/tools/validation.ts
9
+ function validateToolArgs(input, schema) {
10
+ const required = schema.required ?? [];
11
+ for (const field of required) {
12
+ if (!(field in input) || input[field] === void 0 || input[field] === null) {
13
+ return { valid: false, error: `Missing required field: ${field}` };
14
+ }
15
+ }
16
+ return { valid: true };
17
+ }
18
+
19
+ // src/loop.ts
20
+ async function runLoop(ctx) {
21
+ let totalIn = 0;
22
+ let totalOut = 0;
23
+ const startTime = Date.now();
24
+ const maxTurns = 50;
25
+ for (let turn = 0; turn < maxTurns; turn++) {
26
+ if (ctx.signal.aborted) {
27
+ await ctx.hooks.callHook("agent:abort", {});
28
+ break;
29
+ }
30
+ const result = await executeTurn(ctx, turn);
31
+ totalIn += result.usage.input;
32
+ totalOut += result.usage.output;
33
+ if (ctx.signal.aborted) {
34
+ await ctx.hooks.callHook("agent:abort", {});
35
+ break;
36
+ }
37
+ if (ctx.steeringQueue.length > 0) {
38
+ const steerMsg = ctx.steeringQueue.shift();
39
+ await ctx.hooks.callHook("steer:inject", { message: steerMsg });
40
+ ctx.messages.push(ctx.provider.userMessage(steerMsg));
41
+ continue;
42
+ }
43
+ if (result.ended) {
44
+ if (ctx.followUpQueue.length > 0) {
45
+ const followUp = ctx.followUpQueue.shift();
46
+ await ctx.hooks.callHook("steer:inject", { message: followUp });
47
+ ctx.messages.push(ctx.provider.userMessage(followUp));
48
+ continue;
49
+ }
50
+ return { totalIn, totalOut, turns: turn + 1, elapsed: Date.now() - startTime };
51
+ }
52
+ }
53
+ const stats = { totalIn, totalOut, turns: maxTurns, elapsed: Date.now() - startTime };
54
+ await ctx.hooks.callHook("agent:done", stats);
55
+ return stats;
56
+ }
57
+ async function executeTurn(ctx, turn) {
58
+ const streamOptions = {
59
+ model: ctx.model,
60
+ system: ctx.system,
61
+ tools: ctx.formattedTools,
62
+ messages: ctx.messages,
63
+ maxTokens: 16384,
64
+ thinking: ctx.thinking,
65
+ signal: ctx.signal
66
+ };
67
+ await ctx.hooks.callHook("context:transform", { messages: ctx.messages });
68
+ await ctx.hooks.callHook("turn:before", { turn, options: streamOptions });
69
+ let currentText = "";
70
+ const result = await ctx.provider.stream(
71
+ streamOptions,
72
+ {
73
+ onText(delta) {
74
+ currentText += delta;
75
+ ctx.hooks.callHook("stream:text", { delta, text: currentText });
76
+ }
77
+ }
78
+ );
79
+ if (currentText) {
80
+ await ctx.hooks.callHook("stream:end", { text: currentText });
81
+ }
82
+ await ctx.hooks.callHook("turn:after", { turn, usage: result.usage });
83
+ if (result.done) {
84
+ return { ended: true, usage: result.usage };
85
+ }
86
+ ctx.messages.push({ role: "assistant", content: result.assistantMessage });
87
+ const toolResults = ctx.toolExecution === "parallel" ? await executeToolsParallel(ctx, result.toolCalls) : await executeToolsSequential(ctx, result.toolCalls);
88
+ ctx.messages.push(ctx.provider.toolResultsMessage(toolResults));
89
+ return { ended: false, usage: result.usage };
90
+ }
91
+ async function executeSingleTool(ctx, call) {
92
+ const toolDef = ctx.tools[call.name];
93
+ const gateCtx = { name: call.name, input: call.input, block: false, reason: "Tool execution was blocked" };
94
+ await ctx.hooks.callHook("tool:gate", gateCtx);
95
+ if (gateCtx.block) {
96
+ return { result: { id: call.id, content: `Blocked: ${gateCtx.reason}` }, steered: false };
97
+ }
98
+ if (!toolDef) {
99
+ const err = new Error(`Unknown tool: ${call.name}`);
100
+ await ctx.hooks.callHook("tool:error", { name: call.name, input: call.input, error: err });
101
+ return { result: { id: call.id, content: `Tool error: ${err.message}` }, steered: false };
102
+ }
103
+ const validation = validateToolArgs(call.input, toolDef.spec.input_schema);
104
+ if (!validation.valid) {
105
+ return { result: { id: call.id, content: `Validation error: ${validation.error}` }, steered: false };
106
+ }
107
+ await ctx.hooks.callHook("tool:before", { name: call.name, input: call.input });
108
+ let output;
109
+ let isError = false;
110
+ try {
111
+ output = await toolDef.execute(call.input);
112
+ } catch (err) {
113
+ await ctx.hooks.callHook("tool:error", { name: call.name, input: call.input, error: err });
114
+ output = `Tool error: ${err.message}`;
115
+ isError = true;
116
+ }
117
+ const transformCtx = { name: call.name, input: call.input, result: output, isError };
118
+ await ctx.hooks.callHook("tool:transform", transformCtx);
119
+ output = transformCtx.result;
120
+ isError = transformCtx.isError;
121
+ await ctx.hooks.callHook("tool:after", { name: call.name, input: call.input, result: output });
122
+ return { result: { id: call.id, content: output }, steered: false };
123
+ }
124
+ async function executeToolsSequential(ctx, toolCalls) {
125
+ const results = [];
126
+ for (const call of toolCalls) {
127
+ if (ctx.signal.aborted)
128
+ break;
129
+ if (ctx.steeringQueue.length > 0) {
130
+ const steerMsg = ctx.steeringQueue.shift();
131
+ await ctx.hooks.callHook("steer:inject", { message: steerMsg });
132
+ for (const skipped of toolCalls.slice(toolCalls.indexOf(call))) {
133
+ results.push({ id: skipped.id, content: "Skipped: steering message received" });
134
+ }
135
+ ctx.messages.push(ctx.provider.toolResultsMessage(results));
136
+ ctx.messages.push(ctx.provider.userMessage(steerMsg));
137
+ return [];
138
+ }
139
+ const { result } = await executeSingleTool(ctx, call);
140
+ results.push(result);
141
+ }
142
+ return results;
143
+ }
144
+ async function executeToolsParallel(ctx, toolCalls) {
145
+ const executions = toolCalls.map((call) => executeSingleTool(ctx, call));
146
+ const settled = await Promise.all(executions);
147
+ return settled.map((s) => s.result);
148
+ }
149
+
150
+ // src/agent.ts
151
+ function createAgent({ harness, provider, toolExecution = "sequential" }) {
152
+ const hooks = createHooks();
153
+ let abortController;
154
+ let running = false;
155
+ let idleResolve;
156
+ let idlePromise;
157
+ const steeringQueue = [];
158
+ const followUpQueue = [];
159
+ let conversationMessages = [];
160
+ async function run(options) {
161
+ if (running) {
162
+ throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
163
+ }
164
+ running = true;
165
+ abortController = new AbortController();
166
+ idlePromise = new Promise((resolve) => {
167
+ idleResolve = resolve;
168
+ });
169
+ const thinking = options.thinking ?? "off";
170
+ const model = options.model ?? provider.meta.defaultModel;
171
+ const system = options.system || harness.system || "You are a helpful assistant.";
172
+ const tools = harness.tools;
173
+ const toolSpecs = Object.values(tools).map(
174
+ (t) => ({
175
+ name: t.spec.name,
176
+ description: t.spec.description || "",
177
+ input_schema: t.spec.input_schema
178
+ })
179
+ );
180
+ const formattedTools = provider.formatTools(toolSpecs);
181
+ const messages = [];
182
+ if (options.system) {
183
+ await hooks.callHook("system:before", { system: options.system });
184
+ messages.push(provider.userMessage(options.system));
185
+ messages.push(provider.assistantMessage("Understood. I will proceed with these instructions above the rest of my system prompt."));
186
+ }
187
+ messages.push(provider.userMessage(options.prompt, options.images));
188
+ conversationMessages = messages;
189
+ try {
190
+ const stats = await runLoop({
191
+ provider,
192
+ hooks,
193
+ tools,
194
+ toolSpecs,
195
+ formattedTools,
196
+ model,
197
+ system,
198
+ thinking,
199
+ toolExecution,
200
+ signal: abortController.signal,
201
+ steeringQueue,
202
+ followUpQueue,
203
+ messages
204
+ });
205
+ await hooks.callHook("agent:done", stats);
206
+ return stats;
207
+ } catch (err) {
208
+ if (abortController.signal.aborted) {
209
+ const stats = { totalIn: 0, totalOut: 0, turns: 0, elapsed: 0 };
210
+ await hooks.callHook("agent:done", stats);
211
+ return stats;
212
+ }
213
+ throw err;
214
+ } finally {
215
+ running = false;
216
+ abortController = void 0;
217
+ steeringQueue.length = 0;
218
+ followUpQueue.length = 0;
219
+ idleResolve?.();
220
+ idlePromise = void 0;
221
+ idleResolve = void 0;
222
+ }
223
+ }
224
+ function abort() {
225
+ abortController?.abort();
226
+ }
227
+ function steer(message) {
228
+ steeringQueue.push(message);
229
+ }
230
+ function followUpFn(message) {
231
+ followUpQueue.push(message);
232
+ }
233
+ function waitForIdle() {
234
+ return idlePromise ?? Promise.resolve();
235
+ }
236
+ function reset() {
237
+ conversationMessages = [];
238
+ steeringQueue.length = 0;
239
+ followUpQueue.length = 0;
240
+ }
241
+ return {
242
+ hooks,
243
+ run,
244
+ abort,
245
+ steer,
246
+ followUp: followUpFn,
247
+ waitForIdle,
248
+ reset,
249
+ get isRunning() {
250
+ return running;
251
+ },
252
+ get messages() {
253
+ return conversationMessages;
254
+ },
255
+ meta: provider.meta
256
+ };
257
+ }
258
+ export {
259
+ createAgent,
260
+ defineHarness
261
+ };
@@ -0,0 +1 @@
1
+ export { M as Message, P as Provider, c as StreamCallbacks, S as StreamOptions, d as ToolCall, e as ToolResult, f as ToolSpec, g as TurnResult, h as anthropic, i as cerebras, o as openrouter } from './index-ByJfS-kX.js';
@@ -0,0 +1,377 @@
1
+ // src/providers/anthropic.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ import Anthropic from "@anthropic-ai/sdk";
5
+ var CREDENTIALS_FILE = resolve(import.meta.dir, "../../.credentials.json");
6
+ function getApiKey() {
7
+ if (process.env.ANTHROPIC_API_KEY)
8
+ return process.env.ANTHROPIC_API_KEY;
9
+ if (existsSync(CREDENTIALS_FILE)) {
10
+ const creds = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
11
+ if (creds.anthropic?.access)
12
+ return creds.anthropic.access;
13
+ }
14
+ throw new Error("No API key found. Run `bun run auth` first.");
15
+ }
16
+ var THINKING_BUDGETS = {
17
+ minimal: 1024,
18
+ low: 4096,
19
+ medium: 10240,
20
+ high: 32768
21
+ };
22
+ function anthropic() {
23
+ const apiKey = getApiKey();
24
+ const isOAuth = apiKey.includes("sk-ant-oat");
25
+ const client = new Anthropic(
26
+ isOAuth ? {
27
+ apiKey: null,
28
+ authToken: apiKey,
29
+ dangerouslyAllowBrowser: true,
30
+ defaultHeaders: {
31
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
32
+ "anthropic-dangerous-direct-browser-access": "true",
33
+ "user-agent": "zidane/2.0.0",
34
+ "x-app": "cli"
35
+ }
36
+ } : { apiKey }
37
+ );
38
+ return {
39
+ name: "anthropic",
40
+ meta: { defaultModel: "claude-opus-4-6", isOAuth },
41
+ formatTools(tools) {
42
+ return tools.map((t) => ({
43
+ name: t.name,
44
+ description: t.description,
45
+ input_schema: t.input_schema
46
+ }));
47
+ },
48
+ userMessage(content, images) {
49
+ if (images && images.length > 0) {
50
+ const blocks = [
51
+ ...images.map((img) => ({
52
+ type: "image",
53
+ source: {
54
+ type: "base64",
55
+ media_type: img.source.media_type,
56
+ data: img.source.data
57
+ }
58
+ })),
59
+ { type: "text", text: content }
60
+ ];
61
+ return { role: "user", content: blocks };
62
+ }
63
+ return { role: "user", content };
64
+ },
65
+ assistantMessage(content) {
66
+ return { role: "assistant", content };
67
+ },
68
+ toolResultsMessage(results) {
69
+ return {
70
+ role: "user",
71
+ content: results.map((r) => ({
72
+ type: "tool_result",
73
+ tool_use_id: r.id,
74
+ content: r.content
75
+ }))
76
+ };
77
+ },
78
+ async stream(options, callbacks) {
79
+ let system = options.system;
80
+ const messages = [...options.messages];
81
+ const thinking = options.thinking ?? "off";
82
+ if (isOAuth) {
83
+ system = `You are Claude Code, Anthropic's official CLI for Claude.`;
84
+ messages.unshift(
85
+ { role: "user", content: options.system },
86
+ { role: "assistant", content: "Understood. I will proceed with these instructions above the rest of my system prompt." }
87
+ );
88
+ }
89
+ const params = {
90
+ model: options.model,
91
+ max_tokens: options.maxTokens ?? 16384,
92
+ system,
93
+ tools: options.tools,
94
+ messages,
95
+ stream: true
96
+ };
97
+ if (thinking !== "off") {
98
+ const budgetTokens = THINKING_BUDGETS[thinking];
99
+ params.thinking = {
100
+ type: "enabled",
101
+ budget_tokens: budgetTokens
102
+ };
103
+ params.temperature = 1;
104
+ }
105
+ const s = client.messages.stream(params, {
106
+ signal: options.signal
107
+ });
108
+ let text = "";
109
+ s.on("text", (delta) => {
110
+ text += delta;
111
+ callbacks.onText(delta);
112
+ });
113
+ const response = await s.finalMessage();
114
+ const toolCalls = response.content.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, name: b.name, input: b.input }));
115
+ return {
116
+ assistantMessage: response.content,
117
+ text,
118
+ toolCalls,
119
+ done: response.stop_reason === "end_turn" || toolCalls.length === 0,
120
+ usage: { input: response.usage.input_tokens, output: response.usage.output_tokens }
121
+ };
122
+ }
123
+ };
124
+ }
125
+
126
+ // src/providers/openai-compat.ts
127
+ var TOOL_RESULTS_TAG = "__zidane_tool_results__";
128
+ var ASSISTANT_TOOL_CALLS_TAG = "__zidane_assistant_tc__";
129
+ function convertImageContent(img) {
130
+ return {
131
+ type: "image_url",
132
+ image_url: { url: `data:${img.source.media_type};base64,${img.source.data}` }
133
+ };
134
+ }
135
+ async function consumeSSE(response, callbacks, signal) {
136
+ const reader = response.body.getReader();
137
+ const decoder = new TextDecoder();
138
+ let buffer = "";
139
+ let text = "";
140
+ let finishReason = "stop";
141
+ let usage = { input: 0, output: 0 };
142
+ const tcMap = /* @__PURE__ */ new Map();
143
+ try {
144
+ while (true) {
145
+ if (signal?.aborted)
146
+ break;
147
+ const { done, value } = await reader.read();
148
+ if (done)
149
+ break;
150
+ buffer += decoder.decode(value, { stream: true });
151
+ const lines = buffer.split("\n");
152
+ buffer = lines.pop() || "";
153
+ for (const line of lines) {
154
+ if (!line.startsWith("data: "))
155
+ continue;
156
+ const data = line.slice(6).trim();
157
+ if (data === "[DONE]")
158
+ continue;
159
+ let chunk;
160
+ try {
161
+ chunk = JSON.parse(data);
162
+ } catch {
163
+ continue;
164
+ }
165
+ const choice = chunk.choices?.[0];
166
+ if (!choice)
167
+ continue;
168
+ if (choice.finish_reason)
169
+ finishReason = choice.finish_reason;
170
+ if (choice.delta?.content) {
171
+ text += choice.delta.content;
172
+ callbacks.onText(choice.delta.content);
173
+ }
174
+ if (choice.delta?.tool_calls) {
175
+ for (const tc of choice.delta.tool_calls) {
176
+ const existing = tcMap.get(tc.index);
177
+ if (existing) {
178
+ if (tc.function?.arguments)
179
+ existing.args += tc.function.arguments;
180
+ } else {
181
+ tcMap.set(tc.index, {
182
+ id: tc.id || `call_${tc.index}`,
183
+ name: tc.function?.name || "",
184
+ args: tc.function?.arguments || ""
185
+ });
186
+ }
187
+ }
188
+ }
189
+ if (chunk.usage)
190
+ usage = { input: chunk.usage.prompt_tokens, output: chunk.usage.completion_tokens };
191
+ }
192
+ }
193
+ } finally {
194
+ reader.releaseLock();
195
+ }
196
+ const toolCalls = Array.from(tcMap.values()).map((tc) => ({
197
+ id: tc.id,
198
+ name: tc.name,
199
+ input: tc.args ? JSON.parse(tc.args) : {}
200
+ }));
201
+ return { text, toolCalls, finishReason, usage };
202
+ }
203
+ function toOAIMessages(system, messages) {
204
+ const out = [{ role: "system", content: system }];
205
+ for (const msg of messages) {
206
+ const c = msg.content;
207
+ if (c?._tag === TOOL_RESULTS_TAG) {
208
+ for (const tr of c.results) {
209
+ out.push({ role: "tool", tool_call_id: tr.tool_call_id, content: tr.content });
210
+ }
211
+ continue;
212
+ }
213
+ if (c?._tag === ASSISTANT_TOOL_CALLS_TAG) {
214
+ out.push({ role: "assistant", content: c.text || null, tool_calls: c.tool_calls });
215
+ continue;
216
+ }
217
+ out.push({ role: msg.role, content: msg.content });
218
+ }
219
+ return out;
220
+ }
221
+ function formatTools(tools) {
222
+ return tools.map((t) => ({
223
+ type: "function",
224
+ function: { name: t.name, description: t.description, parameters: t.input_schema }
225
+ }));
226
+ }
227
+ function userMessage(content, images) {
228
+ if (images?.length) {
229
+ return {
230
+ role: "user",
231
+ content: [...images.map(convertImageContent), { type: "text", text: content }]
232
+ };
233
+ }
234
+ return { role: "user", content };
235
+ }
236
+ function assistantMessage(content) {
237
+ return { role: "assistant", content };
238
+ }
239
+ function toolResultsMessage(results) {
240
+ return {
241
+ role: "user",
242
+ content: {
243
+ _tag: TOOL_RESULTS_TAG,
244
+ results: results.map((r) => ({ tool_call_id: r.id, content: r.content }))
245
+ }
246
+ };
247
+ }
248
+ function buildAssistantContent(text, toolCalls) {
249
+ if (toolCalls.length > 0) {
250
+ return {
251
+ _tag: ASSISTANT_TOOL_CALLS_TAG,
252
+ text: text || null,
253
+ tool_calls: toolCalls.map((tc) => ({
254
+ id: tc.id,
255
+ type: "function",
256
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
257
+ }))
258
+ };
259
+ }
260
+ return text;
261
+ }
262
+
263
+ // src/providers/cerebras.ts
264
+ var BASE_URL = "https://api.cerebras.ai/v1";
265
+ function getApiKey2() {
266
+ if (process.env.CEREBRAS_API_KEY)
267
+ return process.env.CEREBRAS_API_KEY;
268
+ throw new Error("No Cerebras API key found. Set CEREBRAS_API_KEY in your environment.");
269
+ }
270
+ function cerebras(defaultModel) {
271
+ const apiKey = getApiKey2();
272
+ const fallbackModel = defaultModel || "zai-glm-4.7";
273
+ return {
274
+ name: "cerebras",
275
+ meta: { defaultModel: fallbackModel },
276
+ formatTools,
277
+ userMessage,
278
+ assistantMessage,
279
+ toolResultsMessage,
280
+ async stream(options, callbacks) {
281
+ const modelId = options.model || fallbackModel;
282
+ const messages = toOAIMessages(options.system, options.messages);
283
+ const body = {
284
+ model: modelId,
285
+ messages,
286
+ max_tokens: options.maxTokens,
287
+ stream: true
288
+ };
289
+ if (options.tools && options.tools.length > 0)
290
+ body.tools = options.tools;
291
+ const response = await fetch(`${BASE_URL}/chat/completions`, {
292
+ method: "POST",
293
+ headers: {
294
+ "Authorization": `Bearer ${apiKey}`,
295
+ "Content-Type": "application/json"
296
+ },
297
+ body: JSON.stringify(body),
298
+ signal: options.signal
299
+ });
300
+ if (!response.ok) {
301
+ const errorText = await response.text();
302
+ throw new Error(`Cerebras API error: ${response.status} ${errorText}`);
303
+ }
304
+ const result = await consumeSSE(response, callbacks, options.signal);
305
+ return {
306
+ assistantMessage: buildAssistantContent(result.text, result.toolCalls),
307
+ text: result.text,
308
+ toolCalls: result.toolCalls,
309
+ done: result.finishReason === "stop" || result.toolCalls.length === 0,
310
+ usage: result.usage
311
+ };
312
+ }
313
+ };
314
+ }
315
+
316
+ // src/providers/openrouter.ts
317
+ var BASE_URL2 = "https://openrouter.ai/api/v1";
318
+ function getApiKey3() {
319
+ if (process.env.OPENROUTER_API_KEY)
320
+ return process.env.OPENROUTER_API_KEY;
321
+ throw new Error("No OpenRouter API key found. Set OPENROUTER_API_KEY in your environment.");
322
+ }
323
+ function openrouter(defaultModel) {
324
+ const apiKey = getApiKey3();
325
+ const fallbackModel = defaultModel || "anthropic/claude-sonnet-4-6";
326
+ return {
327
+ name: "openrouter",
328
+ meta: { defaultModel: fallbackModel },
329
+ formatTools,
330
+ userMessage,
331
+ assistantMessage,
332
+ toolResultsMessage,
333
+ async stream(options, callbacks) {
334
+ let modelId = options.model || fallbackModel;
335
+ const thinking = options.thinking ?? "off";
336
+ if (thinking !== "off" && !modelId.includes(":thinking"))
337
+ modelId = `${modelId}:thinking`;
338
+ const messages = toOAIMessages(options.system, options.messages);
339
+ const body = {
340
+ model: modelId,
341
+ messages,
342
+ max_tokens: options.maxTokens,
343
+ stream: true
344
+ };
345
+ if (options.tools && options.tools.length > 0)
346
+ body.tools = options.tools;
347
+ const response = await fetch(`${BASE_URL2}/chat/completions`, {
348
+ method: "POST",
349
+ headers: {
350
+ "Authorization": `Bearer ${apiKey}`,
351
+ "Content-Type": "application/json",
352
+ "HTTP-Referer": "https://github.com/Tahul/zidane",
353
+ "X-Title": "zidane"
354
+ },
355
+ body: JSON.stringify(body),
356
+ signal: options.signal
357
+ });
358
+ if (!response.ok) {
359
+ const errorText = await response.text();
360
+ throw new Error(`OpenRouter API error: ${response.status} ${errorText}`);
361
+ }
362
+ const result = await consumeSSE(response, callbacks, options.signal);
363
+ return {
364
+ assistantMessage: buildAssistantContent(result.text, result.toolCalls),
365
+ text: result.text,
366
+ toolCalls: result.toolCalls,
367
+ done: result.finishReason === "stop" || result.toolCalls.length === 0,
368
+ usage: result.usage
369
+ };
370
+ }
371
+ };
372
+ }
373
+ export {
374
+ anthropic,
375
+ cerebras,
376
+ openrouter
377
+ };
package/package.json CHANGED
@@ -1,11 +1,60 @@
1
1
  {
2
2
  "name": "zidane",
3
- "version": "1.0.2",
4
- "description": "ZidaneJS",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
3
+ "version": "1.1.5",
4
+ "description": "an agent that goes straight to the goal",
5
+ "type": "module",
6
+ "private": false,
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Tahul/zidane"
8
10
  },
9
11
  "author": "Yaël GUILLOUX <yael.guilloux@gmail.com>",
10
- "license": "ISC"
12
+ "license": "MIT",
13
+ "bugs": {
14
+ "url": "https://github.com/Tahul/zidane/issues"
15
+ },
16
+ "homepage": "https://github.com/Tahul/zidane",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ },
22
+ "./providers": {
23
+ "import": "./dist/providers.js",
24
+ "types": "./dist/providers.d.ts"
25
+ },
26
+ "./harnesses": {
27
+ "import": "./dist/harnesses.js",
28
+ "types": "./dist/harnesses.d.ts"
29
+ }
30
+ },
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "auth": "bun run src/auth.ts",
38
+ "start": "bun run src/start.ts",
39
+ "build": "tsup",
40
+ "lint": "eslint .",
41
+ "lint:fix": "eslint . --fix",
42
+ "test": "bun test",
43
+ "release": "bumpp"
44
+ },
45
+ "dependencies": {
46
+ "@anthropic-ai/sdk": "^0.80.0",
47
+ "@yaelg/pi-ai": "^0.58.4",
48
+ "chalk": "^5.6.2",
49
+ "hookable": "^6.1.0",
50
+ "md4x": "^0.0.25"
51
+ },
52
+ "devDependencies": {
53
+ "@antfu/eslint-config": "^7.7.3",
54
+ "@types/bun": "^1.3.11",
55
+ "bumpp": "^11.0.1",
56
+ "eslint": "^10.0.3",
57
+ "jiti": "^2.6.1",
58
+ "tsup": "^8.5.1"
59
+ }
11
60
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- export default () => console.log('1998')
package/zidane.jpeg DELETED
Binary file