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 +296 -0
- package/dist/chunk-ECE5USCO.js +125 -0
- package/dist/harnesses.d.ts +24 -0
- package/dist/harnesses.js +8 -0
- package/dist/index-ByJfS-kX.d.ts +101 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +261 -0
- package/dist/providers.d.ts +1 -0
- package/dist/providers.js +377 -0
- package/package.json +55 -6
- package/index.js +0 -1
- package/zidane.jpeg +0 -0
package/README.md
CHANGED
|
@@ -1 +1,297 @@
|
|
|
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
|
+
## 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,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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
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": "
|
|
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
|