@oh-my-pi/pi-agent-core 15.5.11 → 15.5.12

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 +4 -4
  2. package/src/agent-loop.ts +36 -5
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-agent-core",
4
- "version": "15.5.11",
4
+ "version": "15.5.12",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -35,9 +35,9 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "15.5.11",
39
- "@oh-my-pi/pi-natives": "15.5.11",
40
- "@oh-my-pi/pi-utils": "15.5.11",
38
+ "@oh-my-pi/pi-ai": "15.5.12",
39
+ "@oh-my-pi/pi-natives": "15.5.12",
40
+ "@oh-my-pi/pi-utils": "15.5.12",
41
41
  "@opentelemetry/api": "^1.9.0"
42
42
  },
43
43
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -562,9 +562,19 @@ async function runLoopBody(
562
562
  return;
563
563
  }
564
564
 
565
- // Check for tool calls
566
- const toolCalls = message.content.filter(c => c.type === "toolCall");
567
- hasMoreToolCalls = toolCalls.length > 0;
565
+ // Tool execution is gated on the model's *stop reason* (`toolUse`), not the
566
+ // mere presence of toolCall blocks. Anthropic's documented agentic loop runs
567
+ // tools "while stop_reason == tool_use" and exits on any other reason. With
568
+ // adaptive/interleaved thinking a turn can emit tool calls and then end
569
+ // naturally (`end_turn` → `stop`) when the model decides to wrap up — those
570
+ // calls are abandoned. Executing them and appending tool_results yields an
571
+ // invalid continuation (Anthropic rejects continuing an ended turn), which is
572
+ // what broke interleaved tool use. Providers set `toolUse` whenever they
573
+ // genuinely want tools run (Anthropic on `tool_use`; OpenAI-style providers
574
+ // promote `stop`→`toolUse` whenever tool-call blocks are emitted).
575
+ type ToolCallContent = Extract<AssistantMessage["content"][number], { type: "toolCall" }>;
576
+ const toolCalls = message.content.filter((c): c is ToolCallContent => c.type === "toolCall");
577
+ hasMoreToolCalls = message.stopReason === "toolUse" && toolCalls.length > 0;
568
578
 
569
579
  const toolResults: ToolResultMessage[] = [];
570
580
  if (hasMoreToolCalls) {
@@ -585,6 +595,22 @@ async function runLoopBody(
585
595
  currentContext.messages.push(result);
586
596
  newMessages.push(result);
587
597
  }
598
+ } else if (toolCalls.length > 0) {
599
+ // Model ended the turn (stopReason !== "toolUse") but left toolCall blocks
600
+ // behind. They were abandoned, so don't execute or continue — but pair each
601
+ // with a placeholder result to keep the tool_use/tool_result contract valid
602
+ // for any later request that replays this turn.
603
+ for (const toolCall of toolCalls) {
604
+ const result = createAbortedToolResult(toolCall, stream, "skipped");
605
+ currentContext.messages.push(result);
606
+ newMessages.push(result);
607
+ toolResults.push(result);
608
+ recordSkippedTool(telemetry, {
609
+ toolCallId: toolCall.id,
610
+ toolName: toolCall.name,
611
+ status: "skipped",
612
+ });
613
+ }
588
614
  }
589
615
 
590
616
  stream.push({ type: "turn_end", message, toolResults });
@@ -1241,10 +1267,15 @@ async function executeToolCalls(
1241
1267
  function createAbortedToolResult(
1242
1268
  toolCall: Extract<AssistantMessage["content"][number], { type: "toolCall" }>,
1243
1269
  stream: EventStream<AgentEvent, AgentMessage[]>,
1244
- reason: "aborted" | "error",
1270
+ reason: "aborted" | "error" | "skipped",
1245
1271
  errorMessage?: string,
1246
1272
  ): ToolResultMessage {
1247
- const message = reason === "aborted" ? "Tool execution was aborted" : "Tool execution failed due to an error";
1273
+ const message =
1274
+ reason === "aborted"
1275
+ ? "Tool execution was aborted"
1276
+ : reason === "skipped"
1277
+ ? "Tool call was not executed because the assistant ended its turn"
1278
+ : "Tool execution failed due to an error";
1248
1279
  const result: AgentToolResult<any> = {
1249
1280
  content: [{ type: "text", text: errorMessage ? `${message}: ${errorMessage}` : `${message}.` }],
1250
1281
  details: {},