@oh-my-pi/pi-agent-core 17.0.1 → 17.0.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.2] - 2026-07-17
6
+
7
+ ### Fixed
8
+
9
+ - Improved error visibility in interactive clients by surfacing provider stream failures through the assistant message lifecycle, preventing silent loading spinners.
10
+ - Fixed an issue where Cursor provider contexts omitted host-supplied MCP tools from main and side-channel requests.
11
+
5
12
  ## [17.0.0] - 2026-07-15
6
13
 
7
14
  ### Breaking Changes
@@ -2,7 +2,7 @@
2
2
  * Agent loop that works with AgentMessage throughout.
3
3
  * Transforms to Message[] only at the LLM call boundary.
4
4
  */
5
- import { type Context, EventStream } from "@oh-my-pi/pi-ai";
5
+ import { type AssistantMessage, type Context, EventStream, type ToolResultMessage } from "@oh-my-pi/pi-ai";
6
6
  import { type Dialect } from "@oh-my-pi/pi-ai/dialect";
7
7
  import { type AgentRunCoverage, type AgentRunSummary } from "./run-collector.js";
8
8
  import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types.js";
@@ -111,3 +111,10 @@ export interface SyntheticToolResultDetails {
111
111
  executed: false;
112
112
  upstreamError?: string;
113
113
  }
114
+ /**
115
+ * Create the persisted synthetic result for a tool call that was emitted by
116
+ * the assistant but never invoked locally.
117
+ */
118
+ export declare function createSyntheticToolResultMessage(toolCall: Extract<AssistantMessage["content"][number], {
119
+ type: "toolCall";
120
+ }>, reason: "aborted" | "error" | "skipped" | "length", errorMessage?: string): ToolResultMessage<SyntheticToolResultDetails>;
@@ -157,6 +157,8 @@ export interface AgentOptions {
157
157
  * Cursor exec handlers for local tool execution.
158
158
  */
159
159
  cursorExecHandlers?: CursorExecHandlers;
160
+ /** Additional tools Cursor executes through its MCP request-context bridge, resolved before each provider call. */
161
+ getCursorTools?: () => AgentTool[];
160
162
  /**
161
163
  * Cursor tool result callback for exec tool responses.
162
164
  */
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": "17.0.1",
4
+ "version": "17.0.2",
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,17 +35,17 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "17.0.1",
39
- "@oh-my-pi/pi-catalog": "17.0.1",
40
- "@oh-my-pi/pi-natives": "17.0.1",
41
- "@oh-my-pi/pi-utils": "17.0.1",
42
- "@oh-my-pi/pi-wire": "17.0.1",
43
- "@oh-my-pi/snapcompact": "17.0.1",
38
+ "@oh-my-pi/pi-ai": "17.0.2",
39
+ "@oh-my-pi/pi-catalog": "17.0.2",
40
+ "@oh-my-pi/pi-natives": "17.0.2",
41
+ "@oh-my-pi/pi-utils": "17.0.2",
42
+ "@oh-my-pi/pi-wire": "17.0.2",
43
+ "@oh-my-pi/snapcompact": "17.0.2",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@opentelemetry/context-async-hooks": "^2.7.1",
48
- "@opentelemetry/sdk-trace-base": "^2.7.1",
47
+ "@opentelemetry/context-async-hooks": "^2.9.0",
48
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
49
49
  "@types/bun": "^1.3.14"
50
50
  },
