@witqq/agent-sdk 0.8.0 → 0.9.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.
Files changed (113) hide show
  1. package/dist/{agent-DxY68NZL.d.cts → agent-C6H2CgJA.d.cts} +2 -0
  2. package/dist/{agent-CW9XbmG_.d.ts → agent-F7oB6eKp.d.ts} +2 -0
  3. package/dist/backends/claude.cjs.map +1 -1
  4. package/dist/backends/claude.d.cts +2 -2
  5. package/dist/backends/claude.d.ts +2 -2
  6. package/dist/backends/claude.js.map +1 -1
  7. package/dist/backends/copilot.cjs +8 -15
  8. package/dist/backends/copilot.cjs.map +1 -1
  9. package/dist/backends/copilot.d.cts +2 -2
  10. package/dist/backends/copilot.d.ts +2 -2
  11. package/dist/backends/copilot.js +8 -15
  12. package/dist/backends/copilot.js.map +1 -1
  13. package/dist/backends/mock-llm.cjs +719 -0
  14. package/dist/backends/mock-llm.cjs.map +1 -0
  15. package/dist/backends/mock-llm.d.cts +37 -0
  16. package/dist/backends/mock-llm.d.ts +37 -0
  17. package/dist/backends/mock-llm.js +717 -0
  18. package/dist/backends/mock-llm.js.map +1 -0
  19. package/dist/backends/vercel-ai.cjs +8 -1
  20. package/dist/backends/vercel-ai.cjs.map +1 -1
  21. package/dist/backends/vercel-ai.d.cts +2 -2
  22. package/dist/backends/vercel-ai.d.ts +2 -2
  23. package/dist/backends/vercel-ai.js +8 -1
  24. package/dist/backends/vercel-ai.js.map +1 -1
  25. package/dist/backends-Cno0gZjy.d.cts +114 -0
  26. package/dist/backends-Cno0gZjy.d.ts +114 -0
  27. package/dist/chat/accumulator.cjs.map +1 -1
  28. package/dist/chat/accumulator.d.cts +2 -2
  29. package/dist/chat/accumulator.d.ts +2 -2
  30. package/dist/chat/accumulator.js.map +1 -1
  31. package/dist/chat/backends.cjs +350 -77
  32. package/dist/chat/backends.cjs.map +1 -1
  33. package/dist/chat/backends.d.cts +7 -7
  34. package/dist/chat/backends.d.ts +7 -7
  35. package/dist/chat/backends.js +349 -78
  36. package/dist/chat/backends.js.map +1 -1
  37. package/dist/chat/context.d.cts +2 -2
  38. package/dist/chat/context.d.ts +2 -2
  39. package/dist/chat/core.cjs +35 -25
  40. package/dist/chat/core.cjs.map +1 -1
  41. package/dist/chat/core.d.cts +15 -5
  42. package/dist/chat/core.d.ts +15 -5
  43. package/dist/chat/core.js +35 -26
  44. package/dist/chat/core.js.map +1 -1
  45. package/dist/chat/events.d.cts +2 -2
  46. package/dist/chat/events.d.ts +2 -2
  47. package/dist/chat/index.cjs +418 -122
  48. package/dist/chat/index.cjs.map +1 -1
  49. package/dist/chat/index.d.cts +7 -7
  50. package/dist/chat/index.d.ts +7 -7
  51. package/dist/chat/index.js +418 -124
  52. package/dist/chat/index.js.map +1 -1
  53. package/dist/chat/react.cjs +216 -12
  54. package/dist/chat/react.cjs.map +1 -1
  55. package/dist/chat/react.d.cts +78 -4
  56. package/dist/chat/react.d.ts +78 -4
  57. package/dist/chat/react.js +215 -13
  58. package/dist/chat/react.js.map +1 -1
  59. package/dist/chat/runtime.cjs +6 -2
  60. package/dist/chat/runtime.cjs.map +1 -1
  61. package/dist/chat/runtime.d.cts +2 -2
  62. package/dist/chat/runtime.d.ts +2 -2
  63. package/dist/chat/runtime.js +6 -2
  64. package/dist/chat/runtime.js.map +1 -1
  65. package/dist/chat/server.cjs +15 -5
  66. package/dist/chat/server.cjs.map +1 -1
  67. package/dist/chat/server.d.cts +3 -3
  68. package/dist/chat/server.d.ts +3 -3
  69. package/dist/chat/server.js +15 -5
  70. package/dist/chat/server.js.map +1 -1
  71. package/dist/chat/sessions.cjs +39 -23
  72. package/dist/chat/sessions.cjs.map +1 -1
  73. package/dist/chat/sessions.d.cts +2 -2
  74. package/dist/chat/sessions.d.ts +2 -2
  75. package/dist/chat/sessions.js +40 -24
  76. package/dist/chat/sessions.js.map +1 -1
  77. package/dist/chat/sqlite.cjs +95 -0
  78. package/dist/chat/sqlite.cjs.map +1 -1
  79. package/dist/chat/sqlite.d.cts +39 -3
  80. package/dist/chat/sqlite.d.ts +39 -3
  81. package/dist/chat/sqlite.js +93 -1
  82. package/dist/chat/sqlite.js.map +1 -1
  83. package/dist/chat/state.d.cts +2 -2
  84. package/dist/chat/state.d.ts +2 -2
  85. package/dist/chat/storage.cjs +39 -23
  86. package/dist/chat/storage.cjs.map +1 -1
  87. package/dist/chat/storage.d.cts +7 -3
  88. package/dist/chat/storage.d.ts +7 -3
  89. package/dist/chat/storage.js +40 -24
  90. package/dist/chat/storage.js.map +1 -1
  91. package/dist/{in-process-transport-C1JnJGVR.d.ts → in-process-transport-7EIit9Xk.d.ts} +51 -17
  92. package/dist/{in-process-transport-C7DSqPyX.d.cts → in-process-transport-Ct9YcX8I.d.cts} +51 -17
  93. package/dist/index.cjs +14 -14
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/index.d.cts +4 -2
  96. package/dist/index.d.ts +4 -2
  97. package/dist/index.js +13 -13
  98. package/dist/index.js.map +1 -1
  99. package/dist/testing.cjs +724 -0
  100. package/dist/testing.cjs.map +1 -1
  101. package/dist/testing.d.cts +14 -2
  102. package/dist/testing.d.ts +14 -2
  103. package/dist/testing.js +724 -0
  104. package/dist/testing.js.map +1 -1
  105. package/dist/{transport-Cdh3M0tS.d.cts → transport-DLWCN18G.d.cts} +1 -1
  106. package/dist/{transport-Ciap4PWK.d.ts → transport-DsuS-GeM.d.ts} +1 -1
  107. package/dist/{types-ajANVzf7.d.ts → types-DgtI1hzh.d.ts} +2 -1
  108. package/dist/{types-DRgd_9R7.d.cts → types-DkSXALKg.d.cts} +2 -1
  109. package/package.json +18 -7
  110. package/LICENSE +0 -21
  111. package/README.md +0 -1054
  112. package/dist/backends-BSrsBYFn.d.cts +0 -39
  113. package/dist/backends-BSrsBYFn.d.ts +0 -39
