@oh-my-pi/pi-agent-core 17.0.1 → 17.0.3
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 +7 -0
- package/dist/types/agent-loop.d.ts +8 -1
- package/dist/types/agent.d.ts +2 -0
- package/package.json +9 -9
- package/src/agent-loop.ts +27 -20
- package/src/agent.ts +75 -14
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>;
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "17.0.3",
|
|
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.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
42
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
43
|
-
"@oh-my-pi/snapcompact": "17.0.
|
|
38
|
+
"@oh-my-pi/pi-ai": "17.0.3",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "17.0.3",
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.0.3",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.0.3",
|
|
42
|
+
"@oh-my-pi/pi-wire": "17.0.3",
|
|
43
|
+
"@oh-my-pi/snapcompact": "17.0.3",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@opentelemetry/context-async-hooks": "^2.
|
|
48
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
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
|
|
2296
|
-
* never invoked locally.
|
|
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
|
|
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
|
-
|
|
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.#
|
|
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.
|
|
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
|
|
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
|
-
|
|
1291
|
-
? {
|
|
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 (
|
|
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
|
-
|
|
1321
|
-
this.#
|
|
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;
|