51
51
  "engines": {
package/src/agent-loop.ts CHANGED
@@ -2292,18 +2292,14 @@ function syntheticDetailsFor(
2292
2292
  }
2293
2293
 
2294
2294
  /**
2295
- * Create a tool result for a tool call that was emitted by the assistant but
2296
- * never invoked locally. Maintains the tool_use / tool_result pairing the
2297
- * provider API requires, and tags {@link SyntheticToolResultDetails} so
2298
- * consumers can distinguish this from a real local tool failure without
2299
- * string-matching the content (#4321).
2295
+ * Create the persisted synthetic result for a tool call that was emitted by
2296
+ * the assistant but never invoked locally.
2300
2297
  */
2301
- function createAbortedToolResult(
2298
+ export function createSyntheticToolResultMessage(
2302
2299
  toolCall: Extract<AssistantMessage["content"][number], { type: "toolCall" }>,
2303
- stream: EventStream<AgentEvent, AgentMessage[]>,
2304
2300
  reason: "aborted" | "error" | "skipped" | "length",
2305
2301
  errorMessage?: string,
2306
- ): ToolResultMessage {
2302
+ ): ToolResultMessage<SyntheticToolResultDetails> {
2307
2303
  const message =
2308
2304
  reason === "aborted"
2309
2305
  ? "Tool execution was aborted"
@@ -2313,9 +2309,31 @@ function createAbortedToolResult(
2313
2309
  ? "Tool call was not executed because the assistant ended its turn"
2314
2310
  : "Tool call was not executed because the provider stream ended with an error before the tool could run";
2315
2311
  const details = syntheticDetailsFor(reason, errorMessage);
2316
- const result: AgentToolResult<SyntheticToolResultDetails> = {
2312
+ return {
2313
+ role: "toolResult",
2314
+ toolCallId: toolCall.id,
2315
+ toolName: toolCall.name,
2317
2316
  content: [{ type: "text", text: errorMessage ? `${message}: ${errorMessage}` : `${message}.` }],
2318
2317
  details,
2318
+ isError: true,
2319
+ timestamp: Date.now(),
2320
+ };
2321
+ }
2322
+
2323
+ /**
2324
+ * Create and emit a tool result for a tool call that was emitted by the
2325
+ * assistant but never invoked locally.
2326
+ */
2327
+ function createAbortedToolResult(
2328
+ toolCall: Extract<AssistantMessage["content"][number], { type: "toolCall" }>,
2329
+ stream: EventStream<AgentEvent, AgentMessage[]>,
2330
+ reason: "aborted" | "error" | "skipped" | "length",
2331
+ errorMessage?: string,
2332
+ ): ToolResultMessage {
2333
+ const toolResultMessage = createSyntheticToolResultMessage(toolCall, reason, errorMessage);
2334
+ const result: AgentToolResult<SyntheticToolResultDetails> = {
2335
+ content: toolResultMessage.content,
2336
+ details: toolResultMessage.details,
2319
2337
  };
2320
2338
 
2321
2339
  stream.push({
@@ -2332,17 +2350,6 @@ function createAbortedToolResult(
2332
2350
  result,
2333
2351
  isError: true,
2334
2352
  });
2335
-
2336
- const toolResultMessage: ToolResultMessage<SyntheticToolResultDetails> = {
2337
- role: "toolResult",
2338
- toolCallId: toolCall.id,
2339
- toolName: toolCall.name,
2340
- content: result.content,
2341
- details,
2342
- isError: true,
2343
- timestamp: Date.now(),
2344
- };
2345
-
2346
2353
  stream.push({ type: "message_start", message: toolResultMessage });
2347
2354
  stream.push({ type: "message_end", message: toolResultMessage });
2348
2355
 
package/src/agent.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  abortReasonText,
32
32
  agentLoop,
33
33
  agentLoopContinue,
34
+ createSyntheticToolResultMessage,
34
35
  normalizeMessagesForProvider,
35
36
  normalizeTools,
36
37
  resolveOwnedDialectFromEnv,
@@ -64,12 +65,6 @@ function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
64
65
  });
65
66
  }
66
67
 
67
- const ANTHROPIC_OUTPUT_BLOCKED_PREFIX = "Output blocked by conten";
68
-
69
- function isAnthropicOutputBlockedError(message: string): boolean {
70
- return message.includes(ANTHROPIC_OUTPUT_BLOCKED_PREFIX);
71
- }
72
-
73
68
  function refreshToolChoiceForActiveTools(
74
69
  toolChoice: ToolChoice | undefined,
75
70
  tools: AgentContext["tools"] = [],
@@ -267,6 +262,8 @@ export interface AgentOptions {
267
262
  * Cursor exec handlers for local tool execution.
268
263
  */
269
264
  cursorExecHandlers?: CursorExecHandlers;
265
+ /** Additional tools Cursor executes through its MCP request-context bridge, resolved before each provider call. */
266
+ getCursorTools?: () => AgentTool[];
270
267
 
271
268
  /**
272
269
  * Cursor tool result callback for exec tool responses.
@@ -368,6 +365,7 @@ export class Agent {
368
365
  #maxRetryDelayMs?: number;
369
366
  #getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
370
367
  #cursorExecHandlers?: CursorExecHandlers;
368
+ #getCursorTools?: () => AgentTool[];
371
369
  #cursorOnToolResult?: CursorToolResultHandler;
372
370
  #cwd?: string;
373
371
  #cwdResolver?: () => string | undefined;
@@ -450,6 +448,7 @@ export class Agent {
450
448
  this.#onSseEvent = opts.onSseEvent;
451
449
  this.#getToolContext = opts.getToolContext;
452
450
  this.#cursorExecHandlers = opts.cursorExecHandlers;
451
+ this.#getCursorTools = opts.getCursorTools;
453
452
  this.#cursorOnToolResult = opts.cursorOnToolResult;
454
453
  this.#cwd = opts.cwd;
455
454
  this.#cwdResolver = opts.cwdResolver;
@@ -694,6 +693,22 @@ export class Agent {
694
693
  this.#appendOnlyContext = manager;
695
694
  }
696
695
 
696
+ #toolsForModel(model: Model): AgentTool[] {
697
+ if (model.api !== "cursor-agent" || !this.#getCursorTools) return this.#state.tools;
698
+ const cursorTools = this.#getCursorTools();
699
+ if (cursorTools.length === 0) return this.#state.tools;
700
+
701
+ const names = new Set(this.#state.tools.map(tool => tool.name));
702
+ let merged: AgentTool[] | undefined;
703
+ for (const tool of cursorTools) {
704
+ if (names.has(tool.name)) continue;
705
+ merged ??= this.#state.tools.slice();
706
+ merged.push(tool);
707
+ names.add(tool.name);
708
+ }
709
+ return merged ?? this.#state.tools;
710
+ }
711
+
697
712
  /**
698
713
  * Assemble the provider Context for a side-channel (no-loop) request, mirroring
699
714
  * the main loop's prefix (system + normalized tools) so it shares the prompt
@@ -718,7 +733,7 @@ export class Agent {
718
733
  const tools = ownedDialect
719
734
  ? []
720
735
  : (normalizeTools(
721
- this.#state.tools,
736
+ this.#toolsForModel(model),
722
737
  this.#intentTracing,
723
738
  preferredDialect(model.id),
724
739
  this.#pruneToolDescriptions,
@@ -1152,7 +1167,7 @@ export class Agent {
1152
1167
  await Bun.sleep(0);
1153
1168
  }
1154
1169
  context.systemPrompt = this.#state.systemPrompt;
1155
- context.tools = this.#state.tools;
1170
+ context.tools = this.#toolsForModel(this.#state.model ?? model);
1156
1171
  },
1157
1172
  cursorExecHandlers: this.#cursorExecHandlers,
1158
1173
  cursorOnToolResult,
@@ -1205,6 +1220,7 @@ export class Agent {
1205
1220
  };
1206
1221
 
1207
1222
  let partial: AgentMessage | null = null;
1223
+ const completedToolCallIds = new Set<string>();
1208
1224
 
1209
1225
  try {
1210
1226
  const stream = messages
@@ -1222,6 +1238,9 @@ export class Agent {
1222
1238
  case "message_update":
1223
1239
  partial = event.message;
1224
1240
  this.#state.streamMessage = event.message;
1241
+ if (event.assistantMessageEvent.type === "toolcall_end") {
1242
+ completedToolCallIds.add(event.assistantMessageEvent.toolCall.id);
1243
+ }
1225
1244
  break;
1226
1245
 
1227
1246
  case "message_end":
@@ -1283,12 +1302,22 @@ export class Agent {
1283
1302
  : err instanceof Error
1284
1303
  ? err.message
1285
1304
  : String(err);
1286
- const shouldEmitVisibleOutputBlockedError = !stoppedForAbort && isAnthropicOutputBlockedError(errorMessage);
1305
+ const shouldEmitVisibleError = !stoppedForAbort;
1287
1306
  const assistantPartial = partial?.role === "assistant" ? partial : undefined;
1288
1307
  const hadAssistantStart = assistantPartial !== undefined;
1308
+ const bufferedCursorResults = this.#cursorToolResultBuffer.map(({ toolResult }) => toolResult);
1309
+ const retainedToolCallIds = new Set(completedToolCallIds);
1310
+ for (const { toolCallId } of bufferedCursorResults) retainedToolCallIds.add(toolCallId);
1289
1311
  const errorMsg: AssistantMessage =
1290
- shouldEmitVisibleOutputBlockedError && assistantPartial
1291
- ? { ...assistantPartial, stopReason: "error", errorMessage }
1312
+ shouldEmitVisibleError && assistantPartial
1313
+ ? {
1314
+ ...assistantPartial,
1315
+ content: assistantPartial.content.filter(
1316
+ block => block.type !== "toolCall" || retainedToolCallIds.has(block.id),
1317
+ ),
1318
+ stopReason: "error",
1319
+ errorMessage,
1320
+ }
1292
1321
  : {
1293
1322
  role: "assistant",
1294
1323
  content: [{ type: "text", text: "" }],
@@ -1308,7 +1337,7 @@ export class Agent {
1308
1337
  timestamp: Date.now(),
1309
1338
  };
1310
1339
 
1311
- if (shouldEmitVisibleOutputBlockedError) {
1340
+ if (shouldEmitVisibleError) {
1312
1341
  if (!hadAssistantStart) {
1313
1342
  this.#state.streamMessage = errorMsg;
1314
1343
  this.#emit({ type: "message_start", message: errorMsg });
@@ -1317,8 +1346,40 @@ export class Agent {
1317
1346
  this.appendMessage(errorMsg);
1318
1347
  this.#state.error = errorMessage;
1319
1348
  this.#emit({ type: "message_end", message: errorMsg });
1320
- this.#emit({ type: "turn_end", message: errorMsg, toolResults: [] });
1321
- this.#emit({ type: "agent_end", messages: [errorMsg] });
1349
+ const toolResults: ToolResultMessage[] = [];
1350
+ this.#cursorToolResultBuffer = [];
1351
+ const bufferedCursorToolCallIds = new Set(bufferedCursorResults.map(({ toolCallId }) => toolCallId));
1352
+ for (const toolResult of bufferedCursorResults) {
1353
+ this.appendMessage(toolResult);
1354
+ this.#emit({ type: "message_start", message: toolResult });
1355
+ this.#emit({ type: "message_end", message: toolResult });
1356
+ toolResults.push(toolResult);
1357
+ }
1358
+ for (const block of errorMsg.content) {
1359
+ if (block.type !== "toolCall") continue;
1360
+ if (bufferedCursorToolCallIds.has(block.id)) continue;
1361
+ const toolResult = createSyntheticToolResultMessage(block, "error", errorMessage);
1362
+ this.#emit({
1363
+ type: "tool_execution_start",
1364
+ toolCallId: block.id,
1365
+ toolName: block.name,
1366
+ args: block.arguments,
1367
+ intent: block.intent,
1368
+ });
1369
+ this.#emit({
1370
+ type: "tool_execution_end",
1371
+ toolCallId: block.id,
1372
+ toolName: block.name,
1373
+ result: { content: toolResult.content, details: toolResult.details },
1374
+ isError: true,
1375
+ });
1376
+ this.appendMessage(toolResult);
1377
+ this.#emit({ type: "message_start", message: toolResult });
1378
+ this.#emit({ type: "message_end", message: toolResult });
1379
+ toolResults.push(toolResult);
1380
+ }
1381
+ this.#emit({ type: "turn_end", message: errorMsg, toolResults });
1382
+ this.#emit({ type: "agent_end", messages: [errorMsg, ...toolResults] });
1322
1383
  } else {
1323
1384
  this.appendMessage(errorMsg);
1324
1385
  this.#state.error = errorMessage;