@oh-my-pi/pi-agent-core 18.1.8 → 18.1.10

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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.10] - 2026-09-04
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Codex V2 remote compaction ignoring explicit thinking-off.
10
+ - Fixed Codex V2 remote compaction rebuilding the request prefix differently from normal turns, restoring prompt-cache reuse ([#10786](https://github.com/can1357/oh-my-pi/issues/10786)).
11
+
5
12
  ## [18.1.6] - 2026-09-03
6
13
 
7
14
  ### Added
@@ -7,6 +7,7 @@
7
7
  * compaction item as replacement history.
8
8
  */
9
9
  import type { CodexCompactionContext, FetchImpl, Model, ProviderSessionState } from "@oh-my-pi/pi-ai";
10
+ import { type OpenAICodexCompactionBody } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
10
11
  /** Retained-message budget Codex uses after streamed V2 compaction. */
11
12
  export declare const V2_RETAINED_MESSAGE_TOKEN_BUDGET = 64000;
12
13
  /** Max retries for V2 streaming compaction on transient stream errors. */
@@ -21,18 +22,11 @@ export interface CompactionV2Usage {
21
22
  cachedInputTokens?: number;
22
23
  reasoningOutputTokens?: number;
23
24
  }
24
- /** Request body fields needed for Responses-stream V2 compaction. */
25
+ /** Provider-ready Responses body and local state needed for V2 compaction. */
25
26
  export interface CompactionV2Request {
26
- model: string;
27
+ body: OpenAICodexCompactionBody;
27
28
  input: unknown[];
28
- instructions: string;
29
29
  retainedMessageBudget: number;
30
- tools?: unknown[];
31
- /** Responses reasoning param (effort + summary), matching a normal turn; omitted for non-reasoning models. */
32
- reasoning?: {
33
- effort: string;
34
- summary: string;
35
- };
36
30
  sessionId?: string;
37
31
  promptCacheKey?: string;
38
32
  }
@@ -61,6 +55,12 @@ export declare function buildCompactionV2Request(model: Model, input: unknown[],
61
55
  promptCacheKey?: string;
62
56
  retainedMessageBudget?: number;
63
57
  }): CompactionV2Request;
58
+ /** Wrap a body built by the normal Codex serializer for V2 compaction transport. */
59
+ export declare function buildCompactionV2RequestFromBody(model: Model, body: OpenAICodexCompactionBody, options?: {
60
+ sessionId?: string;
61
+ promptCacheKey?: string;
62
+ retainedMessageBudget?: number;
63
+ }): CompactionV2Request;
64
64
  /** Request V2 compaction over the normal OpenAI Responses streaming endpoint. */
65
65
  export declare function requestCompactionV2Streaming(model: Model, apiKey: string, request: CompactionV2Request, signal?: AbortSignal, options?: {
66
66
  fetch?: FetchImpl;
@@ -160,7 +160,8 @@ export interface SummaryOptions {
160
160
  promptOverride?: string;
161
161
  extraContext?: string[];
162
162
  remoteEndpoint?: string;
163
- remoteInstructions?: string;
163
+ /** Stable system-prompt segments from the live turn, preserved for provider cache reuse. */
164
+ remoteSystemPrompt?: string[];
164
165
  initiatorOverride?: MessageAttribution;
165
166
  metadata?: Record<string, unknown>;
166
167
  convertToLlm?: ConvertToLlm;
@@ -456,7 +456,7 @@ export declare function recordManualChatTelemetry(telemetry: AgentTelemetry | un
456
456
  /**
457
457
  * Options accepted by {@link instrumentedCompleteSimple}. Mirrors the
458
458
  * `streamAssistantResponse` chat-span lifecycle for oneshot LLM calls
459
- * (compaction summaries, handoff document, branch summary, inspect_image).
459
+ * (compaction summaries, handoff document, branch summary, image_question).
460
460
  */
461
461
  export interface InstrumentedChatSpanOptions {
462
462
  readonly telemetry: AgentTelemetry | undefined;
@@ -467,7 +467,7 @@ export interface InstrumentedChatSpanOptions {
467
467
  /**
468
468
  * Tag stamped onto `pi.gen_ai.oneshot.kind`. Values used by the agent:
469
469
  * `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`,
470
- * `handoff`, `branch_summary`, `inspect_image`. Free-form to allow callers
470
+ * `handoff`, `branch_summary`, `image_question`. Free-form to allow callers
471
471
  * outside this package to add new kinds without bumping the helper.
472
472
  */
473
473
  readonly oneshotKind?: 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.1.8",
4
+ "version": "18.1.10",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,16 +35,16 @@
35
35
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "18.1.8",
39
- "@oh-my-pi/pi-catalog": "18.1.8",
40
- "@oh-my-pi/pi-natives": "18.1.8",
41
- "@oh-my-pi/pi-utils": "18.1.8",
42
- "@oh-my-pi/pi-wire": "18.1.8",
43
- "@oh-my-pi/snapcompact": "18.1.8",
38
+ "@oh-my-pi/pi-ai": "18.1.10",
39
+ "@oh-my-pi/pi-catalog": "18.1.10",
40
+ "@oh-my-pi/pi-natives": "18.1.10",
41
+ "@oh-my-pi/pi-utils": "18.1.10",
42
+ "@oh-my-pi/pi-wire": "18.1.10",
43
+ "@oh-my-pi/snapcompact": "18.1.10",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@oh-my-pi/omptype": "18.1.8",
47
+ "@oh-my-pi/omptype": "18.1.10",
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"
@@ -71,15 +71,11 @@ export interface CompactionV2Usage {
71
71
  reasoningOutputTokens?: number;
72
72
  }
73
73
 
74
- /** Request body fields needed for Responses-stream V2 compaction. */
74
+ /** Provider-ready Responses body and local state needed for V2 compaction. */
75
75
  export interface CompactionV2Request {
76
- model: string;
76
+ body: OpenAICodexCompactionBody;
77
77
  input: unknown[];
78
- instructions: string;
79
78
  retainedMessageBudget: number;
80
- tools?: unknown[];
81
- /** Responses reasoning param (effort + summary), matching a normal turn; omitted for non-reasoning models. */
82
- reasoning?: { effort: string; summary: string };
83
79
  sessionId?: string;
84
80
  promptCacheKey?: string;
85
81
  }
@@ -205,13 +201,44 @@ export function buildCompactionV2Request(
205
201
  retainedMessageBudget?: number;
206
202
  },
207
203
  ): CompactionV2Request {
208
- return {
204
+ const cacheOptions = { sessionId: options?.sessionId, promptCacheKey: options?.promptCacheKey };
205
+ const promptCacheKey = getOpenAIPromptCacheKey(cacheOptions);
206
+ const body: OpenAICodexCompactionBody = {
209
207
  model: resolveCompactionV2Model(model),
210
208
  input,
211
209
  instructions,
210
+ stream: true,
211
+ store: false,
212
+ ...(options?.reasoning || model.useResponsesLite
213
+ ? {
214
+ reasoning: model.useResponsesLite ? { ...options?.reasoning, context: "all_turns" } : options?.reasoning,
215
+ include: ["reasoning.encrypted_content"],
216
+ }
217
+ : {}),
218
+ ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
219
+ ...(options?.tools && options.tools.length > 0 ? { tools: options.tools, tool_choice: "auto" } : {}),
220
+ };
221
+ if (model.useResponsesLite) {
222
+ applyCodexResponsesLiteShape(body);
223
+ }
224
+ return buildCompactionV2RequestFromBody(model, body, options);
225
+ }
226
+
227
+ /** Wrap a body built by the normal Codex serializer for V2 compaction transport. */
228
+ export function buildCompactionV2RequestFromBody(
229
+ model: Model,
230
+ body: OpenAICodexCompactionBody,
231
+ options?: {
232
+ sessionId?: string;
233
+ promptCacheKey?: string;
234
+ retainedMessageBudget?: number;
235
+ },
236
+ ): CompactionV2Request {
237
+ const input = Array.isArray(body.input) ? body.input : [];
238
+ return {
239
+ body: { ...body, model: resolveCompactionV2Model(model), input },
240
+ input,
212
241
  retainedMessageBudget: resolveCompactionV2RetainedMessageBudget(options?.retainedMessageBudget),
213
- reasoning: options?.reasoning,
214
- tools: options?.tools,
215
242
  sessionId: options?.sessionId,
216
243
  promptCacheKey: options?.promptCacheKey,
217
244
  };
@@ -312,35 +339,17 @@ async function attemptCompactionV2Streaming(
312
339
  },
313
340
  ): Promise<CompactionV2Response> {
314
341
  // Faithful to Codex: append the compaction trigger as the final input item
315
- // of an otherwise-normal Responses request, then stream the result. `store`
316
- // stays false — compaction must never persist a server-side response object.
317
- const cacheOptions = { sessionId: request.sessionId, promptCacheKey: request.promptCacheKey };
318
- const promptCacheKey = getOpenAIPromptCacheKey(cacheOptions);
342
+ // of an otherwise-normal Responses request. `store` remains false —
343
+ // compaction must never persist a server-side response object.
319
344
  const body: OpenAICodexCompactionBody = {
320
- model: request.model,
345
+ ...request.body,
321
346
  input: [...request.input, COMPACTION_TRIGGER_ITEM],
322
- instructions: request.instructions,
323
- stream: true,
324
347
  store: false,
325
- ...(request.reasoning || model.useResponsesLite
326
- ? {
327
- // Lite implies gpt-5.4+, where codex-rs sends `all_turns` replay.
328
- reasoning: model.useResponsesLite ? { ...request.reasoning, context: "all_turns" } : request.reasoning,
329
- include: ["reasoning.encrypted_content"],
330
- }
331
- : {}),
332
- ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
333
- ...(request.tools && request.tools.length > 0 ? { tools: request.tools, tool_choice: "auto" } : {}),
348
+ stream: true,
334
349
  };
335
350
  if (options.codexMetadata) {
336
351
  body.client_metadata = options.codexMetadata.clientMetadata;
337
352
  }
338
- // Responses Lite models take the same rewrite on the compaction stream:
339
- // instructions/tools ride as input items (codex-rs `compact_remote_v2`
340
- // builds through `build_responses_request`).
341
- if (model.useResponsesLite) {
342
- applyCodexResponsesLiteShape(body);
343
- }
344
353
 
345
354
  if (shouldUseCodexProviderTransport(model)) {
346
355
  const eventStream = await openCodexCompactionEventStream(model, body, {
@@ -425,7 +434,7 @@ function buildCompactionV2Headers(
425
434
  headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
426
435
  headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
427
436
  headers[OPENAI_HEADERS.CODEX_BETA_FEATURES] = OPENAI_HEADER_VALUES.REMOTE_COMPACTION_V2;
428
- headers[OPENAI_HEADERS.ROUTING_HINT] = codexRoutingHint(request.model, undefined);
437
+ headers[OPENAI_HEADERS.ROUTING_HINT] = codexRoutingHint(request.body.model, undefined);
429
438
  if (model.useResponsesLite) {
430
439
  headers[OPENAI_HEADERS.RESPONSES_LITE] = "true";
431
440
  }
@@ -25,7 +25,12 @@ import {
25
25
  } from "@oh-my-pi/pi-ai";
26
26
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
27
27
  import * as AIError from "@oh-my-pi/pi-ai/error";
28
- import { createOpenAICodexCompactionRequestContext } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
28
+ import {
29
+ buildTransformedCodexRequestBody,
30
+ createOpenAICodexCompactionRequestContext,
31
+ type OpenAICodexCompactionBody,
32
+ } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
33
+ import type { InputItem as CodexInputItem } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
29
34
  import { convertTools } from "@oh-my-pi/pi-ai/providers/openai-responses";
30
35
  import { buildResponsesInput, resolveOpenAICompatPolicy } from "@oh-my-pi/pi-ai/providers/openai-shared";
31
36
  import { stripOpenAIResponsesOutputOnlyStatusesForReplay } from "@oh-my-pi/pi-ai/utils";
@@ -39,6 +44,7 @@ import { Tokenizer } from "../tokenizer";
39
44
  import type { AgentMessage } from "../types";
40
45
  import {
41
46
  buildCompactionV2Request,
47
+ buildCompactionV2RequestFromBody,
42
48
  getCompactionV2PreserveData,
43
49
  requestCompactionV2Streaming,
44
50
  shouldUseCompactionV2Streaming,
@@ -660,7 +666,8 @@ export interface SummaryOptions {
660
666
  promptOverride?: string;
661
667
  extraContext?: string[];
662
668
  remoteEndpoint?: string;
663
- remoteInstructions?: string;
669
+ /** Stable system-prompt segments from the live turn, preserved for provider cache reuse. */
670
+ remoteSystemPrompt?: string[];
664
671
  initiatorOverride?: MessageAttribution;
665
672
  metadata?: Record<string, unknown>;
666
673
  convertToLlm?: ConvertToLlm;
@@ -1433,6 +1440,19 @@ export function prepareCompaction(
1433
1440
  // ============================================================================
1434
1441
 
1435
1442
  const TURN_PREFIX_SUMMARIZATION_PROMPT = prompt.render(compactionTurnPrefixPrompt);
1443
+ function isCodexResponsesModel(model: Model): model is Model<"openai-codex-responses"> {
1444
+ return model.api === "openai-codex-responses";
1445
+ }
1446
+
1447
+ function isCodexInputItem(item: Record<string, unknown>): item is CodexInputItem & Record<string, unknown> {
1448
+ return (
1449
+ (item.id === undefined || item.id === null || typeof item.id === "string") &&
1450
+ (item.type === undefined || item.type === null || typeof item.type === "string") &&
1451
+ (item.role === undefined || typeof item.role === "string") &&
1452
+ (item.call_id === undefined || item.call_id === null || typeof item.call_id === "string") &&
1453
+ (item.name === undefined || typeof item.name === "string")
1454
+ );
1455
+ }
1436
1456
 
1437
1457
  function openAiCompatSupportsImageDetailOriginal(model: Model): boolean {
1438
1458
  const compat = model.compat;
@@ -1546,7 +1566,7 @@ export async function compact(
1546
1566
  promptOverride: options?.promptOverride,
1547
1567
  extraContext: options?.extraContext,
1548
1568
  remoteEndpoint: settings.remoteEnabled === false ? undefined : settings.remoteEndpoint,
1549
- remoteInstructions: options?.remoteInstructions,
1569
+ remoteSystemPrompt: options?.remoteSystemPrompt,
1550
1570
  initiatorOverride: options?.initiatorOverride,
1551
1571
  metadata: options?.metadata,
1552
1572
  convertToLlm: options?.convertToLlm,
@@ -1598,17 +1618,63 @@ export async function compact(
1598
1618
  previousRemoteCompaction?.provider === model.provider
1599
1619
  ? previousRemoteCompaction.replacementHistory
1600
1620
  : undefined;
1601
- const remoteHistory = buildOpenAiResponsesCompactionInput(
1602
- (summaryOptions.convertToLlm ?? defaultConvertToLlm)(remoteMessages),
1603
- model,
1604
- previousReplacementHistory,
1605
- );
1621
+ const messages = (summaryOptions.convertToLlm ?? defaultConvertToLlm)(remoteMessages);
1622
+ const remoteSystemPrompt = summaryOptions.remoteSystemPrompt ?? [SUMMARIZATION_SYSTEM_PROMPT];
1623
+ let codexBody: OpenAICodexCompactionBody | undefined;
1624
+ let remoteHistory: Array<Record<string, unknown>>;
1625
+ if (isCodexResponsesModel(model)) {
1626
+ const previousCodexInput: CodexInputItem[] = [];
1627
+ for (const item of previousReplacementHistory ?? []) {
1628
+ if (!isCodexInputItem(item)) {
1629
+ throw new Error("Stored Codex V2 compaction history contains an invalid input item");
1630
+ }
1631
+ previousCodexInput.push(item);
1632
+ }
1633
+ codexBody = await buildTransformedCodexRequestBody(
1634
+ model,
1635
+ { systemPrompt: remoteSystemPrompt, messages, tools: summaryOptions.tools },
1636
+ {
1637
+ reasoning: resolveCompactionEffort(model, summaryOptions.thinkingLevel),
1638
+ forceReasoningOff: summaryOptions.thinkingLevel === ThinkingLevel.Off,
1639
+ responsesLite: model.useResponsesLite,
1640
+ sessionId: summaryOptions.sessionId,
1641
+ promptCacheKey: summaryOptions.promptCacheKey,
1642
+ providerSessionState: summaryOptions.providerSessionState,
1643
+ codexCompaction: createOpenAICodexCompactionRequestContext({
1644
+ context: summaryOptions.codexCompaction,
1645
+ implementation: "responses_compaction_v2",
1646
+ }),
1647
+ },
1648
+ undefined,
1649
+ previousCodexInput,
1650
+ );
1651
+ const input = Array.isArray(codexBody.input) ? codexBody.input : [];
1652
+ const nativeInput: Array<Record<string, unknown>> = [];
1653
+ for (const item of input) {
1654
+ if (!isRecord(item)) {
1655
+ throw new Error("Codex V2 compaction input contains a non-object item");
1656
+ }
1657
+ nativeInput.push(item);
1658
+ }
1659
+ remoteHistory = stripOpenAIResponsesOutputOnlyStatusesForReplay(nativeInput);
1660
+ codexBody.input = remoteHistory;
1661
+ } else {
1662
+ remoteHistory = buildOpenAiResponsesCompactionInput(messages, model, previousReplacementHistory);
1663
+ }
1606
1664
  if (remoteHistory.length > 0) {
1607
1665
  try {
1608
- const instructions = summaryOptions.remoteInstructions ?? SUMMARIZATION_SYSTEM_PROMPT;
1609
- const tools = summaryOptions.tools
1610
- ? convertTools(summaryOptions.tools, model.compat.supportsStrictMode, model)
1611
- : undefined;
1666
+ const instructions = codexBody
1667
+ ? typeof codexBody.instructions === "string"
1668
+ ? codexBody.instructions
1669
+ : ""
1670
+ : remoteSystemPrompt.join("\n\n");
1671
+ const tools = codexBody
1672
+ ? Array.isArray(codexBody.tools)
1673
+ ? codexBody.tools
1674
+ : undefined
1675
+ : summaryOptions.tools
1676
+ ? convertTools(summaryOptions.tools, model.compat.supportsStrictMode, model)
1677
+ : undefined;
1612
1678
  const trimmed = trimRemoteCompactionInputToContextWindow(
1613
1679
  remoteHistory,
1614
1680
  new Tokenizer(model),
@@ -1626,13 +1692,18 @@ export async function compact(
1626
1692
  contextWindow: model.contextWindow,
1627
1693
  });
1628
1694
  }
1629
- const request = buildCompactionV2Request(model, trimmed.input, instructions, {
1630
- tools,
1631
- reasoning: buildCompactionV2Reasoning(model, summaryOptions.thinkingLevel),
1695
+ const requestOptions = {
1632
1696
  sessionId: summaryOptions.sessionId,
1633
1697
  promptCacheKey: summaryOptions.promptCacheKey,
1634
1698
  retainedMessageBudget: settings.v2RetainedMessageBudget,
1635
- });
1699
+ };
1700
+ const request = codexBody
1701
+ ? buildCompactionV2RequestFromBody(model, { ...codexBody, input: trimmed.input }, requestOptions)
1702
+ : buildCompactionV2Request(model, trimmed.input, instructions, {
1703
+ ...requestOptions,
1704
+ tools,
1705
+ reasoning: buildCompactionV2Reasoning(model, summaryOptions.thinkingLevel),
1706
+ });
1636
1707
  const remote = await withAuth(
1637
1708
  apiKey,
1638
1709
  key =>
@@ -1685,7 +1756,7 @@ export async function compact(
1685
1756
  model,
1686
1757
  key,
1687
1758
  remoteHistory,
1688
- summaryOptions.remoteInstructions ?? SUMMARIZATION_SYSTEM_PROMPT,
1759
+ summaryOptions.remoteSystemPrompt?.join("\n\n") ?? SUMMARIZATION_SYSTEM_PROMPT,
1689
1760
  signal,
1690
1761
  {
1691
1762
  fetch: summaryOptions.fetch,
package/src/telemetry.ts CHANGED
@@ -1638,7 +1638,7 @@ export async function recordManualChatTelemetry(
1638
1638
  /**
1639
1639
  * Options accepted by {@link instrumentedCompleteSimple}. Mirrors the
1640
1640
  * `streamAssistantResponse` chat-span lifecycle for oneshot LLM calls
1641
- * (compaction summaries, handoff document, branch summary, inspect_image).
1641
+ * (compaction summaries, handoff document, branch summary, image_question).
1642
1642
  */
1643
1643
  export interface InstrumentedChatSpanOptions {
1644
1644
  readonly telemetry: AgentTelemetry | undefined;
@@ -1649,7 +1649,7 @@ export interface InstrumentedChatSpanOptions {
1649
1649
  /**
1650
1650
  * Tag stamped onto `pi.gen_ai.oneshot.kind`. Values used by the agent:
1651
1651
  * `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`,
1652
- * `handoff`, `branch_summary`, `inspect_image`. Free-form to allow callers
1652
+ * `handoff`, `branch_summary`, `image_question`. Free-form to allow callers
1653
1653
  * outside this package to add new kinds without bumping the helper.
1654
1654
  */
1655
1655
  readonly oneshotKind?: string;