@agentionai/agents 1.6.0 → 1.8.0-beta-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 (41) hide show
  1. package/README.md +4 -2
  2. package/dist/agents/Agent.d.ts +9 -2
  3. package/dist/agents/Agent.js +4 -0
  4. package/dist/agents/AgentConfig.d.ts +76 -2
  5. package/dist/agents/BaseAgent.d.ts +24 -2
  6. package/dist/agents/BaseAgent.js +17 -0
  7. package/dist/agents/anthropic/ClaudeAgent.d.ts +4 -3
  8. package/dist/agents/anthropic/ClaudeAgent.js +47 -17
  9. package/dist/agents/cancellation.d.ts +55 -0
  10. package/dist/agents/cancellation.js +72 -0
  11. package/dist/agents/errors/AgentError.d.ts +50 -2
  12. package/dist/agents/errors/AgentError.js +57 -1
  13. package/dist/agents/google/GeminiAgent.d.ts +3 -2
  14. package/dist/agents/google/GeminiAgent.js +34 -11
  15. package/dist/agents/mistral/MistralAgent.d.ts +3 -2
  16. package/dist/agents/mistral/MistralAgent.js +33 -13
  17. package/dist/agents/ollama/OllamaAgent.d.ts +17 -3
  18. package/dist/agents/ollama/OllamaAgent.js +69 -19
  19. package/dist/agents/openai/OpenAiAgent.d.ts +4 -3
  20. package/dist/agents/openai/OpenAiAgent.js +52 -17
  21. package/dist/agents/openai-compatible/OpenAICompatibleAgent.d.ts +4 -3
  22. package/dist/agents/openai-compatible/OpenAICompatibleAgent.js +48 -19
  23. package/dist/agents/openrouter/OpenRouterAgent.d.ts +234 -0
  24. package/dist/agents/openrouter/OpenRouterAgent.js +711 -0
  25. package/dist/agents/openrouter/types.d.ts +164 -0
  26. package/dist/agents/openrouter/types.js +15 -0
  27. package/dist/core.d.ts +1 -0
  28. package/dist/core.js +1 -0
  29. package/dist/history/transformers.d.ts +80 -0
  30. package/dist/history/transformers.js +156 -1
  31. package/dist/history/types.d.ts +22 -2
  32. package/dist/history/types.js +13 -2
  33. package/dist/index.d.ts +5 -1
  34. package/dist/index.js +5 -1
  35. package/dist/mcp/MCPClient.js +4 -2
  36. package/dist/openrouter.d.ts +6 -0
  37. package/dist/openrouter.js +24 -0
  38. package/dist/tools/Tool.d.ts +13 -3
  39. package/dist/tools/Tool.js +18 -4
  40. package/dist/viz/types.d.ts +1 -1
  41. package/package.json +10 -1
