@witqq/agent-sdk 0.1.1 → 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 CHANGED
@@ -196,21 +196,73 @@ for await (const event of agent.stream("Tell me a story")) {
196
196
  }
197
197
  ```
198
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
+
199
216
  | Event | Fields | Description |
200
217
  |-------|--------|-------------|
201
218
  | `text_delta` | `text` | Incremental text output |
219
+ | `thinking_delta` | `text` | Incremental reasoning/thinking text |
202
220
  | `thinking_start` | — | Model started reasoning |
203
221
  | `thinking_end` | — | Model finished reasoning |
204
- | `tool_call_start` | `toolName`, `args` | Tool invocation began |
205
- | `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 |
206
224
  | `permission_request` | `request` | Permission check initiated |
207
225
  | `permission_response` | `toolName`, `decision` | Permission decision made |
208
226
  | `ask_user` | `request` | User input requested |
209
227
  | `ask_user_response` | `answer` | User response received |
210
- | `usage_update` | `promptTokens`, `completionTokens` | Token usage |
228
+ | `usage_update` | `promptTokens`, `completionTokens`, `model?`, `backend?` | Token usage with metadata |
229
+ | `heartbeat` | — | Keepalive signal during long operations |
211
230
  | `error` | `error`, `recoverable` | Error during execution |
212
231
  | `done` | `finalOutput`, `structuredOutput?` | Execution completed |
213
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
+
214
266
  ## Backend-Specific Options
215
267
 
216
268
  ### Copilot
@@ -54,7 +54,9 @@ var BaseAgent = class {
54
54
  this.state = "running";
55
55
  try {
56
56
  const messages = [{ role: "user", content: prompt }];
57
- return await this.executeRun(messages, options, ac.signal);
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
- return await this.executeRun(messages, options, ac.signal);
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
- return await this.executeRunStructured(
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
- yield* this.executeStream(messages, options, ac.signal);
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
- function mapSDKMessage(msg, thinkingBlockIndices) {
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
- toolName: block.name ?? "unknown",
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
- return toolName ? { type: "tool_call_start", toolName, args: {} } : null;
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;