@oh-my-pi/pi-agent-core 18.11.3 → 18.12.1
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,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.12.0] - 2026-10-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Cursor turns failing with `PROVIDER_ERROR` when a follow-up (a finished background job, a late diagnostic, a reminder) arrived while the previous turn was still streaming ([#15821](https://github.com/can1357/oh-my-pi/pull/15821) by [@robfeldmann](https://github.com/robfeldmann))
|
|
10
|
+
|
|
5
11
|
## [18.11.3] - 2026-10-16
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -58,11 +58,16 @@ export declare function truncateToolResultForSummary(text: string): string;
|
|
|
58
58
|
export declare function escapeSummaryBoundaryTags(text: string): string;
|
|
59
59
|
/**
|
|
60
60
|
* Serialize LLM messages as plain summary input without provider control tokens.
|
|
61
|
+
* `dropThinking` is the summarizer's resolved `classifier-cot-extraction` policy.
|
|
61
62
|
*/
|
|
62
|
-
export declare function serializeConversationForSummary(messages: Message[], dialect?: Dialect): string;
|
|
63
|
+
export declare function serializeConversationForSummary(messages: Message[], dialect?: Dialect, dropThinking?: boolean): string;
|
|
63
64
|
/**
|
|
64
65
|
* Serialize LLM messages to transcript text.
|
|
65
66
|
* Call convertToLlm() first to handle custom message types.
|
|
67
|
+
* `dropThinking` omits assistant reasoning from dialect transcripts: set it from
|
|
68
|
+
* the reader's resolved `classifier-cot-extraction` catalog policy (Claude's
|
|
69
|
+
* classifier refuses inputs that reproduce the model's own reasoning as text,
|
|
70
|
+
* "reasoning_extraction"). Reasoning is ephemeral and low-signal for a summary.
|
|
66
71
|
*/
|
|
67
|
-
export declare function serializeConversation(messages: Message[], dialect?: Dialect): string;
|
|
72
|
+
export declare function serializeConversation(messages: Message[], dialect?: Dialect, dropThinking?: boolean): string;
|
|
68
73
|
export declare const SUMMARIZATION_SYSTEM_PROMPT: string;
|
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": "18.
|
|
4
|
+
"version": "18.12.1",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@oh-my-pi/pi-ai": "18.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.12.1",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.12.1",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.12.1",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.12.1",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.12.1",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.12.1",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.
|
|
50
|
+
"@oh-my-pi/omptype": "18.12.1",
|
|
51
51
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
52
52
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
53
53
|
"@types/bun": "^1.3.14"
|
package/src/agent-loop.ts
CHANGED
|
@@ -76,7 +76,13 @@ import {
|
|
|
76
76
|
startInvokeAgentSpan,
|
|
77
77
|
} from "./telemetry";
|
|
78
78
|
import { validateAgentToolArguments } from "./tool-arguments";
|
|
79
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
createAdditionalContextMessage,
|
|
81
|
+
isNonBlankContext,
|
|
82
|
+
joinAdditionalContext,
|
|
83
|
+
TOOL_RESULT_ADDITIONAL_CONTEXT,
|
|
84
|
+
type ToolResultWithAdditionalContext,
|
|
85
|
+
} from "./tool-context";
|
|
80
86
|
import dsmlToolCallLeakPrompt from "./prompts/dsml-tool-call-leak.md" with { type: "text" };
|
|
81
87
|
import type {
|
|
82
88
|
AgentContext,
|
|
@@ -897,7 +903,11 @@ export function normalizeMessagesForProvider(
|
|
|
897
903
|
messages: Context["messages"],
|
|
898
904
|
model: AgentLoopConfig["model"],
|
|
899
905
|
): Context["messages"] {
|
|
900
|
-
|
|
906
|
+
// The anthropic and ollama transports apply `stripThinkingHistory` with their
|
|
907
|
+
// own turn-shape rules; chat completions has no wire-level handling, so the
|
|
908
|
+
// history is stripped here.
|
|
909
|
+
const compat = model.api === "openai-completions" ? model.compat : undefined;
|
|
910
|
+
if (!compat || !("stripThinkingHistory" in compat) || compat.stripThinkingHistory !== true) {
|
|
901
911
|
return messages;
|
|
902
912
|
}
|
|
903
913
|
|
|
@@ -2082,6 +2092,50 @@ async function streamAssistantResponse(
|
|
|
2082
2092
|
return userOnResponse?.(response, modelInfo);
|
|
2083
2093
|
};
|
|
2084
2094
|
|
|
2095
|
+
// Cursor executes its tools server-side during the stream and hands each
|
|
2096
|
+
// result to `cursorOnToolResult`; the host persists them after the assistant
|
|
2097
|
+
// message, so this loop never runs them and never sees them. A follow-up the
|
|
2098
|
+
// loop picks up after this turn needs them in its own context, or the next
|
|
2099
|
+
// request rebuilds this turn with every call unpaired. The host follows the
|
|
2100
|
+
// results with one message carrying their passive context, so that is
|
|
2101
|
+
// mirrored too. Entries are reserved synchronously so concurrent transforms
|
|
2102
|
+
// keep their arrival order. When the server re-sends a call it already ran
|
|
2103
|
+
// (issue #15749), the newer result replaces the earlier one, matching the
|
|
2104
|
+
// single result the host persists.
|
|
2105
|
+
const cursorToolResults: {
|
|
2106
|
+
result: ToolResultMessage;
|
|
2107
|
+
additionalContext: string | undefined;
|
|
2108
|
+
/** The host transform, which the provider does not always await. */
|
|
2109
|
+
pending: Promise<void>;
|
|
2110
|
+
}[] = [];
|
|
2111
|
+
const userCursorOnToolResult = config.cursorOnToolResult;
|
|
2112
|
+
const collectCursorToolResult: AgentLoopConfig["cursorOnToolResult"] = userCursorOnToolResult
|
|
2113
|
+
? async result => {
|
|
2114
|
+
const superseded = cursorToolResults.findIndex(entry => entry.result.toolCallId === result.toolCallId);
|
|
2115
|
+
if (superseded >= 0) cursorToolResults.splice(superseded, 1);
|
|
2116
|
+
// Read before the transform, which may return a message without it.
|
|
2117
|
+
const additionalContext = (result as ToolResultWithAdditionalContext)[TOOL_RESULT_ADDITIONAL_CONTEXT];
|
|
2118
|
+
const { promise: pending, resolve: settle } = Promise.withResolvers<void>();
|
|
2119
|
+
const entry = { result, additionalContext, pending };
|
|
2120
|
+
cursorToolResults.push(entry);
|
|
2121
|
+
try {
|
|
2122
|
+
const settled = await userCursorOnToolResult(result);
|
|
2123
|
+
if (settled) entry.result = settled;
|
|
2124
|
+
return settled;
|
|
2125
|
+
} finally {
|
|
2126
|
+
settle();
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
: undefined;
|
|
2130
|
+
// Like the host before it persists, wait for transforms still running so the
|
|
2131
|
+
// next request carries the result the transcript stores.
|
|
2132
|
+
const appendCursorToolResults = async (): Promise<void> => {
|
|
2133
|
+
await Promise.all(cursorToolResults.map(entry => entry.pending));
|
|
2134
|
+
for (const { result } of cursorToolResults) context.messages.push(result);
|
|
2135
|
+
const additionalContext = joinAdditionalContext(cursorToolResults.map(entry => entry.additionalContext));
|
|
2136
|
+
if (additionalContext !== undefined) context.messages.push(createAdditionalContextMessage(additionalContext));
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2085
2139
|
const finishChat = async (message: AssistantMessage): Promise<void> => {
|
|
2086
2140
|
await finishChatSpan(telemetry, chatSpan, message, {
|
|
2087
2141
|
stepNumber: chatStepNumber,
|
|
@@ -2106,6 +2160,7 @@ async function streamAssistantResponse(
|
|
|
2106
2160
|
cwd: effectiveCwd,
|
|
2107
2161
|
signal: finalRequestSignal,
|
|
2108
2162
|
onResponse: captureOnResponse,
|
|
2163
|
+
cursorOnToolResult: collectCursorToolResult,
|
|
2109
2164
|
liveSteering: providerCall.liveSteering,
|
|
2110
2165
|
});
|
|
2111
2166
|
if (promptToolWireTools && ownedDialect) {
|
|
@@ -2294,6 +2349,7 @@ async function streamAssistantResponse(
|
|
|
2294
2349
|
} else {
|
|
2295
2350
|
context.messages.push(finalMessage);
|
|
2296
2351
|
}
|
|
2352
|
+
await appendCursorToolResults();
|
|
2297
2353
|
if (!addedPartial) {
|
|
2298
2354
|
stream.push({ type: "message_start", message: snapshotAssistantMessage(finalMessage) });
|
|
2299
2355
|
}
|
|
@@ -2574,6 +2630,7 @@ async function streamAssistantResponse(
|
|
|
2574
2630
|
context.messages.push(trailing);
|
|
2575
2631
|
stream.push({ type: "message_start", message: snapshotAssistantMessage(trailing) });
|
|
2576
2632
|
}
|
|
2633
|
+
await appendCursorToolResults();
|
|
2577
2634
|
stream.push({ type: "message_end", message: snapshotAssistantMessage(trailing) });
|
|
2578
2635
|
await finishChat(trailing);
|
|
2579
2636
|
speculationSettled = true;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Api, ApiKey, AssistantMessage, Context, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { resolveCatalogPolicy } from "@oh-my-pi/pi-catalog/compat/catalog-policy";
|
|
9
10
|
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
10
11
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
11
12
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
@@ -346,7 +347,11 @@ export async function generateBranchSummary(
|
|
|
346
347
|
// Transform to LLM-compatible messages, then serialize to text
|
|
347
348
|
// Serialization prevents the model from treating it as a conversation to continue
|
|
348
349
|
const llmMessages = (options.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
349
|
-
const conversationText = serializeConversationForSummary(
|
|
350
|
+
const conversationText = serializeConversationForSummary(
|
|
351
|
+
llmMessages,
|
|
352
|
+
preferredDialect(model.id),
|
|
353
|
+
resolveCatalogPolicy(model).classifierCotExtraction === true,
|
|
354
|
+
);
|
|
350
355
|
|
|
351
356
|
// Build prompt
|
|
352
357
|
const instructions = customInstructions || BRANCH_SUMMARY_PROMPT;
|
|
@@ -36,6 +36,7 @@ import { convertTools } from "@oh-my-pi/pi-ai/providers/openai-responses";
|
|
|
36
36
|
import { buildResponsesInput, resolveOpenAICompatPolicy } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
37
37
|
import { isSurvivingAssistant } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
38
38
|
import { stripOpenAIResponsesOutputOnlyStatusesForReplay } from "@oh-my-pi/pi-ai/utils";
|
|
39
|
+
import { resolveCatalogPolicy } from "@oh-my-pi/pi-catalog/compat/catalog-policy";
|
|
39
40
|
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
40
41
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
41
42
|
import { isRecord, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
@@ -870,13 +871,14 @@ function planSummaryWindows(
|
|
|
870
871
|
messages: Message[],
|
|
871
872
|
tokenizer: Tokenizer,
|
|
872
873
|
dialect: Dialect | undefined,
|
|
874
|
+
dropThinking: boolean,
|
|
873
875
|
budgetTokens: number,
|
|
874
876
|
): Message[][] {
|
|
875
877
|
const windows: Message[][] = [];
|
|
876
878
|
let current: Message[] = [];
|
|
877
879
|
let currentTokens = 0;
|
|
878
880
|
for (const message of messages) {
|
|
879
|
-
const tokens = tokenizer.countTokens(serializeConversationForSummary([message], dialect));
|
|
881
|
+
const tokens = tokenizer.countTokens(serializeConversationForSummary([message], dialect, dropThinking));
|
|
880
882
|
if (currentTokens > 0 && currentTokens + tokens > budgetTokens) {
|
|
881
883
|
windows.push(current);
|
|
882
884
|
current = [];
|
|
@@ -905,8 +907,9 @@ export async function generateSummary(
|
|
|
905
907
|
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
|
906
908
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(currentMessages);
|
|
907
909
|
const dialect = preferredDialect(model.id);
|
|
910
|
+
const dropThinking = resolveCatalogPolicy(model).classifierCotExtraction === true;
|
|
908
911
|
const tokenizer = new Tokenizer(model);
|
|
909
|
-
const wholeConversation = serializeConversationForSummary(llmMessages, dialect);
|
|
912
|
+
const wholeConversation = serializeConversationForSummary(llmMessages, dialect, dropThinking);
|
|
910
913
|
const budgetTokens = summaryInputBudgetTokens(model, maxTokens);
|
|
911
914
|
// A span that outgrew the summarizer's window is summarized as a fold: each
|
|
912
915
|
// window updates the summary carried out of the previous one, which is the
|
|
@@ -917,12 +920,15 @@ export async function generateSummary(
|
|
|
917
920
|
// common case and costs exactly the one call it always did.
|
|
918
921
|
const pending: SummaryWindow[] = tokenizer.checkTokenBudget(wholeConversation, budgetTokens).fits
|
|
919
922
|
? [{ messages: llmMessages, budgetTokens, text: wholeConversation }]
|
|
920
|
-
: planSummaryWindows(llmMessages, tokenizer, dialect, budgetTokens).map(messages => ({
|
|
923
|
+
: planSummaryWindows(llmMessages, tokenizer, dialect, dropThinking, budgetTokens).map(messages => ({
|
|
924
|
+
messages,
|
|
925
|
+
budgetTokens,
|
|
926
|
+
}));
|
|
921
927
|
|
|
922
928
|
let carriedSummary = previousSummary;
|
|
923
929
|
while (pending.length > 0) {
|
|
924
930
|
const window = pending[0];
|
|
925
|
-
const text = window.text ?? serializeConversationForSummary(window.messages, dialect);
|
|
931
|
+
const text = window.text ?? serializeConversationForSummary(window.messages, dialect, dropThinking);
|
|
926
932
|
// A budget probe, not a raw count: a window whose bytes already fit needs
|
|
927
933
|
// neither an exact count nor the clamp, and the bust path hands back the
|
|
928
934
|
// exact count the proportional clamp needs as its denominator.
|
|
@@ -960,7 +966,7 @@ export async function generateSummary(
|
|
|
960
966
|
pending.splice(
|
|
961
967
|
0,
|
|
962
968
|
1,
|
|
963
|
-
...planSummaryWindows(window.messages, tokenizer, dialect, halved).map(messages => ({
|
|
969
|
+
...planSummaryWindows(window.messages, tokenizer, dialect, dropThinking, halved).map(messages => ({
|
|
964
970
|
messages,
|
|
965
971
|
budgetTokens: halved,
|
|
966
972
|
})),
|
|
@@ -1210,7 +1216,11 @@ async function generateShortSummary(
|
|
|
1210
1216
|
): Promise<string> {
|
|
1211
1217
|
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
|
1212
1218
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(recentMessages);
|
|
1213
|
-
const conversationText = serializeConversationForSummary(
|
|
1219
|
+
const conversationText = serializeConversationForSummary(
|
|
1220
|
+
llmMessages,
|
|
1221
|
+
preferredDialect(model.id),
|
|
1222
|
+
resolveCatalogPolicy(model).classifierCotExtraction === true,
|
|
1223
|
+
);
|
|
1214
1224
|
|
|
1215
1225
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
1216
1226
|
if (historySummary) {
|
|
@@ -2280,7 +2290,11 @@ async function generateTurnPrefixSummary(
|
|
|
2280
2290
|
const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), MAX_SUMMARY_TOKENS); // Smaller budget for turn prefix
|
|
2281
2291
|
|
|
2282
2292
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
2283
|
-
const conversationText = serializeConversationForSummary(
|
|
2293
|
+
const conversationText = serializeConversationForSummary(
|
|
2294
|
+
llmMessages,
|
|
2295
|
+
preferredDialect(model.id),
|
|
2296
|
+
resolveCatalogPolicy(model).classifierCotExtraction === true,
|
|
2297
|
+
);
|
|
2284
2298
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
2285
2299
|
const summarizationMessages = [
|
|
2286
2300
|
{
|
package/src/compaction/utils.ts
CHANGED
|
@@ -226,9 +226,10 @@ export function escapeSummaryBoundaryTags(text: string): string {
|
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
228
|
* Serialize LLM messages as plain summary input without provider control tokens.
|
|
229
|
+
* `dropThinking` is the summarizer's resolved `classifier-cot-extraction` policy.
|
|
229
230
|
*/
|
|
230
|
-
export function serializeConversationForSummary(messages: Message[], dialect?: Dialect): string {
|
|
231
|
-
const conversation = serializeConversation(messages, dialect);
|
|
231
|
+
export function serializeConversationForSummary(messages: Message[], dialect?: Dialect, dropThinking = false): string {
|
|
232
|
+
const conversation = serializeConversation(messages, dialect, dropThinking);
|
|
232
233
|
const escaped = dialect === "harmony" ? escapeHarmonyControlTokens(conversation) : conversation;
|
|
233
234
|
return escapeSummaryBoundaryTags(escaped);
|
|
234
235
|
}
|
|
@@ -236,8 +237,12 @@ export function serializeConversationForSummary(messages: Message[], dialect?: D
|
|
|
236
237
|
/**
|
|
237
238
|
* Serialize LLM messages to transcript text.
|
|
238
239
|
* Call convertToLlm() first to handle custom message types.
|
|
240
|
+
* `dropThinking` omits assistant reasoning from dialect transcripts: set it from
|
|
241
|
+
* the reader's resolved `classifier-cot-extraction` catalog policy (Claude's
|
|
242
|
+
* classifier refuses inputs that reproduce the model's own reasoning as text,
|
|
243
|
+
* "reasoning_extraction"). Reasoning is ephemeral and low-signal for a summary.
|
|
239
244
|
*/
|
|
240
|
-
export function serializeConversation(messages: Message[], dialect?: Dialect): string {
|
|
245
|
+
export function serializeConversation(messages: Message[], dialect?: Dialect, dropThinking = false): string {
|
|
241
246
|
// Tool results flagged contextually useless (and their paired calls) are
|
|
242
247
|
// dropped from the serialized text: the source region is discarded after
|
|
243
248
|
// summarization anyway, so excluding them costs nothing and keeps garbage
|
|
@@ -249,13 +254,6 @@ export function serializeConversation(messages: Message[], dialect?: Dialect): s
|
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
if (dialect) {
|
|
252
|
-
// Claude's classifier refuses inputs that reproduce the model's own
|
|
253
|
-
// reasoning as text ("reasoning_extraction"), and the anthropic dialect
|
|
254
|
-
// otherwise renders thinking verbatim inside <thinking> tags. Reasoning is
|
|
255
|
-
// ephemeral and low-signal for a summary, so drop it from Anthropic-target
|
|
256
|
-
// summary input. Other dialects (e.g. Harmony) carry reasoning natively in
|
|
257
|
-
// their transcript format and keep it.
|
|
258
|
-
const dropThinking = dialect === "anthropic";
|
|
259
257
|
const processed: Message[] = [];
|
|
260
258
|
for (const msg of messages) {
|
|
261
259
|
if (msg.role === "assistant") {
|