@witqq/agent-sdk 0.1.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.
@@ -0,0 +1,615 @@
1
+ // src/types.ts
2
+ function getTextContent(content) {
3
+ if (typeof content === "string") return content;
4
+ return content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
5
+ }
6
+
7
+ // src/errors.ts
8
+ var AgentSDKError = class extends Error {
9
+ constructor(message, options) {
10
+ super(message, options);
11
+ this.name = "AgentSDKError";
12
+ }
13
+ };
14
+ var ReentrancyError = class extends AgentSDKError {
15
+ constructor() {
16
+ super("Agent is already running. Await the current run before starting another.");
17
+ this.name = "ReentrancyError";
18
+ }
19
+ };
20
+ var DisposedError = class extends AgentSDKError {
21
+ constructor(entity) {
22
+ super(`${entity} has been disposed and cannot be used.`);
23
+ this.name = "DisposedError";
24
+ }
25
+ };
26
+ var DependencyError = class extends AgentSDKError {
27
+ packageName;
28
+ constructor(packageName) {
29
+ super(`${packageName} is not installed. Install it: npm install ${packageName}`);
30
+ this.name = "DependencyError";
31
+ this.packageName = packageName;
32
+ }
33
+ };
34
+ var AbortError = class extends AgentSDKError {
35
+ constructor() {
36
+ super("Agent run was aborted.");
37
+ this.name = "AbortError";
38
+ }
39
+ };
40
+ var ToolExecutionError = class extends AgentSDKError {
41
+ toolName;
42
+ constructor(toolName, message, options) {
43
+ super(`Tool "${toolName}" failed: ${message}`, options);
44
+ this.name = "ToolExecutionError";
45
+ this.toolName = toolName;
46
+ }
47
+ };
48
+
49
+ // src/base-agent.ts
50
+ var BaseAgent = class {
51
+ state = "idle";
52
+ abortController = null;
53
+ config;
54
+ constructor(config) {
55
+ this.config = Object.freeze({ ...config });
56
+ }
57
+ // ─── Public Interface ─────────────────────────────────────────
58
+ async run(prompt, options) {
59
+ this.guardReentrancy();
60
+ this.guardDisposed();
61
+ const ac = this.createAbortController(options?.signal);
62
+ this.state = "running";
63
+ try {
64
+ const messages = [{ role: "user", content: prompt }];
65
+ return await this.executeRun(messages, options, ac.signal);
66
+ } finally {
67
+ this.state = "idle";
68
+ this.abortController = null;
69
+ }
70
+ }
71
+ async runWithContext(messages, options) {
72
+ this.guardReentrancy();
73
+ this.guardDisposed();
74
+ const ac = this.createAbortController(options?.signal);
75
+ this.state = "running";
76
+ try {
77
+ return await this.executeRun(messages, options, ac.signal);
78
+ } finally {
79
+ this.state = "idle";
80
+ this.abortController = null;
81
+ }
82
+ }
83
+ async runStructured(prompt, schema, options) {
84
+ this.guardReentrancy();
85
+ this.guardDisposed();
86
+ const ac = this.createAbortController(options?.signal);
87
+ this.state = "running";
88
+ try {
89
+ const messages = [{ role: "user", content: prompt }];
90
+ return await this.executeRunStructured(
91
+ messages,
92
+ schema,
93
+ options,
94
+ ac.signal
95
+ );
96
+ } finally {
97
+ this.state = "idle";
98
+ this.abortController = null;
99
+ }
100
+ }
101
+ async *stream(prompt, options) {
102
+ this.guardReentrancy();
103
+ this.guardDisposed();
104
+ const ac = this.createAbortController(options?.signal);
105
+ this.state = "streaming";
106
+ try {
107
+ const messages = [{ role: "user", content: prompt }];
108
+ yield* this.executeStream(messages, options, ac.signal);
109
+ } finally {
110
+ this.state = "idle";
111
+ this.abortController = null;
112
+ }
113
+ }
114
+ abort() {
115
+ if (this.abortController) {
116
+ this.abortController.abort();
117
+ }
118
+ }
119
+ getState() {
120
+ return this.state;
121
+ }
122
+ getConfig() {
123
+ return this.config;
124
+ }
125
+ /** Mark agent as disposed. Override to add cleanup. */
126
+ dispose() {
127
+ this.abort();
128
+ this.state = "disposed";
129
+ }
130
+ // ─── Guards ───────────────────────────────────────────────────
131
+ guardReentrancy() {
132
+ if (this.state === "running" || this.state === "streaming") {
133
+ throw new ReentrancyError();
134
+ }
135
+ }
136
+ guardDisposed() {
137
+ if (this.state === "disposed") {
138
+ throw new DisposedError("Agent");
139
+ }
140
+ }
141
+ /** Throw AbortError if signal is already aborted */
142
+ checkAbort(signal) {
143
+ if (signal.aborted) {
144
+ throw new AbortError();
145
+ }
146
+ }
147
+ // ─── Internal Helpers ─────────────────────────────────────────
148
+ createAbortController(externalSignal) {
149
+ const ac = new AbortController();
150
+ this.abortController = ac;
151
+ if (externalSignal) {
152
+ if (externalSignal.aborted) {
153
+ ac.abort();
154
+ } else {
155
+ externalSignal.addEventListener("abort", () => ac.abort(), {
156
+ once: true
157
+ });
158
+ }
159
+ }
160
+ return ac;
161
+ }
162
+ };
163
+
164
+ // src/utils/schema.ts
165
+ function zodToJsonSchema(schema) {
166
+ const schemaAny = schema;
167
+ if ("jsonSchema" in schema && typeof schemaAny.jsonSchema === "function") {
168
+ return schemaAny.jsonSchema();
169
+ }
170
+ return extractSchemaFromDef(schema);
171
+ }
172
+ function extractSchemaFromDef(schema) {
173
+ const def = schema._def;
174
+ const typeName = def.typeName;
175
+ switch (typeName) {
176
+ case "ZodString":
177
+ return { type: "string" };
178
+ case "ZodNumber":
179
+ return { type: "number" };
180
+ case "ZodBoolean":
181
+ return { type: "boolean" };
182
+ case "ZodNull":
183
+ return { type: "null" };
184
+ case "ZodArray":
185
+ return {
186
+ type: "array",
187
+ items: extractSchemaFromDef(def.type)
188
+ };
189
+ case "ZodObject": {
190
+ const shape = schema.shape;
191
+ const properties = {};
192
+ const required = [];
193
+ for (const [key, value] of Object.entries(shape)) {
194
+ const valueDef = value._def;
195
+ if (valueDef.typeName === "ZodOptional") {
196
+ properties[key] = extractSchemaFromDef(valueDef.innerType);
197
+ } else {
198
+ properties[key] = extractSchemaFromDef(value);
199
+ required.push(key);
200
+ }
201
+ }
202
+ return {
203
+ type: "object",
204
+ properties,
205
+ ...required.length > 0 ? { required } : {}
206
+ };
207
+ }
208
+ case "ZodOptional":
209
+ return extractSchemaFromDef(def.innerType);
210
+ case "ZodEnum":
211
+ return { type: "string", enum: def.values };
212
+ default:
213
+ return {};
214
+ }
215
+ }
216
+
217
+ // src/backends/vercel-ai.ts
218
+ var sdkModule = null;
219
+ var compatModule = null;
220
+ async function loadSDK() {
221
+ if (sdkModule) return sdkModule;
222
+ try {
223
+ sdkModule = await import('ai');
224
+ return sdkModule;
225
+ } catch {
226
+ throw new DependencyError("ai");
227
+ }
228
+ }
229
+ async function loadCompat() {
230
+ if (compatModule) return compatModule;
231
+ try {
232
+ compatModule = await import('@ai-sdk/openai-compatible');
233
+ return compatModule;
234
+ } catch {
235
+ throw new DependencyError("@ai-sdk/openai-compatible");
236
+ }
237
+ }
238
+ function _injectSDK(mock) {
239
+ sdkModule = mock;
240
+ }
241
+ function _injectCompat(mock) {
242
+ compatModule = mock;
243
+ }
244
+ function _resetSDK() {
245
+ sdkModule = null;
246
+ compatModule = null;
247
+ }
248
+ var DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
249
+ var DEFAULT_PROVIDER = "openrouter";
250
+ var DEFAULT_MAX_TURNS = 10;
251
+ function mapToolsToSDK(sdk, tools, config, sessionApprovals, permissionStore, signal) {
252
+ const toolMap = {};
253
+ const supervisor = config.supervisor;
254
+ for (const ourTool of tools) {
255
+ const jsonSchema = zodToJsonSchema(ourTool.parameters);
256
+ toolMap[ourTool.name] = sdk.tool({
257
+ description: ourTool.description,
258
+ parameters: sdk.jsonSchema(jsonSchema),
259
+ execute: wrapToolExecute(ourTool, supervisor, sessionApprovals, permissionStore, signal),
260
+ ...ourTool.needsApproval && supervisor?.onPermission ? {
261
+ needsApproval: async (_args) => {
262
+ if (permissionStore && await permissionStore.isApproved(ourTool.name)) return false;
263
+ if (sessionApprovals.has(ourTool.name)) return false;
264
+ return true;
265
+ }
266
+ } : {}
267
+ });
268
+ }
269
+ if (supervisor?.onAskUser) {
270
+ const onAskUser = supervisor.onAskUser;
271
+ toolMap["ask_user"] = sdk.tool({
272
+ description: "Ask the user a question and wait for their response",
273
+ parameters: sdk.jsonSchema({
274
+ type: "object",
275
+ properties: {
276
+ question: { type: "string", description: "The question to ask the user" }
277
+ },
278
+ required: ["question"]
279
+ }),
280
+ execute: async (args) => {
281
+ const response = await onAskUser(
282
+ { question: args.question, allowFreeform: true },
283
+ signal
284
+ );
285
+ return response.answer;
286
+ }
287
+ });
288
+ }
289
+ return toolMap;
290
+ }
291
+ function wrapToolExecute(ourTool, supervisor, sessionApprovals, permissionStore, signal) {
292
+ return async (args) => {
293
+ if (ourTool.needsApproval && supervisor?.onPermission) {
294
+ const storeApproved = permissionStore && await permissionStore.isApproved(ourTool.name);
295
+ if (!storeApproved && !sessionApprovals.has(ourTool.name)) {
296
+ const request = {
297
+ toolName: ourTool.name,
298
+ toolArgs: args ?? {}
299
+ };
300
+ const decision = await supervisor.onPermission(
301
+ request,
302
+ signal
303
+ );
304
+ if (!decision.allowed) {
305
+ throw new ToolExecutionError(
306
+ ourTool.name,
307
+ decision.reason ?? "Permission denied"
308
+ );
309
+ }
310
+ if (permissionStore && decision.scope) {
311
+ await permissionStore.approve(ourTool.name, decision.scope);
312
+ }
313
+ if (decision.scope === "session" || decision.scope === "always" || decision.scope === "project") {
314
+ sessionApprovals.add(ourTool.name);
315
+ }
316
+ if (decision.modifiedInput) {
317
+ args = decision.modifiedInput;
318
+ }
319
+ }
320
+ }
321
+ try {
322
+ const result = await ourTool.execute(args);
323
+ return result;
324
+ } catch (e) {
325
+ if (e instanceof ToolExecutionError) throw e;
326
+ throw new ToolExecutionError(
327
+ ourTool.name,
328
+ e instanceof Error ? e.message : String(e)
329
+ );
330
+ }
331
+ };
332
+ }
333
+ function messagesToSDK(messages) {
334
+ return messages.map((msg) => {
335
+ switch (msg.role) {
336
+ case "user":
337
+ return { role: "user", content: getTextContent(msg.content) };
338
+ case "assistant":
339
+ return { role: "assistant", content: getTextContent(msg.content) };
340
+ case "system":
341
+ return { role: "system", content: msg.content };
342
+ case "tool":
343
+ return { role: "tool", content: msg.content ?? "" };
344
+ default:
345
+ return { role: "user", content: "" };
346
+ }
347
+ });
348
+ }
349
+ function mapStreamPart(part) {
350
+ switch (part.type) {
351
+ case "text-delta":
352
+ return { type: "text_delta", text: part.text ?? "" };
353
+ case "tool-call":
354
+ return {
355
+ type: "tool_call_start",
356
+ toolName: part.toolName ?? "unknown",
357
+ args: part.args ?? {}
358
+ };
359
+ case "tool-result":
360
+ return {
361
+ type: "tool_call_end",
362
+ toolName: part.toolName ?? "unknown",
363
+ result: part.result ?? null
364
+ };
365
+ case "tool-error":
366
+ return {
367
+ type: "error",
368
+ error: part.error instanceof Error ? part.error.message : String(part.error ?? "Tool execution failed"),
369
+ recoverable: true
370
+ };
371
+ case "reasoning-start":
372
+ return { type: "thinking_start" };
373
+ case "reasoning-end":
374
+ return { type: "thinking_end" };
375
+ case "reasoning-delta":
376
+ return { type: "text_delta", text: part.text ?? "" };
377
+ case "finish-step":
378
+ return {
379
+ type: "usage_update",
380
+ promptTokens: Number(part.usage?.inputTokens ?? 0),
381
+ completionTokens: Number(part.usage?.outputTokens ?? 0)
382
+ };
383
+ case "error":
384
+ return {
385
+ type: "error",
386
+ error: part.error instanceof Error ? part.error.message : String(part.error ?? "Unknown error"),
387
+ recoverable: false
388
+ };
389
+ default:
390
+ return null;
391
+ }
392
+ }
393
+ var VercelAIAgent = class extends BaseAgent {
394
+ backendOptions;
395
+ sessionApprovals = /* @__PURE__ */ new Set();
396
+ model = null;
397
+ constructor(config, backendOptions) {
398
+ super(config);
399
+ this.backendOptions = backendOptions;
400
+ }
401
+ async getModel() {
402
+ if (this.model) return this.model;
403
+ const compat = await loadCompat();
404
+ const provider = compat.createOpenAICompatible({
405
+ name: this.backendOptions.provider ?? DEFAULT_PROVIDER,
406
+ baseURL: this.backendOptions.baseUrl ?? DEFAULT_BASE_URL,
407
+ apiKey: this.backendOptions.apiKey
408
+ });
409
+ const modelId = this.config.model ?? "anthropic/claude-sonnet-4-5";
410
+ this.model = provider.chatModel(modelId);
411
+ return this.model;
412
+ }
413
+ async getSDKTools(signal) {
414
+ const sdk = await loadSDK();
415
+ return mapToolsToSDK(sdk, this.config.tools, this.config, this.sessionApprovals, this.config.permissionStore, signal);
416
+ }
417
+ // ─── executeRun ─────────────────────────────────────────────────
418
+ async executeRun(messages, _options, signal) {
419
+ this.checkAbort(signal);
420
+ const sdk = await loadSDK();
421
+ const model = await this.getModel();
422
+ const tools = await this.getSDKTools(signal);
423
+ const maxTurns = this.config.maxTurns ?? DEFAULT_MAX_TURNS;
424
+ const sdkMessages = messagesToSDK(messages);
425
+ const hasTools = Object.keys(tools).length > 0;
426
+ const result = await sdk.generateText({
427
+ model,
428
+ system: this.config.systemPrompt,
429
+ messages: sdkMessages,
430
+ tools: hasTools ? tools : void 0,
431
+ maxSteps: maxTurns,
432
+ abortSignal: signal,
433
+ ...this.config.modelParams?.temperature !== void 0 && {
434
+ temperature: this.config.modelParams.temperature
435
+ },
436
+ ...this.config.modelParams?.maxTokens !== void 0 && {
437
+ maxTokens: this.config.modelParams.maxTokens
438
+ },
439
+ ...this.config.modelParams?.topP !== void 0 && {
440
+ topP: this.config.modelParams.topP
441
+ }
442
+ });
443
+ const toolCalls = [];
444
+ for (const step of result.steps) {
445
+ for (const tc of step.toolCalls) {
446
+ const matchingResult = step.toolResults.find(
447
+ (tr) => tr.toolCallId === tc.toolCallId
448
+ );
449
+ toolCalls.push({
450
+ toolName: tc.toolName,
451
+ args: tc.args ?? {},
452
+ result: matchingResult?.result ?? null,
453
+ approved: true
454
+ });
455
+ }
456
+ }
457
+ const usage = {
458
+ promptTokens: Number(result.totalUsage?.inputTokens ?? 0),
459
+ completionTokens: Number(result.totalUsage?.outputTokens ?? 0)
460
+ };
461
+ return {
462
+ output: result.text || null,
463
+ structuredOutput: void 0,
464
+ toolCalls,
465
+ messages: [
466
+ ...messages,
467
+ ...result.text ? [{ role: "assistant", content: result.text }] : []
468
+ ],
469
+ usage
470
+ };
471
+ }
472
+ // ─── executeRunStructured ───────────────────────────────────────
473
+ async executeRunStructured(messages, schema, _options, signal) {
474
+ this.checkAbort(signal);
475
+ const sdk = await loadSDK();
476
+ const model = await this.getModel();
477
+ const sdkMessages = messagesToSDK(messages);
478
+ const jsonSchema = zodToJsonSchema(schema.schema);
479
+ const result = await sdk.generateObject({
480
+ model,
481
+ system: this.config.systemPrompt,
482
+ messages: sdkMessages,
483
+ schema: sdk.jsonSchema(jsonSchema),
484
+ schemaName: schema.name,
485
+ schemaDescription: schema.description,
486
+ abortSignal: signal,
487
+ ...this.config.modelParams?.temperature !== void 0 && {
488
+ temperature: this.config.modelParams.temperature
489
+ },
490
+ ...this.config.modelParams?.maxTokens !== void 0 && {
491
+ maxTokens: this.config.modelParams.maxTokens
492
+ }
493
+ });
494
+ let structuredOutput;
495
+ try {
496
+ structuredOutput = schema.schema.parse(result.object);
497
+ } catch {
498
+ }
499
+ const usage = {
500
+ promptTokens: Number(result.usage?.inputTokens ?? 0),
501
+ completionTokens: Number(result.usage?.outputTokens ?? 0)
502
+ };
503
+ return {
504
+ output: JSON.stringify(result.object),
505
+ structuredOutput,
506
+ toolCalls: [],
507
+ messages: [
508
+ ...messages,
509
+ ...result.object != null ? [{ role: "assistant", content: JSON.stringify(result.object) }] : []
510
+ ],
511
+ usage
512
+ };
513
+ }
514
+ // ─── executeStream ──────────────────────────────────────────────
515
+ async *executeStream(messages, _options, signal) {
516
+ this.checkAbort(signal);
517
+ const sdk = await loadSDK();
518
+ const model = await this.getModel();
519
+ const tools = await this.getSDKTools(signal);
520
+ const maxTurns = this.config.maxTurns ?? DEFAULT_MAX_TURNS;
521
+ const sdkMessages = messagesToSDK(messages);
522
+ const hasTools = Object.keys(tools).length > 0;
523
+ const result = sdk.streamText({
524
+ model,
525
+ system: this.config.systemPrompt,
526
+ messages: sdkMessages,
527
+ tools: hasTools ? tools : void 0,
528
+ maxSteps: maxTurns,
529
+ abortSignal: signal,
530
+ ...this.config.modelParams?.temperature !== void 0 && {
531
+ temperature: this.config.modelParams.temperature
532
+ },
533
+ ...this.config.modelParams?.maxTokens !== void 0 && {
534
+ maxTokens: this.config.modelParams.maxTokens
535
+ },
536
+ ...this.config.modelParams?.topP !== void 0 && {
537
+ topP: this.config.modelParams.topP
538
+ }
539
+ });
540
+ let finalText = "";
541
+ try {
542
+ for await (const part of result.fullStream) {
543
+ if (signal.aborted) throw new AbortError();
544
+ const event = mapStreamPart(part);
545
+ if (event) yield event;
546
+ if (part.type === "text-delta") {
547
+ finalText += part.text ?? "";
548
+ }
549
+ }
550
+ const totalUsage = await result.totalUsage;
551
+ yield {
552
+ type: "usage_update",
553
+ promptTokens: Number(totalUsage?.inputTokens ?? 0),
554
+ completionTokens: Number(totalUsage?.outputTokens ?? 0)
555
+ };
556
+ yield {
557
+ type: "done",
558
+ finalOutput: finalText || null
559
+ };
560
+ } catch (e) {
561
+ if (signal.aborted) throw new AbortError();
562
+ throw e;
563
+ }
564
+ }
565
+ dispose() {
566
+ this.sessionApprovals.clear();
567
+ this.model = null;
568
+ super.dispose();
569
+ }
570
+ };
571
+ var VercelAIAgentService = class {
572
+ name = "vercel-ai";
573
+ disposed = false;
574
+ options;
575
+ constructor(options) {
576
+ this.options = options;
577
+ }
578
+ createAgent(config) {
579
+ if (this.disposed) throw new DisposedError("VercelAIAgentService");
580
+ return new VercelAIAgent(config, this.options);
581
+ }
582
+ async listModels() {
583
+ if (this.disposed) throw new DisposedError("VercelAIAgentService");
584
+ return [];
585
+ }
586
+ async validate() {
587
+ if (this.disposed) throw new DisposedError("VercelAIAgentService");
588
+ const errors = [];
589
+ if (!this.options.apiKey) {
590
+ errors.push("apiKey is required for Vercel AI backend.");
591
+ }
592
+ try {
593
+ await loadSDK();
594
+ } catch (e) {
595
+ errors.push(e instanceof Error ? e.message : String(e));
596
+ }
597
+ try {
598
+ await loadCompat();
599
+ } catch (e) {
600
+ errors.push(e instanceof Error ? e.message : String(e));
601
+ }
602
+ return { valid: errors.length === 0, errors };
603
+ }
604
+ async dispose() {
605
+ if (this.disposed) return;
606
+ this.disposed = true;
607
+ }
608
+ };
609
+ function createVercelAIService(options) {
610
+ return new VercelAIAgentService(options);
611
+ }
612
+
613
+ export { _injectCompat, _injectSDK, _resetSDK, createVercelAIService };
614
+ //# sourceMappingURL=vercel-ai.js.map
615
+ //# sourceMappingURL=vercel-ai.js.map