@oh-my-pi/pi-agent-core 17.3.4 → 17.3.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
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.3.5] - 2026-08-16
6
+
7
+ ### Added
8
+
9
+ - Added automatic retry support for transient provider failures during one-shot completions, allowing callers such as compaction to opt in to resilient request handling.
10
+
11
+ ### Fixed
12
+
13
+ - 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.
14
+
5
15
  ## [17.3.4] - 2026-08-14
6
16
 
7
17
  ### Fixed
@@ -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 "@oh-my-pi/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 "@oh-my-pi/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 "@oh-my-pi/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 "@oh-my-pi/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": "@oh-my-pi/pi-agent-core",
4
- "version": "17.3.4",
4
+ "version": "17.3.7",
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,16 +35,16 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "17.3.4",
39
- "@oh-my-pi/pi-catalog": "17.3.4",
40
- "@oh-my-pi/pi-natives": "17.3.4",
41
- "@oh-my-pi/pi-utils": "17.3.4",
42
- "@oh-my-pi/pi-wire": "17.3.4",
43
- "@oh-my-pi/snapcompact": "17.3.4",
38
+ "@oh-my-pi/pi-ai": "17.3.7",
39
+ "@oh-my-pi/pi-catalog": "17.3.7",
40
+ "@oh-my-pi/pi-natives": "17.3.7",
41
+ "@oh-my-pi/pi-utils": "17.3.7",
42
+ "@oh-my-pi/pi-wire": "17.3.7",
43
+ "@oh-my-pi/snapcompact": "17.3.7",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@oh-my-pi/omptype": "17.3.4",
47
+ "@oh-my-pi/omptype": "17.3.7",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
50
  "@types/bun": "^1.3.14"
@@ -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") {
@@ -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,