package/README.md DELETED
@@ -1,1054 +0,0 @@
1
- # agent-sdk
2
-
3
- Multi-backend AI agent abstraction layer for Node.js. Switch between Copilot CLI, Claude CLI, and Vercel AI SDK backends with a unified API.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @witqq/agent-sdk zod
9
- ```
10
-
11
- ## Backends
12
-
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 |
20
-
21
- Install only the backend you need:
22
-
23
- ```bash
24
- npm install @github/copilot-sdk # copilot
25
- npm install @anthropic-ai/claude-agent-sdk # claude
26
- npm install ai @ai-sdk/openai-compatible # vercel-ai
27
- ```
28
-
29
- ## Quick Start
30
-
31
- ```typescript
32
- import { createAgentService } from "@witqq/agent-sdk";
33
- import { z } from "zod";
34
-
35
- const service = await createAgentService("copilot", { useLoggedInUser: true });
36
-
37
- const agent = service.createAgent({
38
- systemPrompt: "You are a helpful assistant.",
39
- tools: [
40
- {
41
- name: "search",
42
- description: "Search the web",
43
- parameters: z.object({ query: z.string() }),
44
- execute: async ({ query }) => ({ results: [`Result for: ${query}`] }),
45
- },
46
- ],
47
- });
48
-
49
- const result = await agent.run("Find news about AI");
50
- console.log(result.output);
51
-
52
- agent.dispose();
53
- await service.dispose();
54
- ```
55
-
56
- ### Retry on Transient Errors
57
-
58
- `BaseAgent` supports automatic retry for transient failures:
59
-
60
- ```typescript
61
- const agent = service.createAgent({ systemPrompt: "..." });
62
- const result = await agent.run("prompt", {
63
- model: "gpt-5-mini",
64
- retry: {
65
- maxRetries: 3,
66
- initialDelayMs: 1000,
67
- backoffMultiplier: 2,
68
- },
69
- });
70
- ```
71
-
72
- Retries on transient error codes: `TIMEOUT`, `RATE_LIMIT`, `NETWORK`, `MODEL_OVERLOADED`. Never retries `AbortError`, `ReentrancyError`, or `DisposedError`.
73
-
74
- ## Tool Definition
75
-
76
- Tools are defined with a Zod schema for parameters and an `execute` function:
77
-
78
- ```typescript
79
- import { z } from "zod";
80
- import type { ToolDefinition } from "@witqq/agent-sdk";
81
-
82
- // Basic tool
83
- const searchTool: ToolDefinition = {
84
- name: "search",
85
- description: "Search the web",
86
- parameters: z.object({ query: z.string() }),
87
- execute: async ({ query }) => ({ results: [`Result for: ${query}`] }),
88
- };
89
-
90
- // Tool requiring user approval before execution
91
- const writeFileTool: ToolDefinition = {
92
- name: "write_file",
93
- description: "Write content to a file",
94
- parameters: z.object({ path: z.string(), content: z.string() }),
95
- needsApproval: true,
96
- execute: async ({ path, content }) => ({ written: true, path }),
97
- };
98
- ```
99
-
100
- When `needsApproval: true`, the `supervisor.onPermission` callback is invoked before execution. Without a supervisor, approval-required tools are denied by default.
101
-
102
- Runtime-registered tools receive an optional `ToolContext` as their second parameter:
103
-
104
- ```typescript
105
- import type { ToolContext } from "@witqq/agent-sdk";
106
-
107
- const dbTool: ToolDefinition = {
108
- name: "query_db",
109
- description: "Query the database",
110
- parameters: z.object({ sql: z.string() }),
111
- execute: async (params, context?: ToolContext) => {
112
- // context.sessionId — current chat session
113
- // context.custom — session metadata
114
- return db.query(params.sql);
115
- },
116
- };
117
- ```
118
-
119
- ## Permission Handling
120
-
121
- The `supervisor` hooks intercept permission requests and user-facing questions:
122
-
123
- ```typescript
124
- const agent = service.createAgent({
125
- systemPrompt: "File assistant",
126
- tools: [writeFileTool],
127
- supervisor: {
128
- onPermission: async (req, signal) => {
129
- // req.toolName, req.toolArgs, req.suggestedScope
130
- console.log(`${req.toolName} wants to run with`, req.toolArgs);
131
- return {
132
- allowed: true,
133
- scope: "session", // "once" | "session" | "project" | "always"
134
- // modifiedInput: { ... }, // optionally modify args before execution
135
- // reason: "...", // denial reason (if allowed: false)
136
- };
137
- },
138
- onAskUser: async (req, signal) => {
139
- // req.question, req.choices, req.allowFreeform
140
- return { answer: "yes", wasFreeform: false };
141
- },
142
- },
143
- });
144
- ```
145
-
146
- ## Permission Store
147
-
148
- Persist permission decisions across runs so approved tools don't re-prompt:
149
-
150
- ```typescript
151
- import { createDefaultPermissionStore } from "@witqq/agent-sdk";
152
-
153
- const store = createDefaultPermissionStore("./my-project");
154
- const agent = service.createAgent({
155
- systemPrompt: "File assistant",
156
- permissionStore: store,
157
- tools: [writeFileTool],
158
- supervisor: {
159
- onPermission: async (req) => ({ allowed: true, scope: "project" }),
160
- },
161
- });
162
- ```
163
-
164
- Scopes control persistence:
165
- - `"once"` — not stored, one-time approval
166
- - `"session"` — in-memory, cleared on dispose
167
- - `"project"` — persisted to `<projectDir>/.agent-sdk/permissions.json`
168
- - `"always"` — persisted to `~/.agent-sdk/permissions.json`
169
-
170
- Custom stores implement `IPermissionStore`:
171
-
172
- ```typescript
173
- interface IPermissionStore {
174
- isApproved(toolName: string): Promise<boolean>;
175
- approve(toolName: string, scope: PermissionScope): Promise<void>;
176
- revoke(toolName: string): Promise<void>;
177
- clear(): Promise<void>;
178
- dispose(): Promise<void>;
179
- }
180
- ```
181
-
182
- Built-in stores: `InMemoryPermissionStore` (session-scoped), `FilePermissionStore` (persists to JSON file), `CompositePermissionStore` (chains multiple stores — first match wins, writes to the store matching the scope). `createDefaultPermissionStore(projectDir)` returns a `CompositePermissionStore` combining project-level and global `FilePermissionStore` instances.
183
-
184
- ## Structured Output
185
-
186
- Extract typed data from LLM responses using `runStructured`:
187
-
188
- ```typescript
189
- import { z } from "zod";
190
-
191
- const result = await agent.runStructured(
192
- "What is the capital of France?",
193
- {
194
- schema: z.object({
195
- city: z.string(),
196
- country: z.string(),
197
- population: z.number(),
198
- }),
199
- name: "city_info", // optional, helps the LLM
200
- description: "City details", // optional
201
- },
202
- );
203
-
204
- console.log(result.structuredOutput);
205
- // { city: "Paris", country: "France", population: 2161000 }
206
- ```
207
-
208
- The Vercel AI backend uses `generateObject()` for structured output. Copilot and Claude backends extract structured data from the LLM text response.
209
-
210
- ## Streaming Events
211
-
212
- All backends emit the same event types:
213
-
214
- ```typescript
215
- for await (const event of agent.stream("Tell me a story")) {
216
- switch (event.type) {
217
- case "text_delta":
218
- process.stdout.write(event.text);
219
- break;
220
- case "tool_call_start":
221
- console.log(`Calling ${event.toolName}`, event.args);
222
- break;
223
- case "tool_call_end":
224
- console.log(`${event.toolName} returned`, event.result);
225
- break;
226
- case "error":
227
- console.error(event.error, "recoverable:", event.recoverable);
228
- break;
229
- case "done":
230
- console.log("Final:", event.finalOutput);
231
- break;
232
- }
233
- }
234
- ```
235
-
236
- ### Streaming with Conversation History
237
-
238
- Use `streamWithContext` to stream with full conversation history:
239
-
240
- ```typescript
241
- const messages = [
242
- { role: "system" as const, content: "You are helpful." },
243
- { role: "user" as const, content: "Hello" },
244
- { role: "assistant" as const, content: "Hi! How can I help?" },
245
- { role: "user" as const, content: "What is 2+2?" },
246
- ];
247
-
248
- for await (const event of agent.streamWithContext(messages)) {
249
- if (event.type === "text_delta") process.stdout.write(event.text);
250
- }
251
- ```
252
-
253
- | Event | Fields | Description |
254
- |-------|--------|-------------|
255
- | `text_delta` | `text` | Incremental text output |
256
- | `thinking_delta` | `text` | Incremental reasoning/thinking text |
257
- | `thinking_start` | — | Model started reasoning |
258
- | `thinking_end` | — | Model finished reasoning |
259
- | `tool_call_start` | `toolCallId`, `toolName`, `args` | Tool invocation began |
260
- | `tool_call_end` | `toolCallId`, `toolName`, `result` | Tool invocation completed |
261
- | `permission_request` | `request` | Permission check initiated |
262
- | `permission_response` | `toolName`, `decision` | Permission decision made |
263
- | `ask_user` | `request` | User input requested |
264
- | `ask_user_response` | `answer` | User response received |
265
- | `session_info` | `sessionId`, `transcriptPath?`, `backend` | CLI session metadata (streaming only) |
266
- | `usage_update` | `promptTokens`, `completionTokens`, `model?`, `backend?` | Token usage with metadata |
267
- | `heartbeat` | — | Keepalive signal during long operations |
268
- | `error` | `error`, `recoverable` | Error during execution |
269
- | `done` | `finalOutput`, `structuredOutput?` | Execution completed |
270
-
271
- ## Usage Tracking
272
-
273
- Track token usage with the `onUsage` callback. Called after each `run()`/`runWithContext()`/`runStructured()` completion and during `stream()`/`streamWithContext()` when usage data arrives:
274
-
275
- ```typescript
276
- const agent = service.createAgent({
277
- systemPrompt: "You are a helpful assistant.",
278
- onUsage: (usage) => {
279
- console.log(`${usage.backend}/${usage.model}: ${usage.promptTokens}+${usage.completionTokens} tokens`);
280
- },
281
- });
282
- ```
283
-
284
- Usage data includes `promptTokens`, `completionTokens`, and optional `model` and `backend` fields. Callback errors are logged but not propagated (fire-and-forget).
285
-
286
- ## Heartbeat
287
-
288
- Keep HTTP streams alive during long tool executions by emitting periodic heartbeat events:
289
-
290
- ```typescript
291
- const agent = service.createAgent({
292
- systemPrompt: "You are a helpful assistant.",
293
- heartbeatInterval: 15000, // emit heartbeat every 15s during gaps
294
- });
295
-
296
- for await (const event of agent.stream("Run a long analysis")) {
297
- if (event.type === "heartbeat") continue; // ignore keepalive
298
- // handle other events...
299
- }
300
- ```
301
-
302
- 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.
303
-
304
- ## Persistent Sessions (CLI Backends)
305
-
306
- CLI backends (Copilot, Claude) create a fresh subprocess session per `run()`/`stream()` call by default. Set `sessionMode: "persistent"` to reuse the same CLI session across calls — the CLI backend maintains conversation history natively:
307
-
308
- ```typescript
309
- const agent = service.createAgent({
310
- systemPrompt: "You are a helpful assistant.",
311
- sessionMode: "persistent", // reuse CLI session across calls
312
- });
313
-
314
- await agent.run("My name is Alice");
315
- const result = await agent.run("What is my name?");
316
- // result.output contains "Alice" — history maintained by CLI
317
-
318
- console.log(agent.sessionId); // CLI session ID for external tracking
319
- agent.dispose(); // destroys the persistent session
320
- ```
321
-
322
- In persistent mode, if a session encounters an error, it is automatically cleared and recreated on the next call. The `sessionId` property exposes the CLI session ID for logging or external storage.
323
-
324
- ### Interrupting Running Operations
325
-
326
- Call `interrupt()` to gracefully stop a running operation. For CLI backends, this calls the SDK's interrupt/abort method on the active session:
327
-
328
- ```typescript
329
- // In another context (e.g., timeout handler)
330
- await agent.interrupt();
331
- ```
332
-
333
- Default (`"per-call"`): each call creates and destroys a fresh session. Multi-message context is passed via prompt augmentation through `runWithContext()`/`streamWithContext()`.
334
-
335
- API-based backends (Vercel AI) ignore `sessionMode` — they are stateless by design.
336
-
337
- ## Backend-Specific Options
338
-
339
- ### Copilot
340
-
341
- ```typescript
342
- import { createCopilotService } from "@witqq/agent-sdk/copilot";
343
-
344
- const service = createCopilotService({
345
- useLoggedInUser: true, // use GitHub CLI auth
346
- cliPath: "/path/to/copilot", // optional custom CLI path
347
- workingDirectory: process.cwd(),
348
- githubToken: "ghp_...", // optional, alternative to useLoggedInUser
349
- cliArgs: ["--allow-all"], // extra CLI flags for the subprocess
350
- env: { PATH: "/custom/bin" }, // custom env vars for subprocess
351
- });
352
- ```
353
-
354
- **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.
355
-
356
- **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.
357
-
358
- **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).
359
-
360
- **Available tools filter:** Use `availableTools` in `AgentConfig` to restrict which Copilot built-in tools are available:
361
-
362
- ```typescript
363
- const agent = service.createAgent({
364
- systemPrompt: "Research assistant",
365
- tools: [],
366
- availableTools: ["web_search", "web_fetch"], // only these built-in tools
367
- });
368
- ```
369
-
370
- ### Claude
371
-
372
- ```typescript
373
- import { createClaudeService } from "@witqq/agent-sdk/claude";
374
-
375
- const service = createClaudeService({
376
- cliPath: "/path/to/claude", // optional custom CLI path
377
- workingDirectory: process.cwd(),
378
- maxTurns: 10,
379
- env: { CLAUDE_CONFIG_DIR: "/custom/config" }, // custom env vars for subprocess
380
- });
381
- ```
382
-
383
- `supervisor.onAskUser` is not supported by the Claude backend; a warning is emitted if set.
384
-
385
- When `supervisor.onPermission` is set, the Claude backend automatically sets `permissionMode: "default"` so the CLI invokes the callback instead of using built-in rules.
386
-
387
- ### Vercel AI (OpenRouter / OpenAI-compatible)
388
-
389
- ```typescript
390
- import { createVercelAIService } from "@witqq/agent-sdk/vercel-ai";
391
-
392
- const service = createVercelAIService({
393
- apiKey: process.env.OPENROUTER_API_KEY!,
394
- baseUrl: "https://openrouter.ai/api/v1", // default
395
- provider: "openrouter", // default
396
- });
397
-
398
- const agent = service.createAgent({
399
- model: "anthropic/claude-sonnet-4-5",
400
- systemPrompt: "You are a helpful assistant.",
401
- tools: [searchTool],
402
- });
403
- ```
404
-
405
- Uses `generateText()` for runs, `generateObject()` for structured output, `streamText()` for streaming. Supports `supervisor.onAskUser` via an injected `ask_user` tool.
406
-
407
- Pass model-specific options via `providerOptions`:
408
-
409
- ```typescript
410
- const agent = service.createAgent({
411
- model: "google/gemini-2.0-flash",
412
- systemPrompt: "Think step by step.",
413
- providerOptions: {
414
- google: { thinkingConfig: { thinkingBudget: 1024 } },
415
- },
416
- });
417
- ```
418
-
419
- ## Switching Backends
420
-
421
- All backends share the same `AgentConfig` and return the same `AgentResult`. To switch backends, change only the service creation:
422
-
423
- ```typescript
424
- import { createAgentService } from "@witqq/agent-sdk";
425
- import { z } from "zod";
426
-
427
- const tools = [
428
- {
429
- name: "greet",
430
- description: "Greet a user",
431
- parameters: z.object({ name: z.string() }),
432
- execute: async ({ name }) => ({ message: `Hello, ${name}!` }),
433
- },
434
- ];
435
-
436
- const config = {
437
- systemPrompt: "You are a helpful assistant.",
438
- tools,
439
- };
440
-
441
- // Switch backend by changing the first argument:
442
- const service = await createAgentService("copilot", { useLoggedInUser: true });
443
- // const service = await createAgentService("claude", { workingDirectory: "." });
444
- // const service = await createAgentService("vercel-ai", { apiKey: "..." });
445
-
446
- const agent = service.createAgent(config);
447
- const result = await agent.run("Greet Alice");
448
- ```
449
-
450
- Or use direct backend imports to avoid lazy loading:
451
-
452
- ```typescript
453
- import { createCopilotService } from "@witqq/agent-sdk/copilot";
454
- import { createClaudeService } from "@witqq/agent-sdk/claude";
455
- import { createVercelAIService } from "@witqq/agent-sdk/vercel-ai";
456
- ```
457
-
458
- ## Model Names
459
-
460
- `AgentConfig.model` accepts both full model IDs and short names:
461
-
462
- | Backend | Full ID example | Short name |
463
- |---|---|---|
464
- | Copilot | `gpt-4o` | (same) |
465
- | Claude | `claude-sonnet-4-5-20250514` | `sonnet` |
466
- | Vercel AI | `anthropic/claude-sonnet-4-5` | (provider-specific) |
467
-
468
- Use `service.listModels()` to get available model IDs for each backend. Copilot lists models from GitHub API. Claude queries the Anthropic `/v1/models` endpoint when `oauthToken` is provided (returns empty list without token). Vercel AI queries the provider's `/models` endpoint (returns empty list on failure).
469
-
470
- ## Build
471
-
472
- ```bash
473
- npm run build # tsup → ESM + CJS
474
- npm run test # vitest
475
- npm run typecheck # tsc --noEmit
476
- ```
477
-
478
- ## Authentication
479
-
480
- Programmatic OAuth flows for obtaining tokens without manual terminal interaction.
481
-
482
- ```typescript
483
- import { CopilotAuth, ClaudeAuth } from "@witqq/agent-sdk/auth";
484
- ```
485
-
486
- ### Copilot (GitHub Device Flow)
487
-
488
- ```typescript
489
- const auth = new CopilotAuth();
490
- const { verificationUrl, userCode, waitForToken } = await auth.startDeviceFlow();
491
-
492
- // Show the user: open verificationUrl and enter userCode
493
- console.log(`Open ${verificationUrl} and enter code: ${userCode}`);
494
-
495
- const token = await waitForToken(); // polls until authorized
496
- // token.accessToken = "gho_..." (long-lived, no expiration)
497
-
498
- // Use with Copilot backend:
499
- const service = createCopilotService({ githubToken: token.accessToken });
500
- ```
501
-
502
- ### Claude (OAuth + PKCE)
503
-
504
- ```typescript
505
- const auth = new ClaudeAuth();
506
- const { authorizeUrl, completeAuth } = auth.startOAuthFlow();
507
-
508
- // Open authorizeUrl in browser — user authorizes, gets redirected
509
- // completeAuth accepts raw code, full redirect URL, or code#state format
510
- console.log(`Open: ${authorizeUrl}`);
511
-
512
- const token = await completeAuth(codeOrUrl);
513
- // token.accessToken = "sk-ant-oat01-..." (expires in 8h, has refreshToken)
514
-
515
- // Refresh before expiry:
516
- const refreshed = await auth.refreshToken(token.refreshToken);
517
-
518
- // Use with Claude backend:
519
- const service = createClaudeService({ oauthToken: token.accessToken });
520
- ```
521
-
522
- ### Token Types
523
-
524
- ```typescript
525
- interface AuthToken {
526
- accessToken: string;
527
- tokenType: string;
528
- expiresIn?: number; // seconds until expiry (undefined = long-lived)
529
- obtainedAt: number; // Date.now() when token was obtained
530
- }
531
-
532
- interface ClaudeAuthToken extends AuthToken {
533
- refreshToken: string; // for refreshing expired tokens
534
- scopes: string[];
535
- }
536
-
537
- interface CopilotAuthToken extends AuthToken {
538
- login?: string; // GitHub username
539
- }
540
- ```
541
-
542
- ### Token Auto-Refresh
543
-
544
- `TokenRefreshManager` schedules background token refresh before expiry:
545
-
546
- ```typescript
547
- import { TokenRefreshManager } from "@witqq/agent-sdk/auth";
548
-
549
- const manager = new TokenRefreshManager({
550
- token: authToken,
551
- refreshFn: async (token) => claudeAuth.refreshToken(token.refreshToken!),
552
- refreshThreshold: 0.8, // refresh at 80% of token lifetime
553
- });
554
-
555
- manager.on("refreshed", (newToken) => { /* update stored token */ });
556
- manager.on("expired", () => { /* re-authenticate */ });
557
- manager.start();
558
- ```
559
-
560
- ## Chat SDK (experimental)
561
-
562
- Higher-level primitives for building AI chat applications on top of agent-sdk.
563
-
564
- ### Composable Architecture
565
-
566
- The SDK is layered — use only what you need:
567
-
568
- **Standalone agent** (no server, no UI):
569
-
570
- ```typescript
571
- import { createAgentService } from "@witqq/agent-sdk";
572
- const service = await createAgentService("copilot", { useLoggedInUser: true });
573
- const agent = service.createAgent({ systemPrompt: "You are helpful." });
574
- const result = await agent.run("Hello");
575
- ```
576
-
577
- **Server with runtime** (add HTTP layer):
578
-
579
- ```typescript
580
- import * as http from "node:http";
581
- import { createAgentService } from "@witqq/agent-sdk";
582
- import type { AuthToken } from "@witqq/agent-sdk/auth";
583
- import { CopilotAuth } from "@witqq/agent-sdk/auth";
584
- import { CopilotChatAdapter } from "@witqq/agent-sdk/chat/backends";
585
- import { createChatRuntime } from "@witqq/agent-sdk/chat/runtime";
586
- import { createChatServer } from "@witqq/agent-sdk/chat/server";
587
- import { createSQLiteStorage } from "@witqq/agent-sdk/chat/sqlite";
588
-
589
- const { sessionStore, providerStore, tokenStore } = createSQLiteStorage("chat.db");
590
-
591
- const runtime = createChatRuntime({
592
- backends: {
593
- copilot: async (credentials: AuthToken) => {
594
- const svc = await createAgentService("copilot", { githubToken: credentials.accessToken });
595
- return new CopilotChatAdapter({ agentConfig: { systemPrompt: "Hello" }, agentService: svc });
596
- },
597
- },
598
- defaultBackend: "copilot", sessionStore,
599
- });
600
-
601
- const handler = createChatServer({
602
- runtime,
603
- auth: { tokenStore, createCopilotAuth: () => new CopilotAuth() },
604
- providers: { providerStore },
605
- });
606
- http.createServer(handler).listen(3000);
607
- ```
608
-
609
- **Full-stack with React** (add frontend):
610
-
611
- ```typescript
612
- // frontend — 4 lines
613
- import { ChatUI, RemoteChatClient } from "@witqq/agent-sdk/chat/react";
614
-
615
- const runtime = new RemoteChatClient({ baseUrl: "/api/chat" });
616
- <ChatUI runtime={runtime} authBaseUrl="/api" />
617
- ```
618
-
619
- ### Barrel Import
620
-
621
- For most consumer apps, import common types from the barrel:
622
-
623
- ```typescript
624
- // Core types and runtime (barrel export)
625
- import {
626
- ChatMessage, ChatSession, ChatEvent, IChatRuntime,
627
- createChatRuntime, ChatError, classifyError,
628
- MessageAccumulator, SSEChatTransport,
629
- } from "@witqq/agent-sdk/chat";
630
-
631
- // React hooks and components (separate import — not in barrel)
632
- import {
633
- useChat, useRemoteChat, useRemoteAuth,
634
- ChatProvider, Thread, Composer,
635
- } from "@witqq/agent-sdk/chat/react";
636
- ```
637
-
638
- ### Individual Module Imports
639
-
640
- ```typescript
641
- import { ChatMessage, ChatSession, isChatMessage } from "@witqq/agent-sdk/chat/core";
642
- import type { IChatBackend } from "@witqq/agent-sdk/chat/backends";
643
- import {
644
- classifyError, withRetry, isRetryable,
645
- ChatError, ErrorCode,
646
- ExponentialBackoffStrategy
647
- } from "@witqq/agent-sdk/chat/errors";
648
- import { ChatEventBus } from "@witqq/agent-sdk/chat/events";
649
- import { filterEvents, collectText } from "@witqq/agent-sdk/chat/events";
650
- import {
651
- InMemoryStorage, FileStorage,
652
- type IStorageAdapter, StorageError
653
- } from "@witqq/agent-sdk/chat/storage";
654
- import {
655
- InMemorySessionStore, FileSessionStore,
656
- type IChatSessionStore
657
- } from "@witqq/agent-sdk/chat/sessions";
658
- import {
659
- ContextWindowManager, estimateTokens
660
- } from "@witqq/agent-sdk/chat/context";
661
- import {
662
- CopilotChatAdapter, VercelAIChatAdapter, BaseBackendAdapter,
663
- SSEChatTransport, WsChatTransport, InProcessChatTransport,
664
- streamToTransport, withInterceptors,
665
- type IResumableBackend, type BackendAdapterOptions, type IChatTransport
666
- } from "@witqq/agent-sdk/chat/backends";
667
- ```
668
-
669
- ### Error Classification
670
-
671
- ```typescript
672
- try {
673
- await provider.send(message);
674
- } catch (err) {
675
- const classified = classifyError(err);
676
- if (classified.code === ErrorCode.RATE_LIMIT) {
677
- console.log(`Rate limited, retry after ${classified.retryAfter}ms`);
678
- }
679
- }
680
- ```
681
-
682
- ### Retry with Backoff
683
-
684
- ```typescript
685
- const result = await withRetry(
686
- () => provider.send(message),
687
- new ExponentialBackoffStrategy({ maxAttempts: 3 }),
688
- { signal: AbortSignal.timeout(30_000) },
689
- );
690
- ```
691
-
692
- ### Event Bus with Middleware
693
-
694
- ```typescript
695
- const bus = new ChatEventBus();
696
-
697
- // Logging middleware
698
- bus.use((ctx) => {
699
- console.log(`[${ctx.event.type}]`);
700
- ctx.next();
701
- });
702
-
703
- // Filter out heartbeat events
704
- bus.use((ctx) => {
705
- if (ctx.event.type === "heartbeat") ctx.suppress();
706
- else ctx.next();
707
- });
708
-
709
- bus.on("message:delta", (event) => console.log(event.text));
710
- ```
711
-
712
- ### Storage Adapters
713
-
714
- ```typescript
715
- // In-memory (dev/testing)
716
- const mem = new InMemoryStorage<ChatSession>();
717
- await mem.create("s1", session);
718
- const s = await mem.get("s1"); // deep copy, mutation-safe
719
-
720
- // File-based (persistence)
721
- const fs = new FileStorage<ChatSession>({ directory: "./data/sessions" });
722
- await fs.create("s1", session);
723
- const items = await fs.query({
724
- filter: (s) => s.metadata.tags.includes("important"),
725
- sort: (a, b) => b.updatedAt - a.updatedAt,
726
- limit: 10,
727
- });
728
- ```
729
-
730
- ### Session Store
731
-
732
- ```typescript
733
- const store = new InMemorySessionStore();
734
- // or: new FileSessionStore({ directory: "./data/sessions" })
735
-
736
- const session = await store.createSession({
737
- config: { model: "gpt-4", backend: "vercel-ai" },
738
- title: "Code Review",
739
- tags: ["work"],
740
- });
741
-
742
- await store.appendMessage(session.id, message);
743
- const page = await store.loadMessages(session.id, { limit: 20, offset: 0 });
744
- // page.messages, page.total, page.hasMore
745
-
746
- const results = await store.searchSessions({ query: "typescript" });
747
- ```
748
-
749
- ### Context Window Manager
750
-
751
- ```typescript
752
- const manager = new ContextWindowManager({
753
- maxTokens: 4096,
754
- reservedTokens: 500,
755
- strategy: "truncate-oldest", // or "sliding-window", "summarize-placeholder"
756
- });
757
-
758
- const result = manager.fitMessages(messages);
759
- // result.messages — trimmed to fit budget
760
- // result.wasTruncated — whether messages were removed
761
- // result.totalTokens — estimated token usage
762
- // result.removedCount — how many messages were dropped
763
-
764
- // Async variant with optional summarizer (summarize-placeholder strategy)
765
- const asyncManager = new ContextWindowManager({
766
- maxTokens: 4096,
767
- strategy: "summarize-placeholder",
768
- summarizer: async (removed) => {
769
- // Call LLM or custom logic to summarize removed messages
770
- return `Summary of ${removed.length} messages: ...`;
771
- },
772
- });
773
- const asyncResult = await asyncManager.fitMessagesAsync(messages);
774
-
775
- // Per-message estimation
776
- const tokens = estimateTokens(message); // ~chars/4
777
- ```
778
-
779
- ### Backend Adapters
780
-
781
- Backend adapters bridge `IAgentService` to `IChatBackend`, adding session management and resume support:
782
-
783
- ```typescript
784
- import { CopilotChatAdapter } from "@witqq/agent-sdk/chat/backends";
785
-
786
- const adapter = new CopilotChatAdapter({
787
- agentConfig: {
788
- systemPrompt: "You are a helpful assistant.",
789
- model: "gpt-4.1",
790
- },
791
- });
792
-
793
- // Stream a message (creates persistent session automatically)
794
- for await (const event of adapter.streamMessage(session, "Hello")) {
795
- // ChatEvent: text_delta, message_start, message_complete, tool_call_start, etc.
796
- }
797
-
798
- // Resume a previous session
799
- if (adapter.canResume()) {
800
- for await (const event of adapter.resume(session, adapter.backendSessionId!)) {
801
- // Continues the existing conversation
802
- }
803
- }
804
-
805
- adapter.dispose();
806
- ```
807
-
808
- `IResumableBackend` extends `IChatBackend` with `canResume()`, `resume()`, and `backendSessionId`. Built-in adapters: `CopilotChatAdapter`, `ClaudeChatAdapter`, `VercelAIChatAdapter` (stateless, no resume). Create custom adapters by extending `BaseBackendAdapter`.
809
-
810
- Service ownership: when `agentService` is passed via options, the adapter does **not** dispose it — the caller retains ownership. When omitted, the adapter creates and owns its service internally.
811
-
812
- ### Chat Transport
813
-
814
- `IChatTransport` abstracts event delivery to clients. Three built-in implementations:
815
-
816
- | Transport | Use case |
817
- |---|---|
818
- | `SSEChatTransport` | Server-Sent Events over HTTP |
819
- | `WsChatTransport` | WebSocket via `WebSocketLike` abstraction |
820
- | `InProcessChatTransport` | Zero-network async iterable for testing/embedded |
821
-
822
- `streamToTransport()` pipes adapter events to any transport:
823
-
824
- ```typescript
825
- import { SSEChatTransport, WsChatTransport, streamToTransport } from "@witqq/agent-sdk/chat/backends";
826
-
827
- const transport = new SSEChatTransport(res);
828
- await streamToTransport(adapter.streamMessage(session, message), transport);
829
- ```
830
-
831
- **Interceptors** wrap any transport with composable hooks (logging, metrics, rate limiting):
832
-
833
- ```typescript
834
- import { withInterceptors, type TransportInterceptor } from "@witqq/agent-sdk/chat/backends";
835
-
836
- const logger: TransportInterceptor = {
837
- beforeSend(event) { console.log("send:", event.type); return event; },
838
- onError(err) { console.error(err); },
839
- };
840
- const wrapped = withInterceptors(transport, [logger]);
841
- ```
842
-
843
- **Stream watchdog** — set `streamTimeoutMs` in runtime options to abort hanging streams:
844
-
845
- ```typescript
846
- const runtime = createChatRuntime({
847
- streamTimeoutMs: 30_000, // abort after 30s of inactivity
848
- // ...
849
- });
850
- ```
851
-
852
- See [Custom Transports](docs/chat-sdk/custom-transports.md) for the implementation guide.
853
-
854
- ### Chat Runtime
855
-
856
- `IChatRuntime<TMetadata>` is the unified facade that orchestrates backend adapters, sessions, context trimming, streaming, and middleware. `createChatRuntime()` builds one from a config:
857
-
858
- ```typescript
859
- import { createChatRuntime } from "@witqq/agent-sdk/chat/runtime";
860
-
861
- const runtime = createChatRuntime({
862
- backends: {
863
- copilot: async (credentials) => new CopilotChatAdapter({
864
- agentConfig: { systemPrompt: "Hello" },
865
- agentService: await createAgentService("copilot", { githubToken: credentials.accessToken }),
866
- }),
867
- },
868
- defaultBackend: "copilot",
869
- sessionStore: new InMemorySessionStore(),
870
- context: { maxTokens: 8000 },
871
- });
872
-
873
- // Create session, send message, stream events
874
- const session = await runtime.createSession();
875
- for await (const event of runtime.send(session.id, "Hello")) {
876
- console.log(event.type, event);
877
- }
878
- ```
879
-
880
- Key capabilities: session delegation (create/get/list/delete), tool registration via `registerTool(def)` / `removeTool(name)` / `registeredTools` (readonly Map, persists across switches), middleware pipeline (`use(middleware)`), state machine (`status` property), abort support (`abort()`), pre-stream retry with `StreamRetryConfig`, session lifecycle events via `onSessionChange(callback)`, generic `<TMetadata>` for typed session metadata, context stats via `getContextStats(sessionId)`, and `dispose()`. Model and backend are passed per-call via `send(sessionId, msg, { model, backend, credentials })`.
881
-
882
- Context monitoring:
883
-
884
- ```typescript
885
- // Query context usage after send
886
- const stats = runtime.getContextStats(session.id);
887
- // stats: { totalTokens, removedCount, wasTruncated, availableBudget, realPromptTokens?, realCompletionTokens?, modelContextWindow? } | null
888
-
889
- // Handle trimmed messages via callback
890
- const runtime = createChatRuntime({
891
- // ...backends, sessionStore, context
892
- onContextTrimmed: (sessionId, removedMessages) => {
893
- db.saveRemovedMessages(sessionId, removedMessages);
894
- },
895
- });
896
- ```
897
-
898
- ### Server Utilities
899
-
900
- Framework-agnostic HTTP handlers for serving `IChatRuntime` over HTTP. Import from `@witqq/agent-sdk/chat/server`.
901
-
902
- ```typescript
903
- import {
904
- createChatHandler,
905
- createAuthHandler,
906
- FileTokenStore,
907
- corsMiddleware,
908
- createChatServer,
909
- } from "@witqq/agent-sdk/chat/server";
910
- import { createChatRuntime } from "@witqq/agent-sdk/chat/runtime";
911
-
912
- const runtime = createChatRuntime({ /* ... */ });
913
-
914
- // Option 1: Compose handlers manually
915
- const chatHandler = createChatHandler(runtime, { prefix: "/api/chat" });
916
- const authHandler = createAuthHandler({
917
- tokenStore: new FileTokenStore({ directory: "./tokens" }),
918
- onAuth: (backend, token) => { /* handle auth */ },
919
- });
920
-
921
- // Option 2: One-call server factory
922
- const handler = createChatServer({
923
- runtime,
924
- cors: true,
925
- staticDir: "./public",
926
- });
927
- ```
928
-
929
- `createChatHandler` maps all 10 `RemoteChatClient` endpoints (session CRUD, send via SSE, abort, models, backend/model switch). `createAuthHandler` handles Copilot Device Flow, Claude OAuth+PKCE, and API key auth with persistent token storage via `ITokenStore`. `corsMiddleware` supports multi-origin configuration.
930
-
931
- ## Interactive Demo
932
-
933
- Complete chat app showcasing the full SDK.
934
-
935
- ```bash
936
- npm run demo # Build & start in Docker (http://localhost:3456)
937
- npm run demo -- stop # Stop
938
- npm run demo -- logs # Follow logs
939
- npm run demo -- restart # Rebuild & restart
940
- npm run demo -- dev # Local dev without Docker
941
- ```
942
-
943
- Features: multi-backend auth (Copilot Device Flow, Claude OAuth+PKCE, Vercel AI API key), provider management, model selection, SSE streaming with thinking blocks, tool calls with approval, token usage display, error handling, session management, SQLite persistence.
944
-
945
- Server uses `createChatServer` for zero custom routing with stateless backend factories (credentials per-request). Frontend uses `ChatUI` for zero custom components. See [demo README](examples/demo/README.md) for details.
946
-
947
- ## React Bindings
948
-
949
- Headless React hooks and components for building chat UIs:
950
-
951
- ```typescript
952
- import { useChat, Thread, Composer, ChatProvider } from "@witqq/agent-sdk/chat/react";
953
-
954
- function App() {
955
- return (
956
- <ChatProvider runtime={runtime}>
957
- <Thread />
958
- <Composer />
959
- </ChatProvider>
960
- );
961
- }
962
- ```
963
-
964
- For client-server architectures, `useRemoteChat` manages the full auth → runtime → session lifecycle:
965
-
966
- ```typescript
967
- import { useRemoteChat, ChatProvider, Thread, Composer } from "@witqq/agent-sdk/chat/react";
968
-
969
- function App() {
970
- const chat = useRemoteChat({
971
- chatBaseUrl: "/api/chat",
972
- authBaseUrl: "/api",
973
- backend: "copilot",
974
- });
975
-
976
- if (chat.phase !== "ready" || !chat.runtime) return <div>Loading...</div>;
977
-
978
- return (
979
- <ChatProvider runtime={chat.runtime}>
980
- <Thread />
981
- <Composer />
982
- </ChatProvider>
983
- );
984
- }
985
- ```
986
-
987
- Or use `RemoteChatClient` directly for lower-level control:
988
-
989
- ```typescript
990
- import { RemoteChatClient } from "@witqq/agent-sdk/chat/react";
991
-
992
- const runtime = new RemoteChatClient({ baseUrl: "/api/chat" });
993
- ```
994
-
995
- Reactive session list (replaces manual polling):
996
-
997
- ```typescript
998
- import { useSessions } from "@witqq/agent-sdk/chat/react";
999
-
1000
- function SessionList() {
1001
- const { sessions, loading } = useSessions();
1002
- // Auto-updates on create, delete, and message send
1003
- return sessions.map(s => <div key={s.id}>{s.title}</div>);
1004
- }
1005
- ```
1006
-
1007
- Server-delegated authentication (no `node:crypto` in browser):
1008
-
1009
- ```typescript
1010
- import { useRemoteAuth } from "@witqq/agent-sdk/chat/react";
1011
-
1012
- const auth = useRemoteAuth({ backend: "copilot", baseUrl: "/api" });
1013
- // auth.startDeviceFlow(), auth.startOAuthFlow(), auth.submitApiKey()
1014
- ```
1015
-
1016
- `ContextStatsDisplay` renders context window usage:
1017
-
1018
- ```typescript
1019
- import { ContextStatsDisplay } from "@witqq/agent-sdk/chat/react";
1020
-
1021
- // Headless component rendering context window stats
1022
- // Props: { stats: ContextStats | null }
1023
- // Data attributes: data-context-stats, data-context-tokens, data-context-budget,
1024
- // data-context-usage, data-context-removed, data-context-truncated
1025
- <ContextStatsDisplay stats={runtime.getContextStats(sessionId)} />
1026
- ```
1027
-
1028
- `ThreadList` supports search:
1029
-
1030
- ```typescript
1031
- <ThreadList
1032
- sessions={sessions}
1033
- onSelect={handleSelect}
1034
- onDelete={handleDelete}
1035
- searchQuery={query} // controlled search input
1036
- onSearchChange={setQuery} // search input change handler
1037
- />
1038
- ```
1039
-
1040
- See [Chat SDK docs](docs/chat-sdk/README.md) for the full React API reference.
1041
-
1042
- ## Documentation
1043
-
1044
- | Document | Description |
1045
- |----------|-------------|
1046
- | [Chat SDK Modules](docs/chat-sdk/README.md) | Module-by-module API docs for chat primitives |
1047
- | [Custom Transports](docs/chat-sdk/custom-transports.md) | Guide to building custom IChatTransport implementations |
1048
- | [Custom Renderers](docs/chat-sdk/custom-renderers.md) | Three approaches to customizing React UI components |
1049
- | [Demo App](examples/demo/README.md) | Full-stack demo with architecture and API reference |
1050
- | [Changelog](CHANGELOG.md) | Release history and breaking changes |
1051
-
1052
- ## License
1053
-
1054
- MIT