@oh-my-pi/pi-agent-core 4.4.9 → 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.
- package/package.json +3 -3
- 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.
|
|
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.
|
|
17
|
-
"@oh-my-pi/pi-tui": "4.
|
|
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
|
-
|
|
442
|
-
|
|
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
|
}
|