@agentionai/agents 1.11.0 → 1.12.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.
@@ -294,6 +294,7 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
294
294
  beginRun(input) {
295
295
  this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
296
296
  this.resetTokenUsage();
297
+ this.resetPartialTurn();
297
298
  this.lastGeneration = undefined;
298
299
  this.currentToolCallCount = 0;
299
300
  if (VizConfig_1.vizConfig.isEnabled()) {
@@ -323,12 +324,12 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
323
324
  if ((0, cancellation_1.isAbortError)(error, options?.signal)) {
324
325
  const abortError = this.abortError(error, options?.signal);
325
326
  this.closeViz("AbortError", abortError.message, false);
326
- return abortError;
327
+ return this.withPartialTurn(abortError);
327
328
  }
328
329
  const mapped = this.mapProviderError(error);
329
330
  this.emit(AgentEvent_1.AgentEvent.ERROR, mapped);
330
331
  this.closeViz(mapped.name, mapped.message, mapped instanceof AgentError_1.ApiError && mapped.statusCode === 429);
331
- return mapped;
332
+ return this.withPartialTurn(mapped);
332
333
  }
333
334
  /**
334
335
  * Turn an `@openrouter/sdk` error into an {@link AgentError}.
@@ -515,108 +516,140 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
515
516
  let finishReason = null;
516
517
  let streamUsage;
517
518
  let streamError;
518
- for await (const chunk of stream) {
519
- // Once the first token is out the 200 and its headers are committed, so a
520
- // provider failure after that point arrives as an SSE payload instead of
521
- // an HTTP status. Recorded and thrown after the loop, so the tokens
522
- // already spent still get reported.
523
- if (chunk?.error)
524
- streamError = chunk.error;
525
- // Usage rides on whichever chunk OpenRouter chooses — often the last
526
- // content chunk rather than a trailing choice-less one. It is a running
527
- // total for the turn, not a delta, so keeping the most recent covers both
528
- // layouts without double-counting.
529
- if (chunk?.usage)
530
- streamUsage = chunk.usage;
531
- if (chunk?.id || chunk?.model)
532
- this.recordGeneration(chunk);
533
- const choice = chunk?.choices?.[0];
534
- if (!choice)
535
- continue;
536
- finishReason = choice.finishReason ?? finishReason;
537
- const delta = choice.delta ?? {};
538
- if (delta.content) {
539
- this.markFirstToken();
540
- textContent += delta.content;
541
- this.emit(AgentEvent_1.AgentEvent.CHUNK, delta.content);
542
- yield { type: "text", content: delta.content };
519
+ // Set once this frame's assistant message reaches history. Until then the
520
+ // turn exists only in the accumulators above, and the `finally` salvages
521
+ // them a reasoning trail can be minutes of generation, and the stream
522
+ // throwing (or the consumer walking away) would otherwise drop it.
523
+ let committed = false;
524
+ let failure;
525
+ try {
526
+ for await (const chunk of stream) {
527
+ // Once the first token is out the 200 and its headers are committed, so a
528
+ // provider failure after that point arrives as an SSE payload instead of
529
+ // an HTTP status. Recorded and thrown after the loop, so the tokens
530
+ // already spent still get reported.
531
+ if (chunk?.error)
532
+ streamError = chunk.error;
533
+ // Usage rides on whichever chunk OpenRouter chooses — often the last
534
+ // content chunk rather than a trailing choice-less one. It is a running
535
+ // total for the turn, not a delta, so keeping the most recent covers both
536
+ // layouts without double-counting.
537
+ if (chunk?.usage)
538
+ streamUsage = chunk.usage;
539
+ if (chunk?.id || chunk?.model)
540
+ this.recordGeneration(chunk);
541
+ const choice = chunk?.choices?.[0];
542
+ if (!choice)
543
+ continue;
544
+ finishReason = choice.finishReason ?? finishReason;
545
+ const delta = choice.delta ?? {};
546
+ if (delta.content) {
547
+ this.markFirstToken();
548
+ textContent += delta.content;
549
+ this.emit(AgentEvent_1.AgentEvent.CHUNK, delta.content);
550
+ yield { type: "text", content: delta.content };
551
+ }
552
+ if (delta.reasoning) {
553
+ this.markFirstToken();
554
+ // Accumulated as well as yielded: the assistant turn has to carry its
555
+ // reasoning back on the next request.
556
+ reasoningContent += delta.reasoning;
557
+ this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, delta.reasoning);
558
+ yield { type: "reasoning", content: delta.reasoning };
559
+ }
560
+ if (delta.reasoningDetails?.length) {
561
+ reasoningDetails = reasoningDetails.concat(delta.reasoningDetails);
562
+ }
563
+ if (delta.toolCalls) {
564
+ for (const tc of delta.toolCalls) {
565
+ const index = tc.index ?? 0;
566
+ if (!toolCallAcc.has(index)) {
567
+ toolCallAcc.set(index, { id: "", name: "", arguments: "" });
568
+ }
569
+ const acc = toolCallAcc.get(index);
570
+ if (tc.id)
571
+ acc.id = tc.id;
572
+ if (tc.function?.name)
573
+ acc.name += tc.function.name;
574
+ if (tc.function?.arguments)
575
+ acc.arguments += tc.function.arguments;
576
+ }
577
+ }
543
578
  }
544
- if (delta.reasoning) {
545
- this.markFirstToken();
546
- // Accumulated as well as yielded: the assistant turn has to carry its
547
- // reasoning back on the next request.
548
- reasoningContent += delta.reasoning;
549
- this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, delta.reasoning);
550
- yield { type: "reasoning", content: delta.reasoning };
579
+ // Before any throw below, so a turn that failed part way still reports what
580
+ // it spent.
581
+ if (streamUsage)
582
+ this.accumulateUsage(this.parseUsageObject(streamUsage));
583
+ // The SDK's stream iterator stops yielding on abort rather than throwing, so
584
+ // without this an interrupted stream would look like a short but complete
585
+ // turn writing partial text to history and emitting DONE.
586
+ (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
587
+ if (streamError) {
588
+ throw new AgentError_1.ApiError(`OpenRouter stream error: ${unwrapOpenRouterMessage(streamError, streamError.message ?? "no message")}`, streamError.code, streamError);
551
589
  }
552
- if (delta.reasoningDetails?.length) {
553
- reasoningDetails = reasoningDetails.concat(delta.reasoningDetails);
590
+ if (finishReason === "length") {
591
+ const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
592
+ this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
593
+ throw error;
554
594
  }
555
- if (delta.toolCalls) {
556
- for (const tc of delta.toolCalls) {
557
- const index = tc.index ?? 0;
558
- if (!toolCallAcc.has(index)) {
559
- toolCallAcc.set(index, { id: "", name: "", arguments: "" });
560
- }
561
- const acc = toolCallAcc.get(index);
562
- if (tc.id)
563
- acc.id = tc.id;
564
- if (tc.function?.name)
565
- acc.name += tc.function.name;
566
- if (tc.function?.arguments)
567
- acc.arguments += tc.function.arguments;
595
+ const assistantMessage = {
596
+ role: "assistant",
597
+ content: textContent || null,
598
+ reasoning: reasoningContent || null,
599
+ reasoningDetails,
600
+ };
601
+ if (finishReason === "tool_calls" && toolCallAcc.size > 0) {
602
+ // As in handleResponse(): bail out before the assistant turn is written,
603
+ // so a cancelled run leaves no unanswered tool call in history.
604
+ (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
605
+ const toolCalls = Array.from(toolCallAcc.entries())
606
+ .sort(([a], [b]) => a - b)
607
+ .map(([, tc]) => ({
608
+ id: tc.id,
609
+ type: "function",
610
+ function: { name: tc.name, arguments: tc.arguments },
611
+ }));
612
+ this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
613
+ this.currentToolCallCount += toolCalls.length;
614
+ this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage({
615
+ ...assistantMessage,
616
+ toolCalls,
617
+ }));
618
+ committed = true;
619
+ const toolResults = await this.handleToolCalls(toolCalls, options);
620
+ for (const result of toolResults) {
621
+ this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
568
622
  }
623
+ yield* this.streamTurn(options);
624
+ }
625
+ else {
626
+ this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(assistantMessage));
627
+ committed = true;
628
+ this.emit(AgentEvent_1.AgentEvent.DONE, { content: textContent }, this.lastTokenUsage);
629
+ this.completeViz(textContent);
569
630
  }
570
631
  }
571
- // Before any throw below, so a turn that failed part way still reports what
572
- // it spent.
573
- if (streamUsage)
574
- this.accumulateUsage(this.parseUsageObject(streamUsage));
575
- // The SDK's stream iterator stops yielding on abort rather than throwing, so
576
- // without this an interrupted stream would look like a short but complete
577
- // turn — writing partial text to history and emitting DONE.
578
- (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
579
- if (streamError) {
580
- throw new AgentError_1.ApiError(`OpenRouter stream error: ${unwrapOpenRouterMessage(streamError, streamError.message ?? "no message")}`, streamError.code, streamError);
581
- }
582
- if (finishReason === "length") {
583
- const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
584
- this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
632
+ catch (error) {
633
+ failure = error;
585
634
  throw error;
586
635
  }
587
- const assistantMessage = {
588
- role: "assistant",
589
- content: textContent || null,
590
- reasoning: reasoningContent || null,
591
- reasoningDetails,
592
- };
593
- if (finishReason === "tool_calls" && toolCallAcc.size > 0) {
594
- // As in handleResponse(): bail out before the assistant turn is written,
595
- // so a cancelled run leaves no unanswered tool call in history.
596
- (0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
597
- const toolCalls = Array.from(toolCallAcc.entries())
598
- .sort(([a], [b]) => a - b)
599
- .map(([, tc]) => ({
600
- id: tc.id,
601
- type: "function",
602
- function: { name: tc.name, arguments: tc.arguments },
603
- }));
604
- this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
605
- this.currentToolCallCount += toolCalls.length;
606
- this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage({
607
- ...assistantMessage,
608
- toolCalls,
609
- }));
610
- const toolResults = await this.handleToolCalls(toolCalls, options);
611
- for (const result of toolResults) {
612
- this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
636
+ finally {
637
+ if (!committed) {
638
+ this.capturePartialTurn({
639
+ text: textContent,
640
+ reasoning: reasoningContent,
641
+ toolCalls: Array.from(toolCallAcc.entries())
642
+ .sort(([a], [b]) => a - b)
643
+ .map(([, tc]) => ({
644
+ id: tc.id,
645
+ name: tc.name,
646
+ arguments: tc.arguments,
647
+ })),
648
+ reason: this.partialTurnReason(failure, options?.signal),
649
+ error: failure,
650
+ meta: reasoningDetails.length ? { reasoningDetails } : undefined,
651
+ });
613
652
  }
614
- yield* this.streamTurn(options);
615
- }
616
- else {
617
- this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(assistantMessage));
618
- this.emit(AgentEvent_1.AgentEvent.DONE, { content: textContent }, this.lastTokenUsage);
619
- this.completeViz(textContent);
620
653
  }
621
654
  }
622
655
  async handleToolCalls(toolCalls, options) {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Display-only normalization for streamed reasoning text.
3
+ *
4
+ * Reasoning models routed through OpenRouter (and other OpenAI-compatible
5
+ * backends) sometimes stream `reasoning`/`reasoning_content` whose formatting
6
+ * is far noisier than the model's final answer — GLM-series models in
7
+ * particular emit heavily bulleted chain-of-thought with a blank line between
8
+ * almost every point, and some provider routes break tokens one phrase per
9
+ * line instead of wrapping normally. That formatting comes from the model
10
+ * itself (verified against live OpenRouter streams — the SDK and this
11
+ * library's accumulation just concatenate deltas verbatim), so it can't be
12
+ * fixed at the source.
13
+ *
14
+ * This is display-only: never apply it to the string that gets stored in
15
+ * history or replayed to the provider on the next turn (DeepSeek/GLM require
16
+ * that text back byte-for-byte, see {@link OpenAICompatibleAgent.streamTurn}).
17
+ * Apply it only where you render or log a `reasoning` chunk for a human.
18
+ */
19
+ export interface CollapseReasoningWhitespaceOptions {
20
+ /** Collapse runs of 3+ newlines down to a single blank line. Default `true`. */
21
+ collapseBlankLines?: boolean;
22
+ /**
23
+ * Merge consecutive non-blank lines into one, joined by a space — for
24
+ * providers that stream reasoning broken one word or phrase per line. A
25
+ * line starting a markdown block (list item, heading, blockquote) is never
26
+ * merged into the line before it, so intentional structure survives.
27
+ * Off by default since it can also merge genuinely short paragraphs;
28
+ * enable it for the specific model/provider you've seen this on.
29
+ */
30
+ collapseLineWraps?: boolean;
31
+ }
32
+ /**
33
+ * Collapses excess linebreaks in reasoning text for display, leaving the
34
+ * original string untouched for anything that needs it verbatim.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * agent.on(AgentEvent.REASONING_CHUNK, (delta) => {
39
+ * process.stdout.write(collapseReasoningWhitespace(delta));
40
+ * });
41
+ * ```
42
+ */
43
+ export declare function collapseReasoningWhitespace(text: string, options?: CollapseReasoningWhitespaceOptions): string;
44
+ //# sourceMappingURL=reasoning-text.d.ts.map
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collapseReasoningWhitespace = collapseReasoningWhitespace;
4
+ const MARKDOWN_BLOCK_START = /^\s*(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|>)/;
5
+ /**
6
+ * Collapses excess linebreaks in reasoning text for display, leaving the
7
+ * original string untouched for anything that needs it verbatim.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * agent.on(AgentEvent.REASONING_CHUNK, (delta) => {
12
+ * process.stdout.write(collapseReasoningWhitespace(delta));
13
+ * });
14
+ * ```
15
+ */
16
+ function collapseReasoningWhitespace(text, options = {}) {
17
+ const { collapseBlankLines = true, collapseLineWraps = false } = options;
18
+ let result = text;
19
+ if (collapseBlankLines) {
20
+ result = result.replace(/\n{3,}/g, "\n\n");
21
+ }
22
+ if (collapseLineWraps) {
23
+ const lines = result.split("\n");
24
+ const merged = [];
25
+ for (const line of lines) {
26
+ const prev = merged[merged.length - 1];
27
+ const canMergeIntoPrev = prev !== undefined &&
28
+ prev.trim().length > 0 &&
29
+ line.trim().length > 0 &&
30
+ !MARKDOWN_BLOCK_START.test(line) &&
31
+ !MARKDOWN_BLOCK_START.test(prev);
32
+ if (canMergeIntoPrev) {
33
+ merged[merged.length - 1] = `${prev.replace(/\s+$/, "")} ${line.replace(/^\s+/, "")}`;
34
+ }
35
+ else {
36
+ merged.push(line);
37
+ }
38
+ }
39
+ result = merged.join("\n");
40
+ }
41
+ return result;
42
+ }
43
+ //# sourceMappingURL=reasoning-text.js.map
package/dist/core.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./agents/AgentConfig";
4
4
  export * from "./agents/AgentEvent";
5
5
  export * from "./agents/errors/AgentError";
6
6
  export * from "./agents/cancellation";
7
+ export * from "./agents/reasoning-text";
7
8
  export * from "./history/History";
8
9
  export * from "./history/types";
9
10
  export * from "./graph/AgentGraph";
package/dist/core.js CHANGED
@@ -23,6 +23,7 @@ __exportStar(require("./agents/AgentConfig"), exports);
23
23
  __exportStar(require("./agents/AgentEvent"), exports);
24
24
  __exportStar(require("./agents/errors/AgentError"), exports);
25
25
  __exportStar(require("./agents/cancellation"), exports);
26
+ __exportStar(require("./agents/reasoning-text"), exports);
26
27
  // History
27
28
  __exportStar(require("./history/History"), exports);
28
29
  __exportStar(require("./history/types"), exports);
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from "./agents/AgentConfig";
19
19
  export * from "./agents/AgentEvent";
20
20
  export * from "./agents/errors/AgentError";
21
21
  export * from "./agents/cancellation";
22
+ export * from "./agents/reasoning-text";
22
23
  export * from "./history/History";
23
24
  export * from "./history/types";
24
25
  export { anthropicTransformer, openAiTransformer, mistralTransformer, geminiTransformer, ollamaTransformer, chatCompletionsTransformer, openRouterTransformer, } from "./history/transformers";
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ __exportStar(require("./agents/AgentConfig"), exports);
46
46
  __exportStar(require("./agents/AgentEvent"), exports);
47
47
  __exportStar(require("./agents/errors/AgentError"), exports);
48
48
  __exportStar(require("./agents/cancellation"), exports);
49
+ __exportStar(require("./agents/reasoning-text"), exports);
49
50
  // History
50
51
  __exportStar(require("./history/History"), exports);
51
52
  __exportStar(require("./history/types"), exports);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentionai/agents",
3
3
  "author": "Laurent Zuijdwijk",
4
- "version": "1.11.0",
4
+ "version": "1.12.0",
5
5
  "description": "Agent Library",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -118,7 +118,7 @@
118
118
  "@lancedb/lancedb": "^0.23.0",
119
119
  "@mistralai/mistralai": "^1.13.0",
120
120
  "@modelcontextprotocol/sdk": "^1.30.0",
121
- "@openrouter/sdk": "^1.2.37",
121
+ "@openrouter/sdk": "^1.2.106",
122
122
  "@types/jest": "^29.5.0",
123
123
  "@types/node": "^18.15.11",
124
124
  "apache-arrow": "^18.1.0",
@@ -150,7 +150,7 @@
150
150
  "@lancedb/lancedb": "^0.23.0",
151
151
  "@mistralai/mistralai": "^1.13.0",
152
152
  "@modelcontextprotocol/sdk": "^1.26.0",
153
- "@openrouter/sdk": "^1.2.29",
153
+ "@openrouter/sdk": "^1.2.106",
154
154
  "apache-arrow": "^18.0.0",
155
155
  "ollama": "^0.5.18",
156
156
  "openai": "^6.16.0",