@oh-my-pi/pi-agent-core 17.1.8 → 17.2.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/CHANGELOG.md +6 -0
- package/dist/types/compaction/compaction.d.ts +2 -0
- package/dist/types/compaction/errors.d.ts +12 -0
- package/package.json +7 -7
- package/src/compaction/compaction-v2-streaming.ts +12 -6
- package/src/compaction/compaction.ts +37 -4
- package/src/compaction/errors.ts +16 -0
- package/src/compaction/openai.ts +4 -2
- package/src/compaction/utils.ts +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.0] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Provider-native compaction failures now surface their transport error instead of silently switching to generic summarization; streaming V2 still falls back to native V1 when available.
|
|
10
|
+
|
|
5
11
|
## [17.1.7] - 2026-07-27
|
|
6
12
|
|
|
7
13
|
### Changed
|
|
@@ -53,6 +53,8 @@ export interface CompactionSettings {
|
|
|
53
53
|
/** Reserve applied when {@link CompactionSettings.reserveTokens} is unset. */
|
|
54
54
|
export declare const DEFAULT_RESERVE_TOKENS = 16384;
|
|
55
55
|
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
56
|
+
/** Whether a compaction candidate preserves provider-native transport under the effective settings. */
|
|
57
|
+
export declare function shouldUseProviderNativeCompaction(model: Model, settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">): boolean;
|
|
56
58
|
/**
|
|
57
59
|
* Calculate total context tokens from usage.
|
|
58
60
|
* Uses the native totalTokens field when available, falls back to computing from components.
|
|
@@ -13,6 +13,18 @@ export declare class CompactionCancelledError extends Error {
|
|
|
13
13
|
readonly name: "CompactionCancelledError";
|
|
14
14
|
constructor(message?: string);
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* A provider-native compaction request failed after every native protocol
|
|
18
|
+
* available for the selected model was exhausted.
|
|
19
|
+
*
|
|
20
|
+
* The cause stays attached so AI error classification can still recognize
|
|
21
|
+
* authentication failures. Non-auth failures remain distinguishable from
|
|
22
|
+
* ordinary summarization errors and must not fall through to another provider.
|
|
23
|
+
*/
|
|
24
|
+
export declare class NativeCompactionError extends Error {
|
|
25
|
+
readonly name: "NativeCompactionError";
|
|
26
|
+
constructor(cause: unknown);
|
|
27
|
+
}
|
|
16
28
|
/**
|
|
17
29
|
* Outcome of a compaction attempt, surfaced by `CommandController.executeCompaction`
|
|
18
30
|
* so callers (e.g. the plan-mode approval flow) can distinguish a deliberate abort
|
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.
|
|
4
|
+
"version": "17.2.0",
|
|
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,12 +35,12 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "17.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "17.
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.
|
|
42
|
-
"@oh-my-pi/pi-wire": "17.
|
|
43
|
-
"@oh-my-pi/snapcompact": "17.
|
|
38
|
+
"@oh-my-pi/pi-ai": "17.2.0",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "17.2.0",
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.2.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.2.0",
|
|
42
|
+
"@oh-my-pi/pi-wire": "17.2.0",
|
|
43
|
+
"@oh-my-pi/snapcompact": "17.2.0",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Api, CodexCompactionContext, FetchImpl, Model, ProviderSessionState } from "@oh-my-pi/pi-ai";
|
|
11
|
-
import
|
|
11
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
12
12
|
import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
|
|
13
13
|
import {
|
|
14
14
|
createOpenAICodexCompactionRequestContext,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
parseAzureDeploymentNameMap,
|
|
22
22
|
resolveOpenAIRequestSetup,
|
|
23
23
|
} from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
24
|
+
import { captureOpenAIHttpError } from "@oh-my-pi/pi-ai/utils/openai-http";
|
|
24
25
|
import {
|
|
25
26
|
CODEX_BASE_URL,
|
|
26
27
|
getCodexAccountId,
|
|
@@ -334,18 +335,19 @@ async function attemptCompactionV2Streaming(
|
|
|
334
335
|
});
|
|
335
336
|
|
|
336
337
|
if (!response.ok) {
|
|
337
|
-
const
|
|
338
|
+
const cause = await captureOpenAIHttpError(response);
|
|
338
339
|
logger.warn("V2 remote compaction failed", {
|
|
339
340
|
endpoint,
|
|
340
341
|
status: response.status,
|
|
341
342
|
statusText: response.statusText,
|
|
342
|
-
errorText,
|
|
343
|
+
errorText: cause.captured.bodyText ?? "",
|
|
343
344
|
});
|
|
344
|
-
throw new ProviderHttpError(
|
|
345
|
+
throw new AIError.ProviderHttpError(
|
|
345
346
|
`V2 remote compaction failed (${response.status} ${response.statusText})`,
|
|
346
347
|
response.status,
|
|
347
348
|
{
|
|
348
349
|
headers: response.headers,
|
|
350
|
+
cause,
|
|
349
351
|
},
|
|
350
352
|
);
|
|
351
353
|
}
|
|
@@ -560,6 +562,10 @@ function formatCompactionV2Failure(event: Record<string, unknown>, type: string)
|
|
|
560
562
|
}
|
|
561
563
|
|
|
562
564
|
function isRetryableCompactionError(error: Error): boolean {
|
|
565
|
+
// The gateway's synthetic auth_unavailable is an HTTP 503, but the
|
|
566
|
+
// captured response cause classifies it as auth. Let provider fallback run
|
|
567
|
+
// immediately instead of spending the transient retry budget.
|
|
568
|
+
if (AIError.is(AIError.classify(error), AIError.Flag.AuthFailed)) return false;
|
|
563
569
|
if (
|
|
564
570
|
error.name === "AbortError" ||
|
|
565
571
|
error.name === "TimeoutError" ||
|
|
@@ -567,8 +573,8 @@ function isRetryableCompactionError(error: Error): boolean {
|
|
|
567
573
|
) {
|
|
568
574
|
return true;
|
|
569
575
|
}
|
|
570
|
-
if (error instanceof ProviderHttpError) {
|
|
571
|
-
return isTransientStatus(error.status);
|
|
576
|
+
if (error instanceof AIError.ProviderHttpError) {
|
|
577
|
+
return AIError.isTransientStatus(error.status);
|
|
572
578
|
}
|
|
573
579
|
const message = error.message.toLowerCase();
|
|
574
580
|
return (
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type Usage,
|
|
23
23
|
withAuth,
|
|
24
24
|
} from "@oh-my-pi/pi-ai";
|
|
25
|
-
import
|
|
25
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
26
26
|
import { createOpenAICodexCompactionRequestContext } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
|
27
27
|
import { convertTools } from "@oh-my-pi/pi-ai/providers/openai-responses";
|
|
28
28
|
import { buildResponsesInput, resolveOpenAICompatPolicy } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
V2_RETAINED_MESSAGE_TOKEN_BUDGET,
|
|
44
44
|
} from "./compaction-v2-streaming";
|
|
45
45
|
import type { CompactionEntry, SessionEntry } from "./entries";
|
|
46
|
+
import { NativeCompactionError } from "./errors";
|
|
46
47
|
import { isEstimateCacheable, readEstimateCache, writeEstimateCache } from "./message-cache";
|
|
47
48
|
import { type ConvertToLlm, createBranchSummaryMessage, createCustomMessage, defaultConvertToLlm } from "./messages";
|
|
48
49
|
import {
|
|
@@ -202,6 +203,18 @@ export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
|
|
|
202
203
|
v2RetainedMessageBudget: V2_RETAINED_MESSAGE_TOKEN_BUDGET,
|
|
203
204
|
};
|
|
204
205
|
|
|
206
|
+
/** Whether a compaction candidate preserves provider-native transport under the effective settings. */
|
|
207
|
+
export function shouldUseProviderNativeCompaction(
|
|
208
|
+
model: Model,
|
|
209
|
+
settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">,
|
|
210
|
+
): boolean {
|
|
211
|
+
if (settings.remoteEnabled === false) return false;
|
|
212
|
+
return (
|
|
213
|
+
shouldUseOpenAiRemoteCompaction(model) ||
|
|
214
|
+
(settings.remoteStreamingV2Enabled !== false && shouldUseCompactionV2Streaming(model))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
205
218
|
// ============================================================================
|
|
206
219
|
// Token calculation
|
|
207
220
|
// ============================================================================
|
|
@@ -725,7 +738,9 @@ function resolveCompactionEffort(model: Model, level: ThinkingLevel | undefined)
|
|
|
725
738
|
*/
|
|
726
739
|
function createSummarizationError(prefix: string, response: AssistantMessage): Error {
|
|
727
740
|
const text = `${prefix}: ${response.errorMessage || "Unknown error"}`;
|
|
728
|
-
return response.errorStatus === undefined
|
|
741
|
+
return response.errorStatus === undefined
|
|
742
|
+
? new Error(text)
|
|
743
|
+
: new AIError.ProviderHttpError(text, response.errorStatus);
|
|
729
744
|
}
|
|
730
745
|
|
|
731
746
|
function shouldRetryHandoffWithAutoToolChoice(response: AssistantMessage): boolean {
|
|
@@ -1332,6 +1347,17 @@ function buildCompactionV2Reasoning(
|
|
|
1332
1347
|
return { effort: reasoning.wireEffort ?? reasoning.requestedEffort, summary: "auto" };
|
|
1333
1348
|
}
|
|
1334
1349
|
|
|
1350
|
+
/**
|
|
1351
|
+
* Keep any non-auth native protocol failure ahead of authentication failures.
|
|
1352
|
+
* Downstream may retry compaction with another provider only when every native
|
|
1353
|
+
* protocol failed authentication, so a later auth error must not hide an
|
|
1354
|
+
* earlier transport or protocol failure.
|
|
1355
|
+
*/
|
|
1356
|
+
function selectNativeCompactionError(previousError: unknown, nextError: unknown): unknown {
|
|
1357
|
+
if (previousError === undefined) return nextError;
|
|
1358
|
+
return AIError.is(AIError.classify(previousError), AIError.Flag.AuthFailed) ? nextError : previousError;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1335
1361
|
/**
|
|
1336
1362
|
* Generate summaries for compaction using prepared data.
|
|
1337
1363
|
* Returns CompactionResult - SessionManager adds id/parentId when saving.
|
|
@@ -1406,6 +1432,7 @@ export async function compact(
|
|
|
1406
1432
|
...recentMessages,
|
|
1407
1433
|
];
|
|
1408
1434
|
let usedRemoteCompaction = false;
|
|
1435
|
+
let nativeCompactionError: unknown;
|
|
1409
1436
|
if (
|
|
1410
1437
|
settings.remoteEnabled !== false &&
|
|
1411
1438
|
settings.remoteStreamingV2Enabled !== false &&
|
|
@@ -1467,7 +1494,8 @@ export async function compact(
|
|
|
1467
1494
|
// swallowing it here would downgrade Esc into "fall back to local
|
|
1468
1495
|
// summarization" and keep compaction running on an aborted signal.
|
|
1469
1496
|
if (signal?.aborted) throw err;
|
|
1470
|
-
|
|
1497
|
+
nativeCompactionError = selectNativeCompactionError(nativeCompactionError, err);
|
|
1498
|
+
logger.warn("OpenAI V2 remote compaction failed, falling back to V1 remote compaction", {
|
|
1471
1499
|
error: err instanceof Error ? err.message : String(err),
|
|
1472
1500
|
model: model.id,
|
|
1473
1501
|
provider: model.provider,
|
|
@@ -1517,7 +1545,8 @@ export async function compact(
|
|
|
1517
1545
|
// swallowing it here would downgrade Esc into "fall back to local
|
|
1518
1546
|
// summarization" and keep compaction running on an aborted signal.
|
|
1519
1547
|
if (signal?.aborted) throw err;
|
|
1520
|
-
|
|
1548
|
+
nativeCompactionError = selectNativeCompactionError(nativeCompactionError, err);
|
|
1549
|
+
logger.warn("OpenAI remote compaction failed", {
|
|
1521
1550
|
error: err instanceof Error ? err.message : String(err),
|
|
1522
1551
|
model: model.id,
|
|
1523
1552
|
provider: model.provider,
|
|
@@ -1526,6 +1555,10 @@ export async function compact(
|
|
|
1526
1555
|
}
|
|
1527
1556
|
}
|
|
1528
1557
|
|
|
1558
|
+
if (!usedRemoteCompaction && nativeCompactionError !== undefined && !summaryOptions.remoteEndpoint) {
|
|
1559
|
+
throw new NativeCompactionError(nativeCompactionError);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1529
1562
|
// Generate summaries (can be parallel if both needed) and merge into one
|
|
1530
1563
|
let summary: string;
|
|
1531
1564
|
|
package/src/compaction/errors.ts
CHANGED
|
@@ -18,6 +18,22 @@ export class CompactionCancelledError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A provider-native compaction request failed after every native protocol
|
|
23
|
+
* available for the selected model was exhausted.
|
|
24
|
+
*
|
|
25
|
+
* The cause stays attached so AI error classification can still recognize
|
|
26
|
+
* authentication failures. Non-auth failures remain distinguishable from
|
|
27
|
+
* ordinary summarization errors and must not fall through to another provider.
|
|
28
|
+
*/
|
|
29
|
+
export class NativeCompactionError extends Error {
|
|
30
|
+
readonly name = "NativeCompactionError" as const;
|
|
31
|
+
|
|
32
|
+
constructor(cause: unknown) {
|
|
33
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
/**
|
|
22
38
|
* Outcome of a compaction attempt, surfaced by `CommandController.executeCompaction`
|
|
23
39
|
* so callers (e.g. the plan-mode approval flow) can distinguish a deliberate abort
|
package/src/compaction/openai.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
getOpenAIResponsesHistoryPayload,
|
|
39
39
|
normalizeResponsesToolCallId,
|
|
40
40
|
} from "@oh-my-pi/pi-ai/utils";
|
|
41
|
+
import { captureOpenAIHttpError } from "@oh-my-pi/pi-ai/utils/openai-http";
|
|
41
42
|
import {
|
|
42
43
|
CODEX_BASE_URL,
|
|
43
44
|
getCodexAccountId,
|
|
@@ -840,18 +841,19 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
840
841
|
});
|
|
841
842
|
|
|
842
843
|
if (!response.ok) {
|
|
843
|
-
const
|
|
844
|
+
const cause = await captureOpenAIHttpError(response);
|
|
844
845
|
logger.warn("OpenAI remote compaction failed", {
|
|
845
846
|
endpoint,
|
|
846
847
|
status: response.status,
|
|
847
848
|
statusText: response.statusText,
|
|
848
|
-
errorText,
|
|
849
|
+
errorText: cause.captured.bodyText ?? "",
|
|
849
850
|
});
|
|
850
851
|
throw new ProviderHttpError(
|
|
851
852
|
`Remote compaction failed (${response.status} ${response.statusText})`,
|
|
852
853
|
response.status,
|
|
853
854
|
{
|
|
854
855
|
headers: response.headers,
|
|
856
|
+
cause,
|
|
855
857
|
},
|
|
856
858
|
);
|
|
857
859
|
}
|
package/src/compaction/utils.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Message, ToolCall } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import { type Dialect, getDialectDefinition } from "@oh-my-pi/pi-ai/dialect";
|
|
7
|
+
import { escapeHarmonyControlTokens } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
7
8
|
import { formatGroupedPaths, prompt, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type { AgentMessage } from "../types";
|
|
9
10
|
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
@@ -207,15 +208,13 @@ export function truncateToolResultForSummary(text: string): string {
|
|
|
207
208
|
return `${text.slice(0, TOOL_RESULT_MAX_CHARS)}\n\n[... ${truncatedChars} more characters truncated]`;
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
const HARMONY_CONTROL_TOKEN_RE = /<\|(start|end|message|channel|constrain|return|call)\|>/g;
|
|
211
|
-
|
|
212
211
|
/**
|
|
213
212
|
* Serialize LLM messages as plain summary input without provider control tokens.
|
|
214
213
|
*/
|
|
215
214
|
export function serializeConversationForSummary(messages: Message[], dialect?: Dialect): string {
|
|
216
215
|
const conversation = serializeConversation(messages, dialect);
|
|
217
216
|
if (dialect !== "harmony") return conversation;
|
|
218
|
-
return conversation
|
|
217
|
+
return escapeHarmonyControlTokens(conversation);
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
/**
|