@@ -0,0 +1,711 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.OpenRouterAgent = void 0;
37
+ exports.defaultHeadersHook = defaultHeadersHook;
38
+ exports.parseRetryAfter = parseRetryAfter;
39
+ exports.parseResetAt = parseResetAt;
40
+ const BaseAgent_1 = require("../BaseAgent");
41
+ const AgentEvent_1 = require("../AgentEvent");
42
+ const cancellation_1 = require("../cancellation");
43
+ const AgentError_1 = require("../errors/AgentError");
44
+ const transformers_1 = require("../../history/transformers");
45
+ const VizReporter_1 = require("../../viz/VizReporter");
46
+ const VizConfig_1 = require("../../viz/VizConfig");
47
+ /**
48
+ * Retry policy the agent applies when the config does not name one.
49
+ *
50
+ * `@openrouter/sdk` already knows how to honour `Retry-After` and
51
+ * `retry-after-ms` (`lib/retries.js`), but `chat.send()` defaults to
52
+ * `retryCodes: ["5XX"]`, so a 429 never reaches that code — and its default
53
+ * `maxElapsedTime` is 3_600_000, an hour-long retry loop. Both are replaced
54
+ * here; see {@link OpenRouterSpecificConfig.retry}.
55
+ */
56
+ const DEFAULT_RETRY = {
57
+ strategy: "backoff",
58
+ backoff: {
59
+ initialInterval: 500,
60
+ maxInterval: 30000,
61
+ exponent: 1.5,
62
+ // Two minutes: long enough to sit out a couple of `Retry-After` waits on a
63
+ // per-minute limit, short enough that a daily quota (whose reset is hours
64
+ // away) fails fast instead of blocking the run. Use `models` fallbacks for
65
+ // that case, not a longer wait.
66
+ maxElapsedTime: 120000,
67
+ },
68
+ retryConnectionErrors: true,
69
+ };
70
+ /** Status codes retried by default. `429` is the one the SDK omits. */
71
+ const DEFAULT_RETRY_CODES = ["408", "409", "429", "5XX"];
72
+ /**
73
+ * Build a `beforeRequest` hook that adds custom headers to every request.
74
+ *
75
+ * `@openrouter/sdk` has no `defaultHeaders` option like the Anthropic and
76
+ * OpenAI clients, so headers are injected at the HTTP layer instead. They
77
+ * overwrite headers the SDK already set, so that `defaultHeaders` means the
78
+ * same thing on every provider — see `CommonAgentConfig.defaultHeaders`.
79
+ *
80
+ * `httpReferer` / `appTitle` are a separate OpenRouter attribution path
81
+ * (`HTTP-Referer` / `X-Title`). They are not a substitute for tracing or
82
+ * gateway headers.
83
+ */
84
+ function defaultHeadersHook(headers) {
85
+ return (request) => {
86
+ for (const [name, value] of Object.entries(headers)) {
87
+ request.headers.set(name, value);
88
+ }
89
+ };
90
+ }
91
+ /**
92
+ * Agent backed by [OpenRouter](https://openrouter.ai) via the official
93
+ * `@openrouter/sdk`, giving one API key access to models from every provider it
94
+ * fronts.
95
+ *
96
+ * Beyond what an OpenAI-compatible endpoint offers, this agent exposes
97
+ * OpenRouter's routing controls — `models` fallbacks, `provider` preferences —
98
+ * reports the credit cost of each run on {@link lastGeneration}, and round-trips
99
+ * `reasoning_details` so multi-turn tool calls work on reasoning models whose
100
+ * thinking blocks are signed.
101
+ *
102
+ * @requires @openrouter/sdk - Install as a peer dependency:
103
+ * ```bash
104
+ * npm install @openrouter/sdk
105
+ * ```
106
+ * The SDK is ESM-only, so it is loaded through a dynamic import. On CommonJS
107
+ * that needs Node 20.19+ or 22.12+, where `require()` of an ES module works.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * const agent = new OpenRouterAgent({
112
+ * id: "router",
113
+ * name: "Router",
114
+ * description: "Answers questions",
115
+ * apiKey: process.env.OPENROUTER_API_KEY!,
116
+ * model: "anthropic/claude-opus-4-20250514",
117
+ * models: ["openai/gpt-5.6"], // used if the primary is rate limited
118
+ * provider: { sort: "throughput" },
119
+ * });
120
+ *
121
+ * const answer = await agent.execute("Explain recursion");
122
+ * console.log(agent.lastGeneration?.cost, "credits");
123
+ * ```
124
+ */
125
+ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
126
+ constructor(config, history) {
127
+ super({ ...config, vendor: "openrouter" }, history);
128
+ this.currentToolCallCount = 0;
129
+ // Flat config wins over the nested form, matching the other agents.
130
+ const nested = config.vendorConfig?.openrouter ?? {};
131
+ this.config = {
132
+ ...config,
133
+ vendor: "openrouter",
134
+ model: config.model || "openrouter/auto",
135
+ models: config.models ?? nested.models,
136
+ provider: config.provider ?? nested.provider,
137
+ retry: config.retry ?? nested.retry,
138
+ retryCodes: config.retryCodes ?? nested.retryCodes,
139
+ reasoning: config.reasoning ?? nested.reasoning,
140
+ plugins: config.plugins ?? nested.plugins,
141
+ sessionId: config.sessionId ?? nested.sessionId,
142
+ user: config.user ?? nested.user,
143
+ serviceTier: config.serviceTier ?? nested.serviceTier,
144
+ httpReferer: config.httpReferer ?? nested.httpReferer,
145
+ appTitle: config.appTitle ?? nested.appTitle,
146
+ disableParallelToolUse: config.disableParallelToolUse ?? nested.disableParallelToolUse,
147
+ };
148
+ this.model = this.config.model;
149
+ this.addSystemMessage(this.getSystemMessage());
150
+ }
151
+ /**
152
+ * Load `@openrouter/sdk` and construct the client, once per agent.
153
+ *
154
+ * The specifier goes through a variable so TypeScript does not resolve it at
155
+ * build time, which keeps the dependency genuinely optional — the same
156
+ * approach `MCPClient` uses. The promise is memoized including its rejection,
157
+ * so a missing package reports the install hint on every call rather than
158
+ * retrying the import.
159
+ */
160
+ getClient() {
161
+ if (!this.clientPromise) {
162
+ this.clientPromise = this.createClient();
163
+ }
164
+ return this.clientPromise;
165
+ }
166
+ async createClient() {
167
+ const pkg = "@openrouter/sdk";
168
+ let OpenRouter;
169
+ let HTTPClient;
170
+ try {
171
+ ({ OpenRouter, HTTPClient } = (await Promise.resolve(`${pkg}`).then(s => __importStar(require(s)))));
172
+ }
173
+ catch (error) {
174
+ throw new AgentError_1.ExecutionError(`OpenRouterAgent requires the '@openrouter/sdk' package. Install it with: npm install @openrouter/sdk` +
175
+ `\nUnderlying error: ${error instanceof Error ? error.message : "Unknown error"}`);
176
+ }
177
+ let httpClient;
178
+ if (this.config.defaultHeaders) {
179
+ httpClient = new HTTPClient();
180
+ httpClient.addHook("beforeRequest", defaultHeadersHook(this.config.defaultHeaders));
181
+ }
182
+ return new OpenRouter({
183
+ apiKey: this.config.apiKey,
184
+ ...(this.config.baseURL ? { serverURL: this.config.baseURL } : {}),
185
+ ...(this.config.httpReferer ? { httpReferer: this.config.httpReferer } : {}),
186
+ ...(this.config.appTitle ? { appTitle: this.config.appTitle } : {}),
187
+ ...(this.config.timeout ? { timeoutMs: this.config.timeout } : {}),
188
+ ...(this.config.debug ? { debugLogger: console } : {}),
189
+ ...(httpClient ? { httpClient } : {}),
190
+ });
191
+ }
192
+ getToolDefinitions() {
193
+ return Array.from(this.tools.values()).map((tool) => {
194
+ const prompt = tool.getPrompt();
195
+ return {
196
+ type: "function",
197
+ function: {
198
+ name: prompt.name,
199
+ description: prompt.description,
200
+ parameters: prompt.input_schema,
201
+ },
202
+ };
203
+ });
204
+ }
205
+ async process(_input) {
206
+ return "";
207
+ }
208
+ /**
209
+ * List the models OpenRouter offers, following pagination to the end.
210
+ *
211
+ * Fills `contextLength`, `maxOutputTokens` and `capabilities` from
212
+ * OpenRouter's own metadata: `supported_parameters` says whether a model takes
213
+ * `tools` and `reasoning`, and `architecture.input_modalities` whether it
214
+ * accepts images. Per-token pricing is on `raw.pricing`.
215
+ */
216
+ async listModels() {
217
+ try {
218
+ const client = await this.getClient();
219
+ const result = await client.models.list();
220
+ const models = [];
221
+ for await (const page of result) {
222
+ // `models.list()` yields GetModelsResponse: `{ result: { data, links, totalCount } }`.
223
+ const entries = page?.result?.data ?? [];
224
+ for (const card of entries) {
225
+ const params = card.supportedParameters ?? [];
226
+ const modalities = card.architecture?.inputModalities ?? [];
227
+ models.push({
228
+ id: card.id,
229
+ displayName: card.name,
230
+ created: card.created ? new Date(card.created * 1000) : undefined,
231
+ contextLength: card.contextLength ?? card.topProvider?.contextLength ?? undefined,
232
+ maxOutputTokens: card.topProvider?.maxCompletionTokens ?? undefined,
233
+ capabilities: {
234
+ chat: true,
235
+ tools: params.includes("tools"),
236
+ vision: modalities.includes("image"),
237
+ thinking: params.includes("reasoning") || params.includes("include_reasoning"),
238
+ },
239
+ raw: card,
240
+ });
241
+ }
242
+ }
243
+ return models;
244
+ }
245
+ catch (error) {
246
+ throw new AgentError_1.ExecutionError(`Failed to list OpenRouter models: ${error instanceof Error ? error.message : "Unknown error"}`);
247
+ }
248
+ }
249
+ async execute(input, options) {
250
+ this.beginRun(input);
251
+ try {
252
+ const response = await this.callProvider(options);
253
+ this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, response);
254
+ return await this.handleResponse(response, options);
255
+ }
256
+ catch (error) {
257
+ throw this.failRun(error, options);
258
+ }
259
+ finally {
260
+ this.history.endExecution();
261
+ }
262
+ }
263
+ /**
264
+ * Stream a response as an async generator of {@link StreamChunk} objects.
265
+ *
266
+ * Tool calls are executed transparently — the generator keeps streaming after
267
+ * each tool-call round trip.
268
+ */
269
+ async *executeStream(input, options) {
270
+ this.beginRun(input);
271
+ try {
272
+ yield* this.streamTurn(options);
273
+ }
274
+ catch (error) {
275
+ throw this.failRun(error, options);
276
+ }
277
+ finally {
278
+ this.history.endExecution();
279
+ }
280
+ }
281
+ /** Shared setup for `execute()` and `executeStream()`. */
282
+ beginRun(input) {
283
+ this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
284
+ this.resetTokenUsage();
285
+ this.lastGeneration = undefined;
286
+ this.currentToolCallCount = 0;
287
+ if (VizConfig_1.vizConfig.isEnabled()) {
288
+ this.vizEventId = VizReporter_1.vizReporter.agentStart(this.id, this.name, this.config.model, this.vendor, typeof input === "string" ? input : JSON.stringify(input));
289
+ }
290
+ if (this.history.transient) {
291
+ this.history.clear();
292
+ this.addSystemMessage(this.getSystemMessage());
293
+ }
294
+ if (typeof input === "string") {
295
+ this.addTextToHistory("user", input);
296
+ }
297
+ else {
298
+ this.addMessageToHistory("user", input);
299
+ }
300
+ this.history.setSessionAnchor();
301
+ this.history.beginExecution();
302
+ }
303
+ /**
304
+ * Map whatever a run threw onto this library's error types, emit it, and close
305
+ * any open visualization event. Returns the error for the caller to throw.
306
+ */
307
+ failRun(error, options) {
308
+ // The abort branch comes first and keys off the signal rather than the
309
+ // error's name, so a cancellation still surfaces as an AbortError even when
310
+ // an inner catch already wrapped it.
311
+ if ((0, cancellation_1.isAbortError)(error, options?.signal)) {
312
+ const abortError = this.abortError(error, options?.signal);
313
+ this.closeViz("AbortError", abortError.message, false);
314
+ return abortError;
315
+ }
316
+ const mapped = this.mapProviderError(error);
317
+ this.emit(AgentEvent_1.AgentEvent.ERROR, mapped);
318
+ this.closeViz(mapped.name, mapped.message, mapped instanceof AgentError_1.ApiError && mapped.statusCode === 429);
319
+ return mapped;
320
+ }
321
+ /**
322
+ * Turn an `@openrouter/sdk` error into an {@link AgentError}.
323
+ *
324
+ * The SDK throws one class per status code, all extending `OpenRouterError`
325
+ * with `statusCode`, `headers` and `body`. Rather than importing those classes
326
+ * — which would make the optional peer dependency mandatory — this reads the
327
+ * shape structurally.
328
+ */
329
+ mapProviderError(error) {
330
+ if (error instanceof AgentError_1.AgentError)
331
+ return error;
332
+ const err = error;
333
+ if (typeof err?.statusCode === "number") {
334
+ if (err.statusCode === 429) {
335
+ return this.rateLimitError(err);
336
+ }
337
+ return new AgentError_1.ApiError(`OpenRouter API error: ${err.message ?? "Unknown error"}`, err.statusCode, error);
338
+ }
339
+ return new AgentError_1.ExecutionError(`OpenRouter error: ${error instanceof Error ? error.message : "Unknown error"}`);
340
+ }
341
+ /**
342
+ * Build a {@link RateLimitError} from a 429, lifting OpenRouter's rate-limit
343
+ * headers onto it. They are only present on OpenRouter's own platform limits —
344
+ * a 429 passed through from an upstream provider carries neither, which is why
345
+ * every field is optional.
346
+ */
347
+ rateLimitError(err) {
348
+ const headers = err.headers;
349
+ const num = (name) => {
350
+ const raw = headers?.get(name);
351
+ if (!raw)
352
+ return undefined;
353
+ const parsed = Number(raw);
354
+ return Number.isFinite(parsed) ? parsed : undefined;
355
+ };
356
+ // `Retry-After` is defined by RFC 9110 as either delay-seconds or an
357
+ // HTTP-date. OpenRouter sends the numeric form, but the date form is
358
+ // legal and costs one branch to accept.
359
+ const retryAfterRaw = headers?.get("retry-after");
360
+ const retryAfterMs = parseRetryAfter(retryAfterRaw);
361
+ return new AgentError_1.RateLimitError(`OpenRouter rate limit: ${err.message ?? "Too many requests"}`, retryAfterMs, num("x-ratelimit-limit"), num("x-ratelimit-remaining"), parseResetAt(num("x-ratelimit-reset")), err);
362
+ }
363
+ closeViz(name, message, throttled) {
364
+ if (!this.vizEventId)
365
+ return;
366
+ VizReporter_1.vizReporter.agentError(this.vizEventId, name, message, throttled);
367
+ this.vizEventId = undefined;
368
+ }
369
+ /**
370
+ * Wrap a `ChatRequest` in the envelope `@openrouter/sdk` `chat.send()` expects.
371
+ * Passing the body bare fails Speakeasy validation (`Input validation failed`).
372
+ */
373
+ sendRequest(stream) {
374
+ return {
375
+ chatRequest: this.buildRequest(stream),
376
+ ...(this.config.httpReferer ? { httpReferer: this.config.httpReferer } : {}),
377
+ ...(this.config.appTitle ? { appTitle: this.config.appTitle } : {}),
378
+ };
379
+ }
380
+ /** The `ChatRequest` body, identical for the streaming and buffered paths. */
381
+ buildRequest(stream) {
382
+ const messages = transformers_1.openRouterTransformer.toProvider(this.history.getEntries());
383
+ const tools = this.tools.size > 0 ? this.getToolDefinitions() : undefined;
384
+ return {
385
+ model: this.config.model,
386
+ messages,
387
+ stream,
388
+ // Ask for usage in the stream. Without this OpenRouter often omits the
389
+ // `usage` chunk, leaving `lastTokenUsage` / `lastGeneration.cost` empty
390
+ // on `executeStream()` — same option the OpenAI-compatible agent sends.
391
+ ...(stream ? { stream_options: { include_usage: true } } : {}),
392
+ ...(tools ? { tools } : {}),
393
+ ...(this.config.models?.length ? { models: this.config.models } : {}),
394
+ ...(this.config.provider ? { provider: this.config.provider } : {}),
395
+ ...(this.config.reasoning ? { reasoning: this.config.reasoning } : {}),
396
+ ...(this.config.plugins?.length ? { plugins: this.config.plugins } : {}),
397
+ ...(this.config.sessionId ? { sessionId: this.config.sessionId } : {}),
398
+ ...(this.config.user ? { user: this.config.user } : {}),
399
+ ...(this.config.serviceTier ? { serviceTier: this.config.serviceTier } : {}),
400
+ ...(this.config.disableParallelToolUse !== undefined
401
+ ? { parallelToolCalls: !this.config.disableParallelToolUse }
402
+ : {}),
403
+ maxTokens: this.config.maxTokens,
404
+ temperature: this.config.temperature,
405
+ topP: this.config.topP,
406
+ topK: this.config.topK,
407
+ stop: this.config.stopSequences,
408
+ seed: this.config.seed,
409
+ presencePenalty: this.config.presencePenalty,
410
+ frequencyPenalty: this.config.frequencyPenalty,
411
+ };
412
+ }
413
+ /**
414
+ * Per-request options: the cancellation signal plus the retry policy.
415
+ *
416
+ * `retryCodes` has to be passed on every call — the SDK reads it only from the
417
+ * call options, never from the client's, so setting it once at construction
418
+ * would silently do nothing.
419
+ */
420
+ requestOptions(options) {
421
+ return {
422
+ ...(options?.signal ? { signal: options.signal } : {}),
423
+ retries: this.config.retry ?? DEFAULT_RETRY,
424
+ retryCodes: this.config.retryCodes ?? DEFAULT_RETRY_CODES,
425
+ // Ask OpenRouter to include `openrouter_metadata` (attempt count, etc.)
426
+ // in the response. It only does so when the header is present, and default
427
+ // is off — without it `openrouterMetadata` never appears.
428
+ headers: { "X-OpenRouter-Metadata": "1" },
429
+ };
430
+ }
431
+ async callProvider(options) {
432
+ const client = await this.getClient();
433
+ this.startTurnTimer();
434
+ return client.chat.send(this.sendRequest(false), this.requestOptions(options));
435
+ }
436
+ async handleResponse(response, options) {
437
+ const usage = this.accumulateUsage(this.parseUsage(response));
438
+ this.recordGeneration(response);
439
+ const choice = response?.choices?.[0];
440
+ // OpenRouter can report a provider failure inside a 200 body rather than as
441
+ // an HTTP error, with whatever text was generated before it failed. Without
442
+ // this the run would look like a successful short answer.
443
+ if (!choice) {
444
+ throw new AgentError_1.ExecutionError(`OpenRouter returned no choices: ${response?.error?.message ?? "empty response"}`);
445
+ }
446
+ if (choice.finishReason === "error") {
447
+ throw new AgentError_1.ApiError(`OpenRouter provider error mid-generation: ${response?.error?.message ?? "no message"}`, response?.error?.code, response);
448
+ }
449
+ const message = choice.message ?? {};
450
+ if (choice.finishReason === "length") {
451
+ const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
452
+ this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
453
+ throw error;
454
+ }
455
+ const toolCalls = message.toolCalls ?? [];
456
+ if (toolCalls.length === 0) {
457
+ const textContent = message.content || "";
458
+ this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(message));
459
+ this.emit(AgentEvent_1.AgentEvent.DONE, message, usage);
460
+ this.completeViz(textContent);
461
+ return textContent;
462
+ }
463
+ // Stop before the assistant turn is written: bailing out here avoids both
464
+ // running the tools' side effects and leaving a tool call in history with no
465
+ // tool message to answer it.
466
+ (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
467
+ this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
468
+ this.currentToolCallCount += toolCalls.length;
469
+ this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(message));
470
+ const toolResults = await this.handleToolCalls(toolCalls, options);
471
+ for (const result of toolResults) {
472
+ this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
473
+ }
474
+ const newResponse = await this.callProvider(options);
475
+ this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, newResponse);
476
+ return this.handleResponse(newResponse, options);
477
+ }
478
+ async *streamTurn(options) {
479
+ const client = await this.getClient();
480
+ this.startTurnTimer();
481
+ const stream = await client.chat.send(this.sendRequest(true), this.requestOptions(options));
482
+ let textContent = "";
483
+ let reasoningContent = "";
484
+ let reasoningDetails = [];
485
+ const toolCallAcc = new Map();
486
+ let finishReason = null;
487
+ let streamUsage;
488
+ let streamError;
489
+ for await (const chunk of stream) {
490
+ // Once the first token is out the 200 and its headers are committed, so a
491
+ // provider failure after that point arrives as an SSE payload instead of
492
+ // an HTTP status. Recorded and thrown after the loop, so the tokens
493
+ // already spent still get reported.
494
+ if (chunk?.error)
495
+ streamError = chunk.error;
496
+ // Usage rides on whichever chunk OpenRouter chooses — often the last
497
+ // content chunk rather than a trailing choice-less one. It is a running
498
+ // total for the turn, not a delta, so keeping the most recent covers both
499
+ // layouts without double-counting.
500
+ if (chunk?.usage)
501
+ streamUsage = chunk.usage;
502
+ if (chunk?.id || chunk?.model)
503
+ this.recordGeneration(chunk);
504
+ const choice = chunk?.choices?.[0];
505
+ if (!choice)
506
+ continue;
507
+ finishReason = choice.finishReason ?? finishReason;
508
+ const delta = choice.delta ?? {};
509
+ if (delta.content) {
510
+ this.markFirstToken();
511
+ textContent += delta.content;
512
+ this.emit(AgentEvent_1.AgentEvent.CHUNK, delta.content);
513
+ yield { type: "text", content: delta.content };
514
+ }
515
+ if (delta.reasoning) {
516
+ this.markFirstToken();
517
+ // Accumulated as well as yielded: the assistant turn has to carry its
518
+ // reasoning back on the next request.
519
+ reasoningContent += delta.reasoning;
520
+ this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, delta.reasoning);
521
+ yield { type: "reasoning", content: delta.reasoning };
522
+ }
523
+ if (delta.reasoningDetails?.length) {
524
+ reasoningDetails = reasoningDetails.concat(delta.reasoningDetails);
525
+ }
526
+ if (delta.toolCalls) {
527
+ for (const tc of delta.toolCalls) {
528
+ const index = tc.index ?? 0;
529
+ if (!toolCallAcc.has(index)) {
530
+ toolCallAcc.set(index, { id: "", name: "", arguments: "" });
531
+ }
532
+ const acc = toolCallAcc.get(index);
533
+ if (tc.id)
534
+ acc.id = tc.id;
535
+ if (tc.function?.name)
536
+ acc.name += tc.function.name;
537
+ if (tc.function?.arguments)
538
+ acc.arguments += tc.function.arguments;
539
+ }
540
+ }
541
+ }
542
+ // Before any throw below, so a turn that failed part way still reports what
543
+ // it spent.
544
+ if (streamUsage)
545
+ this.accumulateUsage(this.parseUsageObject(streamUsage));
546
+ // The SDK's stream iterator stops yielding on abort rather than throwing, so
547
+ // without this an interrupted stream would look like a short but complete
548
+ // turn — writing partial text to history and emitting DONE.
549
+ (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
550
+ if (streamError) {
551
+ throw new AgentError_1.ApiError(`OpenRouter stream error: ${streamError.message ?? "no message"}`, streamError.code, streamError);
552
+ }
553
+ if (finishReason === "length") {
554
+ const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
555
+ this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
556
+ throw error;
557
+ }
558
+ const assistantMessage = {
559
+ role: "assistant",
560
+ content: textContent || null,
561
+ reasoning: reasoningContent || null,
562
+ reasoningDetails,
563
+ };
564
+ if (finishReason === "tool_calls" && toolCallAcc.size > 0) {
565
+ // As in handleResponse(): bail out before the assistant turn is written,
566
+ // so a cancelled run leaves no unanswered tool call in history.
567
+ (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
568
+ const toolCalls = Array.from(toolCallAcc.entries())
569
+ .sort(([a], [b]) => a - b)
570
+ .map(([, tc]) => ({
571
+ id: tc.id,
572
+ type: "function",
573
+ function: { name: tc.name, arguments: tc.arguments },
574
+ }));
575
+ this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
576
+ this.currentToolCallCount += toolCalls.length;
577
+ this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage({
578
+ ...assistantMessage,
579
+ toolCalls,
580
+ }));
581
+ const toolResults = await this.handleToolCalls(toolCalls, options);
582
+ for (const result of toolResults) {
583
+ this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
584
+ }
585
+ yield* this.streamTurn(options);
586
+ }
587
+ else {
588
+ this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(assistantMessage));
589
+ this.emit(AgentEvent_1.AgentEvent.DONE, { content: textContent }, this.lastTokenUsage);
590
+ this.completeViz(textContent);
591
+ }
592
+ }
593
+ async handleToolCalls(toolCalls, options) {
594
+ return Promise.all(toolCalls.map(async (toolCall) => {
595
+ const toolName = toolCall.function?.name ?? "";
596
+ const tool = this.tools.get(toolName);
597
+ const toolCallId = toolCall.id;
598
+ if (!toolCall.function || !tool) {
599
+ const errorMessage = `Tool '${toolName}' not found`;
600
+ this.emit(AgentEvent_1.AgentEvent.TOOL_ERROR, new AgentError_1.ToolExecutionError(errorMessage, toolName, toolCall.function?.arguments));
601
+ return { toolCallId, content: errorMessage };
602
+ }
603
+ try {
604
+ const args = JSON.parse(toolCall.function.arguments || "{}");
605
+ const result = await tool.execute(this.getId(), this.getName(), args, toolCallId, this.config.model, this.vendor, { signal: options?.signal });
606
+ return { toolCallId, content: JSON.stringify(result) };
607
+ }
608
+ catch (error) {
609
+ const errorMessage = `Error executing tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`;
610
+ if (this.debug) {
611
+ console.error(errorMessage);
612
+ }
613
+ this.emit(AgentEvent_1.AgentEvent.TOOL_ERROR, new AgentError_1.ToolExecutionError(errorMessage, toolName, toolCall.function.arguments));
614
+ return { toolCallId, content: errorMessage };
615
+ }
616
+ }));
617
+ }
618
+ /**
619
+ * Fold one API call's cost and routing facts into {@link lastGeneration}.
620
+ * Cost is summed — a tool loop bills once per hop — while the id and model
621
+ * describe the most recent call.
622
+ */
623
+ recordGeneration(response) {
624
+ const cost = response?.usage?.cost;
625
+ const previous = this.lastGeneration;
626
+ this.lastGeneration = {
627
+ id: response?.id ?? previous?.id,
628
+ model: response?.model ?? previous?.model,
629
+ cost: typeof cost === "number"
630
+ ? (previous?.cost ?? 0) + cost
631
+ : previous?.cost,
632
+ isByok: response?.usage?.isByok ?? previous?.isByok,
633
+ attempts: response?.openrouterMetadata?.attempt ?? previous?.attempts,
634
+ };
635
+ }
636
+ parseUsage(response) {
637
+ return this.parseUsageObject(response?.usage);
638
+ }
639
+ parseUsageObject(usage) {
640
+ return {
641
+ input_tokens: usage?.promptTokens ?? 0,
642
+ output_tokens: usage?.completionTokens ?? 0,
643
+ total_tokens: usage?.totalTokens ?? 0,
644
+ reasoning_tokens: usage?.completionTokensDetails?.reasoningTokens ?? undefined,
645
+ };
646
+ }
647
+ completeViz(textContent) {
648
+ if (!this.vizEventId)
649
+ return;
650
+ VizReporter_1.vizReporter.agentComplete(this.vizEventId, {
651
+ input: this.lastTokenUsage?.input_tokens || 0,
652
+ output: this.lastTokenUsage?.output_tokens || 0,
653
+ total: this.lastTokenUsage?.total_tokens || 0,
654
+ }, "end_turn", this.currentToolCallCount > 0, this.currentToolCallCount, textContent);
655
+ this.vizEventId = undefined;
656
+ }
657
+ }
658
+ exports.OpenRouterAgent = OpenRouterAgent;
659
+ /**
660
+ * Milliseconds to wait from a `Retry-After` header.
661
+ *
662
+ * RFC 9110 allows two forms — delay-seconds (`120`) and an HTTP-date
663
+ * (`Wed, 21 Oct 2026 07:28:00 GMT`). OpenRouter sends the first; the second is
664
+ * accepted because it is legal and cheap to support. A date already in the past
665
+ * yields `0` rather than a negative wait.
666
+ *
667
+ * @returns The delay in milliseconds, or `undefined` when the header is absent
668
+ * or unparseable.
669
+ */
670
+ function parseRetryAfter(raw) {
671
+ if (!raw)
672
+ return undefined;
673
+ const seconds = Number(raw);
674
+ if (Number.isFinite(seconds)) {
675
+ return Math.max(0, seconds * 1000);
676
+ }
677
+ const date = Date.parse(raw);
678
+ if (Number.isFinite(date)) {
679
+ return Math.max(0, date - Date.now());
680
+ }
681
+ return undefined;
682
+ }
683
+ /**
684
+ * The instant an `X-RateLimit-Reset` header points at.
685
+ *
686
+ * OpenRouter documents that the header exists but not what is in it, and the
687
+ * three encodings in common use across APIs are indistinguishable by type — so
688
+ * they are told apart by magnitude, taking "the answer is somewhere near now" as
689
+ * the tiebreaker:
690
+ *
691
+ * - below `10^9` — a duration in seconds from now (a literal epoch would be
692
+ * before 2001, which no live API means)
693
+ * - below `10^11` — Unix **seconds** (`10^11` seconds is the year 5138, so
694
+ * anything under it is a plausible timestamp and anything over it is not)
695
+ * - otherwise — Unix **milliseconds**
696
+ *
697
+ * Returns `undefined` for a missing or non-finite value, so callers see "not
698
+ * reported" rather than a date in 1970. Prefer
699
+ * {@link RateLimitError.retryAfterMs} when both are present: it is unambiguous.
700
+ */
701
+ function parseResetAt(value) {
702
+ if (value === undefined || !Number.isFinite(value) || value < 0) {
703
+ return undefined;
704
+ }
705
+ if (value < 1e9)
706
+ return new Date(Date.now() + value * 1000);
707
+ if (value < 1e11)
708
+ return new Date(value * 1000);
709
+ return new Date(value);
710
+ }
711
+ //# sourceMappingURL=OpenRouterAgent.js.map