@oh-my-pi/pi-agent-core 4.5.0 → 4.6.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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/agent.ts +137 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-agent-core",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -13,8 +13,8 @@
13
13
  "test": "vitest --run"
14
14
  },
15
15
  "dependencies": {
16
- "@oh-my-pi/pi-ai": "4.5.0",
17
- "@oh-my-pi/pi-tui": "4.5.0"
16
+ "@oh-my-pi/pi-ai": "4.6.0",
17
+ "@oh-my-pi/pi-tui": "4.6.0"
18
18
  },
19
19
  "keywords": [
20
20
  "ai",
package/src/agent.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import {
7
+ type AssistantMessage,
7
8
  type CursorExecHandlers,
8
9
  type CursorToolResultHandler,
9
10
  getModel,
@@ -106,6 +107,12 @@ export interface AgentOptions {
106
107
  cursorOnToolResult?: CursorToolResultHandler;
107
108
  }
108
109
 
110
+ /** Buffered Cursor tool result with text position at time of call */
111
+ interface CursorToolResultEntry {
112
+ toolResult: ToolResultMessage;
113
+ textLengthAtCall: number;
114
+ }
115
+
109
116
  export class Agent {
110
117
  private _state: AgentState = {
111
118
  systemPrompt: "",
@@ -138,6 +145,9 @@ export class Agent {
138
145
  private runningPrompt?: Promise<void>;
139
146
  private resolveRunningPrompt?: () => void;
140
147
 
148
+ /** Buffered Cursor tool results with text length at time of call (for correct ordering) */
149
+ private _cursorToolResultBuffer: CursorToolResultEntry[] = [];
150
+
141
151
  constructor(opts: AgentOptions = {}) {
142
152
  this._state = { ...this._state, ...opts.initialState };
143
153
  this.convertToLlm = opts.convertToLlm || defaultConvertToLlm;
@@ -418,6 +428,9 @@ export class Agent {
418
428
  this._state.streamMessage = null;
419
429
  this._state.error = undefined;
420
430
 
431
+ // Clear Cursor tool result buffer at start of each run
432
+ this._cursorToolResultBuffer = [];
433
+
421
434
  const reasoning = this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel;
422
435
 
423
436
  const context: AgentContext = {
@@ -438,8 +451,12 @@ export class Agent {
438
451
  }
439
452
  } catch {}
440
453
  }
441
- this.emitExternalEvent({ type: "message_start", message: finalMessage });
442
- this.emitExternalEvent({ type: "message_end", message: finalMessage });
454
+ // Buffer tool result with current text length for correct ordering later.
455
+ // Cursor executes tools server-side during streaming, so the assistant message
456
+ // already incorporates results. We buffer here and emit in correct order
457
+ // when the assistant message ends.
458
+ const textLength = this._getAssistantTextLength(this._state.streamMessage);
459
+ this._cursorToolResultBuffer.push({ toolResult: finalMessage, textLengthAtCall: textLength });
443
460
  return finalMessage;
444
461
  }
445
462
  : undefined;
@@ -508,6 +525,12 @@ export class Agent {
508
525
 
509
526
  case "message_end":
510
527
  partial = null;
528
+ // Check if this is an assistant message with buffered Cursor tool results.
529
+ // If so, split the message to emit tool results at the correct position.
530
+ if (event.message.role === "assistant" && this._cursorToolResultBuffer.length > 0) {
531
+ this._emitCursorSplitAssistantMessage(event.message as AssistantMessage);
532
+ continue; // Skip default emit - split method handles everything
533
+ }
511
534
  this._state.streamMessage = null;
512
535
  this.appendMessage(event.message);
513
536
  break;
@@ -597,4 +620,116 @@ export class Agent {
597
620
  listener(e);
598
621
  }
599
622
  }
623
+
624
+ /** Calculate total text length from an assistant message's content blocks */
625
+ private _getAssistantTextLength(message: AgentMessage | null): number {
626
+ if (!message || message.role !== "assistant" || !Array.isArray(message.content)) {
627
+ return 0;
628
+ }
629
+ let length = 0;
630
+ for (const block of message.content) {
631
+ if (block.type === "text") {
632
+ length += (block as TextContent).text.length;
633
+ }
634
+ }
635
+ return length;
636
+ }
637
+
638
+ /**
639
+ * Emit a Cursor assistant message split around tool results.
640
+ * This fixes the ordering issue where tool results appear after the full explanation.
641
+ *
642
+ * Output order: Assistant(preamble) -> ToolResults -> Assistant(continuation)
643
+ */
644
+ private _emitCursorSplitAssistantMessage(assistantMessage: AssistantMessage): void {
645
+ const buffer = this._cursorToolResultBuffer;
646
+ this._cursorToolResultBuffer = [];
647
+
648
+ if (buffer.length === 0) {
649
+ // No tool results, emit normally
650
+ this._state.streamMessage = null;
651
+ this.appendMessage(assistantMessage);
652
+ this.emit({ type: "message_end", message: assistantMessage });
653
+ return;
654
+ }
655
+
656
+ // Find the split point: minimum text length at first tool call
657
+ const splitPoint = Math.min(...buffer.map((r) => r.textLengthAtCall));
658
+
659
+ // Extract text content from assistant message
660
+ const content = assistantMessage.content;
661
+ let fullText = "";
662
+ for (const block of content) {
663
+ if (block.type === "text") {
664
+ fullText += block.text;
665
+ }
666
+ }
667
+
668
+ // If no text or split point is 0 or at/past end, don't split
669
+ if (fullText.length === 0 || splitPoint <= 0 || splitPoint >= fullText.length) {
670
+ // Emit assistant message first, then tool results (original behavior but with buffered results)
671
+ this._state.streamMessage = null;
672
+ this.appendMessage(assistantMessage);
673
+ this.emit({ type: "message_end", message: assistantMessage });
674
+
675
+ // Emit buffered tool results
676
+ for (const { toolResult } of buffer) {
677
+ this.emit({ type: "message_start", message: toolResult });
678
+ this.appendMessage(toolResult);
679
+ this.emit({ type: "message_end", message: toolResult });
680
+ }
681
+ return;
682
+ }
683
+
684
+ // Split the text
685
+ const preambleText = fullText.slice(0, splitPoint);
686
+ const continuationText = fullText.slice(splitPoint);
687
+
688
+ // Create preamble message (text before tools)
689
+ const preambleContent = content.map((block) => {
690
+ if (block.type === "text") {
691
+ return { ...block, text: preambleText };
692
+ }
693
+ return block;
694
+ });
695
+ const preambleMessage: AssistantMessage = {
696
+ ...assistantMessage,
697
+ content: preambleContent,
698
+ };
699
+
700
+ // Emit preamble
701
+ this._state.streamMessage = null;
702
+ this.appendMessage(preambleMessage);
703
+ this.emit({ type: "message_end", message: preambleMessage });
704
+
705
+ // Emit buffered tool results
706
+ for (const { toolResult } of buffer) {
707
+ this.emit({ type: "message_start", message: toolResult });
708
+ this.appendMessage(toolResult);
709
+ this.emit({ type: "message_end", message: toolResult });
710
+ }
711
+
712
+ // Emit continuation message (text after tools) if non-empty
713
+ const trimmedContinuation = continuationText.trim();
714
+ if (trimmedContinuation.length > 0) {
715
+ // Create continuation message with only text content (no thinking/toolCalls)
716
+ const continuationContent: TextContent[] = [{ type: "text", text: continuationText }];
717
+ const continuationMessage: AssistantMessage = {
718
+ ...assistantMessage,
719
+ content: continuationContent,
720
+ // Zero out usage for continuation since it's part of same response
721
+ usage: {
722
+ input: 0,
723
+ output: 0,
724
+ cacheRead: 0,
725
+ cacheWrite: 0,
726
+ totalTokens: 0,
727
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
728
+ },
729
+ };
730
+ this.emit({ type: "message_start", message: continuationMessage });
731
+ this.appendMessage(continuationMessage);
732
+ this.emit({ type: "message_end", message: continuationMessage });
733
+ }
734
+ }
600
735
  }