@witqq/agent-sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -8
- package/dist/backends/claude.cjs +160 -8
- package/dist/backends/claude.cjs.map +1 -1
- package/dist/backends/claude.d.cts +1 -1
- package/dist/backends/claude.d.ts +1 -1
- package/dist/backends/claude.js +160 -8
- package/dist/backends/claude.js.map +1 -1
- package/dist/backends/copilot.cjs +140 -10
- package/dist/backends/copilot.cjs.map +1 -1
- package/dist/backends/copilot.d.cts +3 -1
- package/dist/backends/copilot.d.ts +3 -1
- package/dist/backends/copilot.js +140 -10
- package/dist/backends/copilot.js.map +1 -1
- package/dist/backends/vercel-ai.cjs +117 -5
- package/dist/backends/vercel-ai.cjs.map +1 -1
- package/dist/backends/vercel-ai.d.cts +1 -1
- package/dist/backends/vercel-ai.d.ts +1 -1
- package/dist/backends/vercel-ai.js +117 -5
- package/dist/backends/vercel-ai.js.map +1 -1
- package/dist/index.cjs +113 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -5
- package/dist/index.d.ts +54 -5
- package/dist/index.js +113 -4
- package/dist/index.js.map +1 -1
- package/dist/{types-JVBEqeDw.d.cts → types-CBzhRrN9.d.cts} +41 -4
- package/dist/{types-JVBEqeDw.d.ts → types-CBzhRrN9.d.ts} +41 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,11 +10,13 @@ npm install @witqq/agent-sdk zod
|
|
|
10
10
|
|
|
11
11
|
## Backends
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
|
16
|
-
|
|
17
|
-
| `
|
|
13
|
+
`zod` is the only required peer dependency. Backend SDKs are **optional** — install only what you use:
|
|
14
|
+
|
|
15
|
+
| Backend | Peer dependency | Required | Type |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `copilot` | `@github/copilot-sdk` ^0.1.22 | optional | CLI subprocess |
|
|
18
|
+
| `claude` | `@anthropic-ai/claude-agent-sdk` >=0.2.0 | optional | CLI subprocess |
|
|
19
|
+
| `vercel-ai` | `ai` >=4.0.0 + `@ai-sdk/openai-compatible` >=2.0.0 | optional | API-based |
|
|
18
20
|
|
|
19
21
|
Install only the backend you need:
|
|
20
22
|
|
|
@@ -194,21 +196,73 @@ for await (const event of agent.stream("Tell me a story")) {
|
|
|
194
196
|
}
|
|
195
197
|
```
|
|
196
198
|
|
|
199
|
+
### Streaming with Conversation History
|
|
200
|
+
|
|
201
|
+
Use `streamWithContext` to stream with full conversation history:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const messages = [
|
|
205
|
+
{ role: "system" as const, content: "You are helpful." },
|
|
206
|
+
{ role: "user" as const, content: "Hello" },
|
|
207
|
+
{ role: "assistant" as const, content: "Hi! How can I help?" },
|
|
208
|
+
{ role: "user" as const, content: "What is 2+2?" },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
for await (const event of agent.streamWithContext(messages)) {
|
|
212
|
+
if (event.type === "text_delta") process.stdout.write(event.text);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
197
216
|
| Event | Fields | Description |
|
|
198
217
|
|-------|--------|-------------|
|
|
199
218
|
| `text_delta` | `text` | Incremental text output |
|
|
219
|
+
| `thinking_delta` | `text` | Incremental reasoning/thinking text |
|
|
200
220
|
| `thinking_start` | — | Model started reasoning |
|
|
201
221
|
| `thinking_end` | — | Model finished reasoning |
|
|
202
|
-
| `tool_call_start` | `toolName`, `args` | Tool invocation began |
|
|
203
|
-
| `tool_call_end` | `toolName`, `result` | Tool invocation completed |
|
|
222
|
+
| `tool_call_start` | `toolCallId`, `toolName`, `args` | Tool invocation began |
|
|
223
|
+
| `tool_call_end` | `toolCallId`, `toolName`, `result` | Tool invocation completed |
|
|
204
224
|
| `permission_request` | `request` | Permission check initiated |
|
|
205
225
|
| `permission_response` | `toolName`, `decision` | Permission decision made |
|
|
206
226
|
| `ask_user` | `request` | User input requested |
|
|
207
227
|
| `ask_user_response` | `answer` | User response received |
|
|
208
|
-
| `usage_update` | `promptTokens`, `completionTokens` | Token usage |
|
|
228
|
+
| `usage_update` | `promptTokens`, `completionTokens`, `model?`, `backend?` | Token usage with metadata |
|
|
229
|
+
| `heartbeat` | — | Keepalive signal during long operations |
|
|
209
230
|
| `error` | `error`, `recoverable` | Error during execution |
|
|
210
231
|
| `done` | `finalOutput`, `structuredOutput?` | Execution completed |
|
|
211
232
|
|
|
233
|
+
## Usage Tracking
|
|
234
|
+
|
|
235
|
+
Track token usage with the `onUsage` callback. Called after each `run()`/`runWithContext()`/`runStructured()` completion and during `stream()`/`streamWithContext()` when usage data arrives:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const agent = service.createAgent({
|
|
239
|
+
systemPrompt: "You are a helpful assistant.",
|
|
240
|
+
onUsage: (usage) => {
|
|
241
|
+
console.log(`${usage.backend}/${usage.model}: ${usage.promptTokens}+${usage.completionTokens} tokens`);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Usage data includes `promptTokens`, `completionTokens`, and optional `model` and `backend` fields. Callback errors are logged but not propagated (fire-and-forget).
|
|
247
|
+
|
|
248
|
+
## Heartbeat
|
|
249
|
+
|
|
250
|
+
Keep HTTP streams alive during long tool executions by emitting periodic heartbeat events:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const agent = service.createAgent({
|
|
254
|
+
systemPrompt: "You are a helpful assistant.",
|
|
255
|
+
heartbeatInterval: 15000, // emit heartbeat every 15s during gaps
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
for await (const event of agent.stream("Run a long analysis")) {
|
|
259
|
+
if (event.type === "heartbeat") continue; // ignore keepalive
|
|
260
|
+
// handle other events...
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
When `heartbeatInterval` is set, heartbeat events are emitted during streaming gaps (e.g., while a tool executes). No heartbeats are emitted when backend events flow continuously. The timer is cleaned up when the stream completes, errors, or is aborted.
|
|
265
|
+
|
|
212
266
|
## Backend-Specific Options
|
|
213
267
|
|
|
214
268
|
### Copilot
|
|
@@ -221,6 +275,23 @@ const service = createCopilotService({
|
|
|
221
275
|
cliPath: "/path/to/copilot", // optional custom CLI path
|
|
222
276
|
workingDirectory: process.cwd(),
|
|
223
277
|
githubToken: "ghp_...", // optional, alternative to useLoggedInUser
|
|
278
|
+
cliArgs: ["--allow-all"], // extra CLI flags for the subprocess
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**System requirements:** `@github/copilot-sdk` includes a native binary that requires glibc. Alpine Linux (musl) is not supported — use `node:20-bookworm-slim` or similar glibc-based images.
|
|
283
|
+
|
|
284
|
+
**Headless defaults:** When `supervisor.onPermission` or `supervisor.onAskUser` are not provided, the Copilot backend auto-approves permission requests and auto-answers user questions to prevent the SDK from hanging in headless mode.
|
|
285
|
+
|
|
286
|
+
**System prompt mode:** By default, `systemPrompt` is appended to the Copilot CLI's built-in prompt (`mode: "append"`). Set `systemMessageMode: "replace"` in `AgentConfig` to fully replace it (note: this removes built-in tool instructions).
|
|
287
|
+
|
|
288
|
+
**Available tools filter:** Use `availableTools` in `AgentConfig` to restrict which Copilot built-in tools are available:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const agent = service.createAgent({
|
|
292
|
+
systemPrompt: "Research assistant",
|
|
293
|
+
tools: [],
|
|
294
|
+
availableTools: ["web_search", "web_fetch"], // only these built-in tools
|
|
224
295
|
});
|
|
225
296
|
```
|
|
226
297
|
|
|
@@ -297,6 +368,18 @@ import { createClaudeService } from "@witqq/agent-sdk/claude";
|
|
|
297
368
|
import { createVercelAIService } from "@witqq/agent-sdk/vercel-ai";
|
|
298
369
|
```
|
|
299
370
|
|
|
371
|
+
## Model Names
|
|
372
|
+
|
|
373
|
+
`AgentConfig.model` accepts both full model IDs and short names:
|
|
374
|
+
|
|
375
|
+
| Backend | Full ID example | Short name |
|
|
376
|
+
|---|---|---|
|
|
377
|
+
| Copilot | `gpt-4o` | (same) |
|
|
378
|
+
| Claude | `claude-sonnet-4-5-20250514` | `sonnet` |
|
|
379
|
+
| Vercel AI | `anthropic/claude-sonnet-4-5` | (provider-specific) |
|
|
380
|
+
|
|
381
|
+
Use `service.listModels()` to get available model IDs for each backend.
|
|
382
|
+
|
|
300
383
|
## Build
|
|
301
384
|
|
|
302
385
|
```bash
|
package/dist/backends/claude.cjs
CHANGED
|
@@ -54,7 +54,9 @@ var BaseAgent = class {
|
|
|
54
54
|
this.state = "running";
|
|
55
55
|
try {
|
|
56
56
|
const messages = [{ role: "user", content: prompt }];
|
|
57
|
-
|
|
57
|
+
const result = await this.executeRun(messages, options, ac.signal);
|
|
58
|
+
this.enrichAndNotifyUsage(result);
|
|
59
|
+
return result;
|
|
58
60
|
} finally {
|
|
59
61
|
this.state = "idle";
|
|
60
62
|
this.abortController = null;
|
|
@@ -66,7 +68,9 @@ var BaseAgent = class {
|
|
|
66
68
|
const ac = this.createAbortController(options?.signal);
|
|
67
69
|
this.state = "running";
|
|
68
70
|
try {
|
|
69
|
-
|
|
71
|
+
const result = await this.executeRun(messages, options, ac.signal);
|
|
72
|
+
this.enrichAndNotifyUsage(result);
|
|
73
|
+
return result;
|
|
70
74
|
} finally {
|
|
71
75
|
this.state = "idle";
|
|
72
76
|
this.abortController = null;
|
|
@@ -79,12 +83,14 @@ var BaseAgent = class {
|
|
|
79
83
|
this.state = "running";
|
|
80
84
|
try {
|
|
81
85
|
const messages = [{ role: "user", content: prompt }];
|
|
82
|
-
|
|
86
|
+
const result = await this.executeRunStructured(
|
|
83
87
|
messages,
|
|
84
88
|
schema,
|
|
85
89
|
options,
|
|
86
90
|
ac.signal
|
|
87
91
|
);
|
|
92
|
+
this.enrichAndNotifyUsage(result);
|
|
93
|
+
return result;
|
|
88
94
|
} finally {
|
|
89
95
|
this.state = "idle";
|
|
90
96
|
this.abortController = null;
|
|
@@ -97,7 +103,21 @@ var BaseAgent = class {
|
|
|
97
103
|
this.state = "streaming";
|
|
98
104
|
try {
|
|
99
105
|
const messages = [{ role: "user", content: prompt }];
|
|
100
|
-
|
|
106
|
+
const enriched = this.enrichStream(this.executeStream(messages, options, ac.signal));
|
|
107
|
+
yield* this.heartbeatStream(enriched);
|
|
108
|
+
} finally {
|
|
109
|
+
this.state = "idle";
|
|
110
|
+
this.abortController = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async *streamWithContext(messages, options) {
|
|
114
|
+
this.guardReentrancy();
|
|
115
|
+
this.guardDisposed();
|
|
116
|
+
const ac = this.createAbortController(options?.signal);
|
|
117
|
+
this.state = "streaming";
|
|
118
|
+
try {
|
|
119
|
+
const enriched = this.enrichStream(this.executeStream(messages, options, ac.signal));
|
|
120
|
+
yield* this.heartbeatStream(enriched);
|
|
101
121
|
} finally {
|
|
102
122
|
this.state = "idle";
|
|
103
123
|
this.abortController = null;
|
|
@@ -119,6 +139,95 @@ var BaseAgent = class {
|
|
|
119
139
|
this.abort();
|
|
120
140
|
this.state = "disposed";
|
|
121
141
|
}
|
|
142
|
+
// ─── Usage Enrichment ───────────────────────────────────────────
|
|
143
|
+
/** Enrich result usage with model/backend and fire onUsage callback */
|
|
144
|
+
enrichAndNotifyUsage(result) {
|
|
145
|
+
if (result.usage) {
|
|
146
|
+
result.usage = {
|
|
147
|
+
...result.usage,
|
|
148
|
+
model: this.config.model,
|
|
149
|
+
backend: this.backendName
|
|
150
|
+
};
|
|
151
|
+
this.callOnUsage(result.usage);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Wrap a stream to enrich usage_update events and fire onUsage callback */
|
|
155
|
+
async *enrichStream(source) {
|
|
156
|
+
for await (const event of source) {
|
|
157
|
+
if (event.type === "usage_update") {
|
|
158
|
+
const usage = {
|
|
159
|
+
promptTokens: event.promptTokens,
|
|
160
|
+
completionTokens: event.completionTokens,
|
|
161
|
+
model: this.config.model,
|
|
162
|
+
backend: this.backendName
|
|
163
|
+
};
|
|
164
|
+
this.callOnUsage(usage);
|
|
165
|
+
yield { type: "usage_update", ...usage };
|
|
166
|
+
} else {
|
|
167
|
+
yield event;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Fire onUsage callback (fire-and-forget: errors logged, not propagated) */
|
|
172
|
+
callOnUsage(usage) {
|
|
173
|
+
if (!this.config.onUsage) return;
|
|
174
|
+
try {
|
|
175
|
+
this.config.onUsage(usage);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.warn(
|
|
178
|
+
"[agent-sdk] onUsage callback error:",
|
|
179
|
+
e instanceof Error ? e.message : String(e)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ─── Heartbeat ───────────────────────────────────────────────
|
|
184
|
+
/** Wrap a stream to emit heartbeat events at configured intervals.
|
|
185
|
+
* When heartbeatInterval is not set, passes through directly. */
|
|
186
|
+
async *heartbeatStream(source) {
|
|
187
|
+
const interval = this.config.heartbeatInterval;
|
|
188
|
+
if (!interval || interval <= 0) {
|
|
189
|
+
yield* source;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
193
|
+
let pendingEvent = null;
|
|
194
|
+
let heartbeatResolve = null;
|
|
195
|
+
const timer = setInterval(() => {
|
|
196
|
+
if (heartbeatResolve) {
|
|
197
|
+
const resolve = heartbeatResolve;
|
|
198
|
+
heartbeatResolve = null;
|
|
199
|
+
resolve();
|
|
200
|
+
}
|
|
201
|
+
}, interval);
|
|
202
|
+
try {
|
|
203
|
+
while (true) {
|
|
204
|
+
if (!pendingEvent) {
|
|
205
|
+
pendingEvent = iterator.next();
|
|
206
|
+
}
|
|
207
|
+
const heartbeatPromise = new Promise((resolve) => {
|
|
208
|
+
heartbeatResolve = resolve;
|
|
209
|
+
});
|
|
210
|
+
const eventDone = pendingEvent.then(
|
|
211
|
+
(r) => ({ kind: "event", result: r })
|
|
212
|
+
);
|
|
213
|
+
const heartbeatDone = heartbeatPromise.then(
|
|
214
|
+
() => ({ kind: "heartbeat" })
|
|
215
|
+
);
|
|
216
|
+
const winner = await Promise.race([eventDone, heartbeatDone]);
|
|
217
|
+
if (winner.kind === "heartbeat") {
|
|
218
|
+
yield { type: "heartbeat" };
|
|
219
|
+
} else {
|
|
220
|
+
pendingEvent = null;
|
|
221
|
+
heartbeatResolve = null;
|
|
222
|
+
if (winner.result.done) break;
|
|
223
|
+
yield winner.result.value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
clearInterval(timer);
|
|
228
|
+
heartbeatResolve = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
122
231
|
// ─── Guards ───────────────────────────────────────────────────
|
|
123
232
|
guardReentrancy() {
|
|
124
233
|
if (this.state === "running" || this.state === "streaming") {
|
|
@@ -344,7 +453,31 @@ function aggregateUsage(modelUsage) {
|
|
|
344
453
|
}
|
|
345
454
|
return { promptTokens, completionTokens };
|
|
346
455
|
}
|
|
347
|
-
|
|
456
|
+
var ClaudeToolCallTracker = class {
|
|
457
|
+
queues = /* @__PURE__ */ new Map();
|
|
458
|
+
trackStart(toolCallId, toolName) {
|
|
459
|
+
if (!this.queues.has(toolName)) {
|
|
460
|
+
this.queues.set(toolName, []);
|
|
461
|
+
}
|
|
462
|
+
this.queues.get(toolName).push(toolCallId);
|
|
463
|
+
}
|
|
464
|
+
/** Peek at the current tool call ID for a tool name (does not consume) */
|
|
465
|
+
peekToolCallId(toolName) {
|
|
466
|
+
const queue = this.queues.get(toolName);
|
|
467
|
+
if (!queue || queue.length === 0) return "";
|
|
468
|
+
return queue[0];
|
|
469
|
+
}
|
|
470
|
+
/** Consume and return the first tool call ID for a tool name */
|
|
471
|
+
consumeToolCallId(toolName) {
|
|
472
|
+
const queue = this.queues.get(toolName);
|
|
473
|
+
if (!queue || queue.length === 0) return "";
|
|
474
|
+
return queue.shift();
|
|
475
|
+
}
|
|
476
|
+
clear() {
|
|
477
|
+
this.queues.clear();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
function mapSDKMessage(msg, thinkingBlockIndices, toolCallTracker) {
|
|
348
481
|
switch (msg.type) {
|
|
349
482
|
case "assistant": {
|
|
350
483
|
const betaMessage = msg.message;
|
|
@@ -356,9 +489,15 @@ function mapSDKMessage(msg, thinkingBlockIndices) {
|
|
|
356
489
|
}
|
|
357
490
|
for (const block of betaMessage.content) {
|
|
358
491
|
if (block.type === "tool_use") {
|
|
492
|
+
const toolCallId = String(block.id ?? "");
|
|
493
|
+
const toolName = block.name ?? "unknown";
|
|
494
|
+
if (toolCallTracker) {
|
|
495
|
+
toolCallTracker.trackStart(toolCallId, toolName);
|
|
496
|
+
}
|
|
359
497
|
events.push({
|
|
360
498
|
type: "tool_call_start",
|
|
361
|
-
|
|
499
|
+
toolCallId,
|
|
500
|
+
toolName,
|
|
362
501
|
args: block.input ?? {}
|
|
363
502
|
});
|
|
364
503
|
}
|
|
@@ -375,9 +514,18 @@ function mapSDKMessage(msg, thinkingBlockIndices) {
|
|
|
375
514
|
case "tool_use_summary": {
|
|
376
515
|
const summary = msg.summary;
|
|
377
516
|
const toolName = msg.tool_name ?? "unknown";
|
|
517
|
+
const precedingIds = msg.preceding_tool_use_ids;
|
|
518
|
+
let toolCallId = "";
|
|
519
|
+
if (precedingIds && precedingIds.length > 0) {
|
|
520
|
+
toolCallId = precedingIds[0];
|
|
521
|
+
if (toolCallTracker) toolCallTracker.consumeToolCallId(toolName);
|
|
522
|
+
} else if (toolCallTracker) {
|
|
523
|
+
toolCallId = toolCallTracker.consumeToolCallId(toolName);
|
|
524
|
+
}
|
|
378
525
|
if (summary) {
|
|
379
526
|
return {
|
|
380
527
|
type: "tool_call_end",
|
|
528
|
+
toolCallId,
|
|
381
529
|
toolName,
|
|
382
530
|
result: summary
|
|
383
531
|
};
|
|
@@ -404,7 +552,9 @@ function mapSDKMessage(msg, thinkingBlockIndices) {
|
|
|
404
552
|
}
|
|
405
553
|
case "tool_progress": {
|
|
406
554
|
const toolName = msg.tool_name;
|
|
407
|
-
|
|
555
|
+
if (!toolName) return null;
|
|
556
|
+
const toolCallId = toolCallTracker?.peekToolCallId(toolName) ?? "";
|
|
557
|
+
return { type: "tool_call_start", toolCallId, toolName, args: {} };
|
|
408
558
|
}
|
|
409
559
|
case "result": {
|
|
410
560
|
if (msg.subtype === "success") {
|
|
@@ -429,6 +579,7 @@ function mapSDKMessage(msg, thinkingBlockIndices) {
|
|
|
429
579
|
}
|
|
430
580
|
}
|
|
431
581
|
var ClaudeAgent = class extends BaseAgent {
|
|
582
|
+
backendName = "claude";
|
|
432
583
|
options;
|
|
433
584
|
tools;
|
|
434
585
|
canUseTool;
|
|
@@ -609,10 +760,11 @@ var ClaudeAgent = class extends BaseAgent {
|
|
|
609
760
|
opts = await this.buildMcpConfig(opts);
|
|
610
761
|
const q = sdk.query({ prompt, options: opts });
|
|
611
762
|
const thinkingBlockIndices = /* @__PURE__ */ new Set();
|
|
763
|
+
const toolCallTracker = new ClaudeToolCallTracker();
|
|
612
764
|
try {
|
|
613
765
|
for await (const msg of q) {
|
|
614
766
|
if (signal.aborted) throw new AbortError();
|
|
615
|
-
const event = mapSDKMessage(msg, thinkingBlockIndices);
|
|
767
|
+
const event = mapSDKMessage(msg, thinkingBlockIndices, toolCallTracker);
|
|
616
768
|
if (event) {
|
|
617
769
|
if (Array.isArray(event)) {
|
|
618
770
|
for (const e of event) yield e;
|