@linxiraos/pi-agent-core 1.0.5 → 1.0.7

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
@@ -1,8 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## [1.0.5] - 2026-08-16
3
+ ## [Unreleased]
4
4
 
5
- ## [1.0.2] - 2026-08-15
5
+ ## [1.0.7] - 2026-08-18
6
+
7
+ ## [17.3.5] - 2026-08-16
8
+
9
+ ### Added
10
+
11
+ - Added automatic retry support for transient provider failures during one-shot completions, allowing callers such as compaction to opt in to resilient request handling.
12
+
13
+ ### Fixed
14
+
15
+ - Fixed /handoff, branch summarization, and manual /compact failing outright on transient provider errors (e.g. Anthropic overloaded/429/529 responses); these operations now retry automatically instead of leaving the user's context full.
16
+
17
+ ## [17.3.4] - 2026-08-14
18
+
19
+ ### Fixed
20
+
21
+ - Fixed Codex-compatible V2 remote compaction with an explicit `v2Endpoint` by sending the required feature-negotiation header ([#8524](https://github.com/can1357/oh-my-pi/issues/8524)).
6
22
 
7
23
  ## [17.3.0] - 2026-08-13
8
24
 
@@ -1204,17 +1220,17 @@
1204
1220
 
1205
1221
  ### Changed
1206
1222
 
1207
- - Forked to @oh-my-pi scope with unified versioning across all packages
1223
+ - Forked to @linxiraos scope with unified versioning across all packages
1208
1224
 
1209
1225
  ## [1.337.0] - 2026-01-02
1210
1226
 
1211
- Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mono](https://github.com/badlogic/pi-mono).
1227
+ Initial release under @linxiraos scope. See previous releases at [badlogic/pi-mono](https://github.com/badlogic/pi-mono).
1212
1228
 
1213
1229
  ## [1.0.0] - 2026-08-13
1214
1230
 
1215
1231
  ### Changed
1216
1232
 
1217
- - Reset the version to 1.0.0 and republished under the `@linxiraos/*` scope, breaking from the `@oh-my-pi` version lineage.
1233
+ - Reset the version to 1.0.0 and republished under the `@linxiraos/*` scope, breaking from the `@linxiraos` version lineage.
1218
1234
 
1219
1235
  ## [0.38.0] - 2026-01-08
1220
1236
 
@@ -1264,7 +1280,7 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
1264
1280
  - **`AppMessage` renamed to `AgentMessage`**: All references to `AppMessage` have been renamed to `AgentMessage` for consistency.
1265
1281
  - **`CustomMessages` renamed to `CustomAgentMessages`**: The declaration merging interface has been renamed.
1266
1282
  - **`UserMessageWithAttachments` and `Attachment` types removed**: Attachment handling is now the responsibility of the `convertToLlm` function.
1267
- - **Agent loop moved from `@linxiraos/pi-ai`**: The `agentLoop`, `agentLoopContinue`, and related types have moved to this package. Import from `@oh-my-pi/pi-agent` instead.
1283
+ - **Agent loop moved from `@linxiraos/pi-ai`**: The `agentLoop`, `agentLoopContinue`, and related types have moved to this package. Import from `@linxiraos/pi-agent` instead.
1268
1284
 
1269
1285
  ### Added
1270
1286
 
@@ -4,7 +4,7 @@
4
4
  * Pure functions for compaction logic. The session manager handles I/O,
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
- import { type Api, type ApiKey, type AssistantMessage, type CodexCompactionContext, type Context, type FetchImpl, type MessageAttribution, type Model, type ProviderSessionState, type SimpleStreamOptions, type Tool, type Usage } from "@linxiraos/pi-ai";
7
+ import { type Api, type ApiKey, type AssistantMessage, type CodexCompactionContext, type Context, type FetchImpl, type MessageAttribution, type Model, type OneshotRetryOptions, type ProviderSessionState, type SimpleStreamOptions, type Tool, type Usage } from "@linxiraos/pi-ai";
8
8
  import { type AgentTelemetry } from "../telemetry.js";
9
9
  import { ThinkingLevel } from "../thinking.js";
10
10
  import type { AgentMessage } from "../types.js";
@@ -217,6 +217,20 @@ export interface SummaryOptions {
217
217
  * the live agent turn also bracket compaction HTTP requests.
218
218
  */
219
219
  completeImpl?: <TApi extends Api>(model: Model<TApi>, ctx: Context, options: SimpleStreamOptions) => Promise<AssistantMessage>;
220
+ /**
221
+ * Transient-failure retry for the summarization oneshots (`generateSummary`,
222
+ * `generateShortSummary`, `generateTurnPrefixSummary`).
223
+ *
224
+ * Defaults to enabled, which is what a one-shot caller such as manual
225
+ * `/compact` needs: a single Anthropic `overloaded_error` / 429 / 529 should
226
+ * not abort compaction and leave the context full.
227
+ *
228
+ * Pass `false` when the CALLER already owns a retry loop around the whole
229
+ * compaction attempt — auto-compaction does — otherwise the two budgets
230
+ * multiply (10 outer attempts x 3 inner = 30 requests) and each outer wait
231
+ * stacks on top of the inner backoff.
232
+ */
233
+ oneshotRetry?: OneshotRetryOptions | false;
220
234
  }
221
235
  export declare function generateSummary(currentMessages: AgentMessage[], model: Model, reserveTokens: number, apiKey: ApiKey, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, options?: SummaryOptions): Promise<string>;
222
236
  export interface HandoffOptions {
@@ -22,7 +22,7 @@
22
22
  * registered, `@opentelemetry/api` returns a no-op tracer and all calls are
23
23
  * cheap pass-throughs.
24
24
  */
25
- import { type Api, type AssistantMessage, type Context, type Message, type Model, type ServiceTier, type SimpleStreamOptions, type StopReason, type ToolChoice, type Usage } from "@linxiraos/pi-ai";
25
+ import { type Api, type AssistantMessage, type Context, type Message, type Model, type OneshotRetryOptions, type ServiceTier, type SimpleStreamOptions, type StopReason, type ToolChoice, type Usage } from "@linxiraos/pi-ai";
26
26
  import { type Attributes, type AttributeValue, type Span, SpanKind, SpanStatusCode, type Tracer, trace } from "@opentelemetry/api";
27
27
  import { AgentRunCollector, type AgentRunCoverage, type AgentRunSummary, type ToolStatus } from "./run-collector.js";
28
28
  import type { AgentTool } from "./types.js";
@@ -479,6 +479,23 @@ export interface InstrumentedChatSpanOptions {
479
479
  * seam while still going through the chat-span lifecycle.
480
480
  */
481
481
  readonly completeImpl?: <TApi extends Api>(model: Model<TApi>, ctx: Context, options: SimpleStreamOptions) => Promise<AssistantMessage>;
482
+ /**
483
+ * Opt in to transient-failure retry for this oneshot (Anthropic
484
+ * `overloaded_error`, `rate_limit_error`, 429/500/502/503/529). Omitted or
485
+ * `undefined` means **no retry** — the failure is surfaced exactly as before.
486
+ *
487
+ * Deliberately opt-in rather than default-on: `oneshotKind` is free-form and
488
+ * callers may pass arbitrary `ctx.tools` / `options.toolChoice`, so this
489
+ * funnel cannot itself prove a given request is replay-safe. Re-issuing is
490
+ * only safe when the call performs no side effect and nothing consumed
491
+ * partial output — true for summaries, titles, handoffs and image
492
+ * descriptions, which parse a complete response after it resolves. Enable it
493
+ * per call site, as a reviewed decision.
494
+ *
495
+ * Pass `{}` to accept the {@link retryTransientCompletion} defaults
496
+ * (3 attempts, 500ms base backoff, `retry-after` honored).
497
+ */
498
+ readonly retry?: OneshotRetryOptions;
482
499
  }
483
500
  /**
484
501
  * Wrap a {@link completeSimple} round-trip with the same chat-span lifecycle
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-agent-core",
4
- "version": "1.0.5",
4
+ "version": "1.0.7",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://linxira-os.github.io",
7
7
  "author": "Can Boluk",
@@ -35,13 +35,13 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@linxiraos/pi-ai": "1.0.5",
39
- "@linxiraos/pi-catalog": "1.0.5",
40
- "@linxiraos/pi-natives": "1.0.2",
41
- "@linxiraos/pi-utils": "1.0.5",
38
+ "@linxiraos/pi-ai": "1.0.7",
39
+ "@linxiraos/pi-catalog": "1.0.7",
40
+ "@linxiraos/pi-natives": "1.0.4",
41
+ "@linxiraos/pi-utils": "1.0.7",
42
42
  "@linxiraos/pi-wire": "1.0.2",
43
- "@linxiraos/pi-snapcompact": "1.0.5",
44
- "@opentelemetry/api": "^1.9.1"
43
+ "@opentelemetry/api": "^1.9.1",
44
+ "@linxiraos/pi-snapcompact": "1.0.7"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@linxiraos/pi-omptype": "1.0.2",
@@ -334,12 +334,18 @@ export async function generateBranchSummary(
334
334
  ];
335
335
 
336
336
  // Call LLM for summarization
337
- const response = await instrumentedCompleteSimple(
338
- model,
339
- { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
340
- { apiKey, signal, maxTokens: 2048, metadata },
341
- { telemetry: options.telemetry, oneshotKind: "branch_summary", completeImpl: options.completeImpl },
342
- );
337
+ let response: AssistantMessage;
338
+ try {
339
+ response = await instrumentedCompleteSimple(
340
+ model,
341
+ { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
342
+ { apiKey, signal, maxTokens: 2048, metadata },
343
+ { telemetry: options.telemetry, oneshotKind: "branch_summary", completeImpl: options.completeImpl, retry: {} },
344
+ );
345
+ } catch (error) {
346
+ if (signal.aborted) return { aborted: true };
347
+ throw error;
348
+ }
343
349
 
344
350
  // Check if aborted or errored
345
351
  if (response.stopReason === "aborted") {
@@ -423,6 +423,7 @@ function buildCompactionV2Headers(
423
423
  }
424
424
  headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
425
425
  headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
426
+ headers[OPENAI_HEADERS.CODEX_BETA_FEATURES] = OPENAI_HEADER_VALUES.REMOTE_COMPACTION_V2;
426
427
  if (model.useResponsesLite) {
427
428
  headers[OPENAI_HEADERS.RESPONSES_LITE] = "true";
428
429
  }
@@ -16,6 +16,7 @@ import {
16
16
  type Message,
17
17
  type MessageAttribution,
18
18
  type Model,
19
+ type OneshotRetryOptions,
19
20
  type ProviderSessionState,
20
21
  type SimpleStreamOptions,
21
22
  type Tool,
@@ -470,8 +471,8 @@ function computeMessageTokens(message: AgentMessage, options?: { excludeEncrypte
470
471
  if (!options?.excludeEncryptedReasoning) fragments.push(block.data);
471
472
  } else if (block.type === "anthropicServerTool") {
472
473
  // Native Anthropic server-tool call/result replayed verbatim on the
473
- // wire (server_tool_use input, web_search_tool_result
474
- // encrypted_content). Opaque provider-replay state the provider still
474
+ // wire (server_tool_use input and opaque result content). This opaque
475
+ // provider-replay state the provider still
475
476
  // bills for on same-provider replay; excluded from the compaction
476
477
  // floor like other encrypted reasoning because its local byte size
477
478
  // diverges from provider billing.
@@ -831,6 +832,31 @@ export interface SummaryOptions {
831
832
  ctx: Context,
832
833
  options: SimpleStreamOptions,
833
834
  ) => Promise<AssistantMessage>;
835
+ /**
836
+ * Transient-failure retry for the summarization oneshots (`generateSummary`,
837
+ * `generateShortSummary`, `generateTurnPrefixSummary`).
838
+ *
839
+ * Defaults to enabled, which is what a one-shot caller such as manual
840
+ * `/compact` needs: a single Anthropic `overloaded_error` / 429 / 529 should
841
+ * not abort compaction and leave the context full.
842
+ *
843
+ * Pass `false` when the CALLER already owns a retry loop around the whole
844
+ * compaction attempt — auto-compaction does — otherwise the two budgets
845
+ * multiply (10 outer attempts x 3 inner = 30 requests) and each outer wait
846
+ * stacks on top of the inner backoff.
847
+ */
848
+ oneshotRetry?: OneshotRetryOptions | false;
849
+ }
850
+
851
+ /**
852
+ * Resolve the oneshot retry policy for a summarization call. Enabled by default
853
+ * so a lone transient blip cannot abort compaction; `false` opts out for callers
854
+ * that already retry the whole attempt (see `SummaryOptions.oneshotRetry`).
855
+ */
856
+ function summaryOneshotRetry(options: SummaryOptions | undefined): OneshotRetryOptions | undefined {
857
+ const configured = options?.oneshotRetry;
858
+ if (configured === false) return undefined;
859
+ return configured ?? {};
834
860
  }
835
861
 
836
862
  function localCodexCompaction(options: SummaryOptions | undefined) {
@@ -935,7 +961,12 @@ export async function generateSummary(
935
961
  providerSessionState: options?.providerSessionState,
936
962
  codexCompaction: localCodexCompaction(options),
937
963
  },
938
- { telemetry: options?.telemetry, oneshotKind: "compaction_summary", completeImpl: options?.completeImpl },
964
+ {
965
+ telemetry: options?.telemetry,
966
+ oneshotKind: "compaction_summary",
967
+ completeImpl: options?.completeImpl,
968
+ retry: summaryOneshotRetry(options),
969
+ },
939
970
  );
940
971
 
941
972
  if (response.stopReason === "error") {
@@ -1034,13 +1065,14 @@ export async function generateHandoffFromContext(
1034
1065
  telemetry: options.telemetry,
1035
1066
  oneshotKind: "handoff",
1036
1067
  completeImpl: options.completeImpl,
1068
+ retry: {},
1037
1069
  });
1038
1070
  if (response.stopReason === "error" && shouldRetryHandoffWithAutoToolChoice(response)) {
1039
1071
  response = await instrumentedCompleteSimple(
1040
1072
  model,
1041
1073
  context,
1042
1074
  { ...requestOptions, toolChoice: "auto" },
1043
- { telemetry: options.telemetry, oneshotKind: "handoff", completeImpl: options.completeImpl },
1075
+ { telemetry: options.telemetry, oneshotKind: "handoff", completeImpl: options.completeImpl, retry: {} },
1044
1076
  );
1045
1077
  }
1046
1078
 
@@ -1143,7 +1175,12 @@ async function generateShortSummary(
1143
1175
  providerSessionState: options?.providerSessionState,
1144
1176
  codexCompaction: localCodexCompaction(options),
1145
1177
  },
1146
- { telemetry: options?.telemetry, oneshotKind: "compaction_short_summary", completeImpl: options?.completeImpl },
1178
+ {
1179
+ telemetry: options?.telemetry,
1180
+ oneshotKind: "compaction_short_summary",
1181
+ completeImpl: options?.completeImpl,
1182
+ retry: summaryOneshotRetry(options),
1183
+ },
1147
1184
  );
1148
1185
 
1149
1186
  if (response.stopReason === "error") {
@@ -1719,7 +1756,12 @@ async function generateTurnPrefixSummary(
1719
1756
  providerSessionState: options?.providerSessionState,
1720
1757
  codexCompaction: localCodexCompaction(options),
1721
1758
  },
1722
- { telemetry: options?.telemetry, oneshotKind: "compaction_turn_prefix", completeImpl: options?.completeImpl },
1759
+ {
1760
+ telemetry: options?.telemetry,
1761
+ oneshotKind: "compaction_turn_prefix",
1762
+ completeImpl: options?.completeImpl,
1763
+ retry: summaryOneshotRetry(options),
1764
+ },
1723
1765
  );
1724
1766
 
1725
1767
  if (response.stopReason === "error") {
package/src/telemetry.ts CHANGED
@@ -30,6 +30,8 @@ import {
30
30
  completeSimple,
31
31
  type Message,
32
32
  type Model,
33
+ type OneshotRetryOptions,
34
+ retryTransientCompletion,
33
35
  type ServiceTier,
34
36
  type SimpleStreamOptions,
35
37
  type StopReason,
@@ -1663,6 +1665,23 @@ export interface InstrumentedChatSpanOptions {
1663
1665
  ctx: Context,
1664
1666
  options: SimpleStreamOptions,
1665
1667
  ) => Promise<AssistantMessage>;
1668
+ /**
1669
+ * Opt in to transient-failure retry for this oneshot (Anthropic
1670
+ * `overloaded_error`, `rate_limit_error`, 429/500/502/503/529). Omitted or
1671
+ * `undefined` means **no retry** — the failure is surfaced exactly as before.
1672
+ *
1673
+ * Deliberately opt-in rather than default-on: `oneshotKind` is free-form and
1674
+ * callers may pass arbitrary `ctx.tools` / `options.toolChoice`, so this
1675
+ * funnel cannot itself prove a given request is replay-safe. Re-issuing is
1676
+ * only safe when the call performs no side effect and nothing consumed
1677
+ * partial output — true for summaries, titles, handoffs and image
1678
+ * descriptions, which parse a complete response after it resolves. Enable it
1679
+ * per call site, as a reviewed decision.
1680
+ *
1681
+ * Pass `{}` to accept the {@link retryTransientCompletion} defaults
1682
+ * (3 attempts, 500ms base backoff, `retry-after` honored).
1683
+ */
1684
+ readonly retry?: OneshotRetryOptions;
1666
1685
  }
1667
1686
 
1668
1687
  /**
@@ -1723,10 +1742,26 @@ export async function instrumentedCompleteSimple<TApi extends Api>(
1723
1742
  try {
1724
1743
  return await runInActiveSpan(chatSpan, async () => {
1725
1744
  const complete = span.completeImpl ?? completeSimple;
1726
- const message = await complete(model, ctx, {
1727
- ...options,
1728
- onResponse: captureOnResponse,
1729
- });
1745
+ // Opt-in only (see `retry` on InstrumentedChatSpanOptions): each attempt
1746
+ // re-issues the whole request, which is safe only for replay-safe
1747
+ // oneshots. `getResponseHeaders` hands the failed attempt's headers to
1748
+ // the retry layer — an AssistantMessage carries none, so this is what
1749
+ // makes `retry-after` on a real 429/529 actually honored.
1750
+ const runOnce = () => {
1751
+ // Clear first so a previous attempt's `retry-after` can never be
1752
+ // reused for a later failure that arrived without headers.
1753
+ capturedHeaders = undefined;
1754
+ return complete(model, ctx, { ...options, onResponse: captureOnResponse });
1755
+ };
1756
+ const message = span.retry
1757
+ ? await retryTransientCompletion(runOnce, {
1758
+ ...span.retry,
1759
+ // Framework-owned: the caller must not be able to detach the
1760
+ // abort signal or the header source by passing them itself.
1761
+ signal: options.signal,
1762
+ getResponseHeaders: () => capturedHeaders,
1763
+ })
1764
+ : await runOnce();
1730
1765
  await finishChatSpan(telemetry, chatSpan, message, {
1731
1766
  stepNumber,
1732
1767
  serviceTier: options.serviceTier,