@oh-my-pi/pi-agent-core 18.13.3 → 18.14.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,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.14.1] - 2026-10-31
6
+
7
+ ### Breaking Changes
8
+
9
+ - `SentToolDefinitions.inactiveFor(messages, tools)` is replaced by `withInactiveTools(context)`, which also clears a stale `inactiveTools`; `transformProviderContext` now receives the request's `inactiveTools` ([#17492](https://github.com/can1357/oh-my-pi/pull/17492))
10
+
11
+ ### Fixed
12
+
13
+ - Long tool-call intents no longer block tool execution when intent tracing is enabled ([#13140](https://github.com/can1357/oh-my-pi/issues/13140)).
14
+
15
+ ## [18.14.0] - 2026-10-29
16
+
17
+ ### Breaking Changes
18
+
19
+ - Removed the Bun event-loop workaround utilities from `@oh-my-pi/pi-agent-core/utils/yield`; agent loops no longer insert cooperative sleeps while streaming or between tool calls.
20
+
21
+ ### Added
22
+
23
+ - Added `AgentTool.approvalRestrictionPaths` so tools can report the paths they read, allowing restricted-folder approvals to cover those paths without remembering approval separately for each path.
24
+ - Added `Agent.logContext`, which adds custom context fields to every log record written during an agent run.
25
+
5
26
  ## [18.13.3] - 2026-10-28
6
27
 
7
28
  ### Changed
package/README.md CHANGED
@@ -21,7 +21,7 @@ const agent = new Agent({
21
21
  },
22
22
  });
23
23
 
24
- agent.subscribe((event) => {
24
+ agent.subscribe(event => {
25
25
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
26
26
  // Stream just the new text chunk
27
27
  process.stdout.write(event.assistantMessageEvent.delta);
@@ -219,7 +219,7 @@ await agent.waitForIdle(); // Wait for completion
219
219
  ### Events
220
220
 
221
221
  ```typescript
222
- const unsubscribe = agent.subscribe((event) => {
222
+ const unsubscribe = agent.subscribe(event => {
223
223
  console.log(event.type);
224
224
  });
225
225
  unsubscribe();
@@ -274,8 +274,8 @@ Handle custom types in `convertToLlm`:
274
274
 
275
275
  ```typescript
276
276
  const agent = new Agent({
277
- convertToLlm: (messages) =>
278
- messages.flatMap((m) => {
277
+ convertToLlm: messages =>
278
+ messages.flatMap(m => {
279
279
  if (m.role === "notification") return []; // Filter out
280
280
  return [m];
281
281
  }),
@@ -360,7 +360,7 @@ const context: AgentContext = {
360
360
 
361
361
  const config: AgentLoopConfig = {
362
362
  model: getModel("openai", "gpt-4o"),
363
- convertToLlm: (msgs) => msgs.filter((m) => ["user", "assistant", "toolResult"].includes(m.role)),
363
+ convertToLlm: msgs => msgs.filter(m => ["user", "assistant", "toolResult"].includes(m.role)),
364
364
  };
365
365
 
366
366
  const userMessage = { role: "user", content: "Hello", timestamp: Date.now() };
@@ -376,6 +376,7 @@ for await (const event of agentLoopContinue(context, config)) {
376
376
  ```
377
377
 
378
378
  ## Run-level telemetry
379
+
379
380
  Every `invoke_agent` produces two values alongside the OTEL spans:
380
381
 
381
382
  - **`AgentRunSummary`** — chat / tool / usage / cost / error counters bucketed
@@ -447,10 +448,7 @@ Callers that drive the loop multiple times (verify pass, benchmark harness)
447
448
  fold N summaries with `aggregateAgentRunSummaries` / `aggregateAgentRunCoverage`:
448
449
 
449
450
  ```typescript
450
- import {
451
- aggregateAgentRunSummaries,
452
- aggregateAgentRunCoverage,
453
- } from "@oh-my-pi/pi-agent";
451
+ import { aggregateAgentRunSummaries, aggregateAgentRunCoverage } from "@oh-my-pi/pi-agent";
454
452
 
455
453
  const summaries: AgentRunSummary[] = [];
456
454
  const coverages: AgentRunCoverage[] = [];
@@ -295,6 +295,11 @@ export declare class Agent {
295
295
  * Set the prompt cache key forwarded to providers.
296
296
  */
297
297
  set promptCacheKey(value: string | undefined);
298
+ /**
299
+ * Fields added to every log record written while this agent runs (the
300
+ * owning session's `sessionId`), so in-process agents' records stay apart.
301
+ */
302
+ logContext: Readonly<Record<string, unknown>> | undefined;
298
303
  /**
299
304
  * Static metadata forwarded to every API request when no resolver is installed
300
305
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -379,6 +384,13 @@ export declare class Agent {
379
384
  /** Strip tool descriptions from provider-bound specs; read per request. */
380
385
  get pruneToolDescriptions(): boolean;
381
386
  set pruneToolDescriptions(value: boolean);
387
+ /**
388
+ * Whether requests to `model` actually ship description-stripped tool specs.
389
+ * Owned dialects render full definitions in-band, so they never prune; the
390
+ * loop applies the same rule (`pruneToolDescriptions && !ownedDialect`).
391
+ * Context accounting reads this instead of the raw setting.
392
+ */
393
+ prunesToolDescriptions(model: Model | undefined): boolean;
382
394
  /** Inject/strip the intent field on tool calls; applies from the next prompt run. */
383
395
  get intentTracing(): boolean;
384
396
  set intentTracing(value: boolean);
@@ -7,7 +7,7 @@
7
7
  * compaction item as replacement history.
8
8
  */
9
9
  import type { Api, 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
+ import type { OpenAICodexCompactionBody } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
11
11
  import type { CatalogPolicyInputs } from "@oh-my-pi/pi-catalog/types";
12
12
  /** Retained-message budget Codex uses after streamed V2 compaction. */
13
13
  export declare const V2_RETAINED_MESSAGE_TOKEN_BUDGET = 64000;
@@ -85,6 +85,7 @@ export declare function requestCompactionV2Streaming(model: Model, apiKey: strin
85
85
  providerSessionState?: Map<string, ProviderSessionState>;
86
86
  codexCompaction?: CodexCompactionContext;
87
87
  preferWebsockets?: boolean;
88
+ codexAccessPrograms?: boolean;
88
89
  }): Promise<CompactionV2Response>;
89
90
  /** Build Codex-style V2 replacement history from retention candidates plus compaction output. */
90
91
  export declare function buildCompactionV2ReplacementHistory(input: unknown[], compactionItem: Record<string, unknown>, retainedMessageBudget?: number): {
@@ -256,6 +256,8 @@ export interface SummaryOptions {
256
256
  providerSessionState?: Map<string, ProviderSessionState>;
257
257
  /** Whether Codex remote compaction should prefer the provider WebSocket transport. */
258
258
  preferWebsockets?: boolean;
259
+ /** Daybreak Blue request on Codex remote compaction; see `StreamOptions.codexAccessPrograms`. */
260
+ codexAccessPrograms?: boolean;
259
261
  /** Classification shared by every provider request in this logical compaction. */
260
262
  codexCompaction?: CodexCompactionContext;
261
263
  /** Provider-visible tools for remote compaction transports that replay native tool history. */
@@ -15,4 +15,3 @@ export * from "./tool-context.js";
15
15
  export * from "./tokenizer.js";
16
16
  export * from "./tool-arguments.js";
17
17
  export * from "./types.js";
18
- export * from "./utils/yield.js";
@@ -1,4 +1,4 @@
1
- import type { Message, Tool } from "@oh-my-pi/pi-ai";
1
+ import type { Context, Tool } from "@oh-my-pi/pi-ai";
2
2
  /**
3
3
  * Last wire definition this Agent sent for each tool name, so a provider that keeps
4
4
  * withdrawn tools declared (Anthropic `tool_removal`) can re-declare them byte-identically.
@@ -9,9 +9,10 @@ export declare class SentToolDefinitions {
9
9
  /** Remember the definitions a request is about to send. */
10
10
  record(tools: readonly Tool[]): void;
11
11
  /**
12
- * Definitions for names the latest `requestControls.tools.declared` in `messages` holds
13
- * that are not in `active`; undefined when none. Names never sent by this Agent are
14
- * skipped: the provider drops them from the declaration.
12
+ * `context` with {@link Context.inactiveTools} set to the definitions for names the latest
13
+ * `requestControls.tools.declared` in its messages holds that are not in its `tools`; cleared when
14
+ * there are none or the request declares no tools. Names never sent by this Agent are skipped: the
15
+ * provider drops them from the declaration.
15
16
  */
16
- inactiveFor(messages: readonly Message[], active: readonly Tool[]): Tool[] | undefined;
17
+ withInactiveTools(context: Context): Context;
17
18
  }
@@ -1111,6 +1111,11 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
1111
1111
  * approving the call can record them. Omit it (or return `undefined`) and the call prompts by tier.
1112
1112
  */
1113
1113
  approvalCacheTargets?: (args: unknown) => readonly string[] | undefined;
1114
+ /**
1115
+ * Paths a call reads beyond its working directory, matched against restricted folders when it
1116
+ * reports no `approvalCacheTargets`. Unlike cache targets they never key a remembered grant.
1117
+ */
1118
+ approvalRestrictionPaths?: (args: unknown) => readonly string[] | undefined;
1114
1119
  /** The main execution callback for this tool. */
1115
1120
  execute: AgentToolExecFn<TParameters, TDetails, TTheme>;
1116
1121
  /** Optional custom rendering for tool call display (returns UI component) */
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.13.3",
4
+ "version": "18.14.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.13.3",
42
- "@oh-my-pi/pi-catalog": "18.13.3",
43
- "@oh-my-pi/pi-natives": "18.13.3",
44
- "@oh-my-pi/pi-utils": "18.13.3",
45
- "@oh-my-pi/pi-wire": "18.13.3",
46
- "@oh-my-pi/snapcompact": "18.13.3",
41
+ "@oh-my-pi/pi-ai": "18.14.1",
42
+ "@oh-my-pi/pi-catalog": "18.14.1",
43
+ "@oh-my-pi/pi-natives": "18.14.1",
44
+ "@oh-my-pi/pi-utils": "18.14.1",
45
+ "@oh-my-pi/pi-wire": "18.14.1",
46
+ "@oh-my-pi/snapcompact": "18.14.1",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.13.3",
50
+ "@oh-my-pi/omptype": "18.14.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"
@@ -60,7 +60,6 @@
60
60
  "README.md",
61
61
  "CHANGELOG.md",
62
62
  "LICENSE",
63
- "THIRD-PARTY-NOTICES.txt",
64
63
  "dist/types"
65
64
  ],
66
65
  "exports": {
package/src/agent-loop.ts CHANGED
@@ -109,7 +109,6 @@ import {
109
109
  isSoftToolRequirement,
110
110
  SPECULATIVE_STREAM_SESSION,
111
111
  } from "./types";
112
- import { yieldIfDue } from "./utils/yield";
113
112
 
114
113
  async function waitForProcessPause(config: AgentLoopConfig, signal?: AbortSignal): Promise<void> {
115
114
  try {
@@ -944,7 +943,8 @@ export function normalizeMessagesForProvider(
944
943
  });
945
944
  }
946
945
 
947
- const INTENT_FIELD_DESCRIPTION = "concise intent";
946
+ /** Schema description of the injected `i` field; `PI_INTENT_DESC` replaces it. */
947
+ const DEFAULT_INTENT_FIELD_DESCRIPTION = "concise intent";
948
948
  const INTENT_SCHEMA_UNION_KEYS = ["anyOf", "oneOf"] as const;
949
949
 
950
950
  // Memoize injection per input schema identity: normalizeTools runs on every
@@ -1021,7 +1021,7 @@ function injectIntentIntoSchema(
1021
1021
  ...schemaRecord,
1022
1022
  properties: {
1023
1023
  [INTENT_FIELD]: describeIntent
1024
- ? { type: "string", description: INTENT_FIELD_DESCRIPTION }
1024
+ ? { type: "string", description: Bun.env.PI_INTENT_DESC || DEFAULT_INTENT_FIELD_DESCRIPTION }
1025
1025
  : { type: "string" },
1026
1026
  ...properties,
1027
1027
  },
@@ -1071,13 +1071,6 @@ function resolveIntentMode(intent: AgentTool["intent"]): "require" | "optional"
1071
1071
  return "require";
1072
1072
  }
1073
1073
 
1074
- /**
1075
- * Longest `i` value accepted as an intent. The injected field is described as
1076
- * a "concise intent" (INTENT_FIELD_DESCRIPTION); anything past this is a tool
1077
- * payload the model put in the wrong field, not a label.
1078
- */
1079
- const MAX_INTENT_LENGTH = 200;
1080
-
1081
1074
  function extractIntent(args: Record<string, unknown>): { intent?: string; strippedArgs: Record<string, unknown> } {
1082
1075
  const { [INTENT_FIELD]: intent, ...strippedArgs } = args;
1083
1076
  if (typeof intent !== "string") {
@@ -1336,9 +1329,6 @@ async function runLoopBody(
1336
1329
  endAgentStream(stream, newMessages, telemetry, stepCounter.count);
1337
1330
  return;
1338
1331
  }
1339
- // Yield at the top of each iteration to prevent busy-wait when
1340
- // the agent loop is executing tool calls back-to-back.
1341
- await yieldIfDue();
1342
1332
  // Park at the turn boundary while the process-wide pause gate is
1343
1333
  // engaged (host /pause). An external abort releases the park so a
1344
1334
  // cancelled run still unwinds while everything else stays frozen.
@@ -1980,6 +1970,9 @@ async function prepareProviderCall(
1980
1970
  }),
1981
1971
  };
1982
1972
  }
1973
+ // Before the transform too, so a request-size clamp in it measures the withdrawn definitions the provider
1974
+ // re-declares; recomputed below against what the transform returns.
1975
+ if (config.sentToolDefinitions) llmContext = config.sentToolDefinitions.withInactiveTools(llmContext);
1983
1976
  if (config.transformProviderContext) {
1984
1977
  llmContext = await config.transformProviderContext(llmContext, model);
1985
1978
  }
@@ -1995,10 +1988,9 @@ async function prepareProviderCall(
1995
1988
  };
1996
1989
  }
1997
1990
  // After `transformProviderContext`, so the recorded definitions are exactly what the provider receives.
1998
- if (config.sentToolDefinitions && llmContext.tools) {
1999
- config.sentToolDefinitions.record(llmContext.tools);
2000
- const inactiveTools = config.sentToolDefinitions.inactiveFor(llmContext.messages, llmContext.tools);
2001
- if (inactiveTools) llmContext = { ...llmContext, inactiveTools };
1991
+ if (config.sentToolDefinitions) {
1992
+ if (llmContext.tools) config.sentToolDefinitions.record(llmContext.tools);
1993
+ llmContext = config.sentToolDefinitions.withInactiveTools(llmContext);
2002
1994
  }
2003
1995
  return { model, context: llmContext, promptToolWireTools, ownedDialect };
2004
1996
  }
@@ -2379,10 +2371,6 @@ async function streamAssistantResponse(
2379
2371
  return await finishAbortedStream();
2380
2372
  }
2381
2373
 
2382
- // Yield to the event loop periodically to prevent busy-wait
2383
- // when the LLM is streaming chunks faster than the loop can rest.
2384
- await yieldIfDue();
2385
-
2386
2374
  if (event.type === "toolcall_start") {
2387
2375
  const block = event.partial.content[event.contentIndex];
2388
2376
  if (block?.type === "toolCall") {
@@ -2790,14 +2778,7 @@ function buildToolCallAbortMessages(
2790
2778
  * `signal.reason` is the default `AbortError` `DOMException`) falls back to the
2791
2779
  * generic sentinel that downstream renderers treat as "no specific reason". */
2792
2780
  export function abortReasonText(signal: AbortSignal | undefined): string {
2793
- const scopedReason = toolScopedAbortReason(signal);
2794
- if (scopedReason) return scopedReason.message;
2795
- const reason = signal?.reason;
2796
- if (typeof reason === "string" && reason.trim().length > 0) return reason;
2797
- if (reason instanceof Error && reason.name !== "AbortError" && reason.message.trim().length > 0) {
2798
- return reason.message;
2799
- }
2800
- return "Request was aborted";
2781
+ return toolScopedAbortReason(signal)?.message ?? AIError.callerAbortReason(signal) ?? "Request was aborted";
2801
2782
  }
2802
2783
 
2803
2784
  function emitAbortedAssistantMessage(
@@ -3120,11 +3101,6 @@ async function prepareToolCallDispatch(
3120
3101
  ? { intent: undefined, strippedArgs: argsForExecution }
3121
3102
  : extractIntent(toolCall.arguments);
3122
3103
  argsForExecution = strippedArgs;
3123
- if (intent !== undefined && intent.length > MAX_INTENT_LENGTH && tool) {
3124
- entry.args = strippedArgs;
3125
- entry.validationErrorMessage = `\`${INTENT_FIELD}\` is a short intent label (at most ${MAX_INTENT_LENGTH} chars); the value you sent is ${intent.length} chars. The tool was not run. Put that content in the tool's own parameters and retry with a brief \`${INTENT_FIELD}\`.`;
3126
- continue;
3127
- }
3128
3104
  if (intent) {
3129
3105
  toolCall.intent = intent;
3130
3106
  } else if (typeof tool?.intent === "function") {
@@ -3917,9 +3893,6 @@ async function executeToolCalls(
3917
3893
  await steeringWatchPromise?.catch(() => undefined);
3918
3894
  clearInterval(steeringWatchTimer);
3919
3895
  }
3920
- // Yield after batch tool execution to let GC and I/O catch up,
3921
- // especially when tool results are large (e.g. bash output).
3922
- await yieldIfDue();
3923
3896
 
3924
3897
  for (const record of records) {
3925
3898
  if (!record.toolResultMessage) {
package/src/agent.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  type ToolResultMessage,
24
24
  } from "@oh-my-pi/pi-ai";
25
25
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
26
- import { openAIResponsesReplaysNativeHistory } from "@oh-my-pi/pi-ai/providers/openai-responses";
26
+ import { openAIResponsesReplaysNativeHistory } from "@oh-my-pi/pi-ai/providers/openai-responses-state";
27
27
  import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
28
28
  import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
29
29
  import { logger } from "@oh-my-pi/pi-utils";
@@ -69,7 +69,6 @@ import type {
69
69
  ToolChoiceDirective,
70
70
  } from "./types";
71
71
  import { isSoftToolRequirement } from "./types";
72
- import { EventLoopKeepalive } from "./utils/yield";
73
72
 
74
73
  /**
75
74
  * Default convertToLlm: Keep only LLM-compatible replay messages.
@@ -643,6 +642,12 @@ export class Agent {
643
642
  this.#promptCacheKey = value;
644
643
  }
645
644
 
645
+ /**
646
+ * Fields added to every log record written while this agent runs (the
647
+ * owning session's `sessionId`), so in-process agents' records stay apart.
648
+ */
649
+ logContext: Readonly<Record<string, unknown>> | undefined;
650
+
646
651
  /**
647
652
  * Static metadata forwarded to every API request when no resolver is installed
648
653
  * (e.g. `metadata.user_id` for Anthropic session attribution). Setting this
@@ -819,6 +824,23 @@ export class Agent {
819
824
  this.#pruneToolDescriptions = value;
820
825
  }
821
826
 
827
+ /**
828
+ * Whether requests to `model` actually ship description-stripped tool specs.
829
+ * Owned dialects render full definitions in-band, so they never prune; the
830
+ * loop applies the same rule (`pruneToolDescriptions && !ownedDialect`).
831
+ * Context accounting reads this instead of the raw setting.
832
+ */
833
+ prunesToolDescriptions(model: Model | undefined): boolean {
834
+ return this.#pruneToolDescriptions && model !== undefined && !this.#ownedDialect(model);
835
+ }
836
+
837
+ #ownedDialect(model: Model): Dialect | undefined {
838
+ return (
839
+ (this.#dialectResolver ? this.#dialectResolver(model) : this.#dialect) ??
840
+ resolveOwnedDialectFromEnv(Bun.env.PI_DIALECT)
841
+ );
842
+ }
843
+
822
844
  /** Inject/strip the intent field on tool calls; applies from the next prompt run. */
823
845
  get intentTracing(): boolean {
824
846
  return this.#intentTracing;
@@ -928,9 +950,7 @@ export class Agent {
928
950
  ): Promise<Context> {
929
951
  const model = this.#state.model;
930
952
  if (!model) throw new Error("No active model on agent");
931
- const ownedDialect =
932
- (this.#dialectResolver ? this.#dialectResolver(model) : this.#dialect) ??
933
- resolveOwnedDialectFromEnv(Bun.env.PI_DIALECT);
953
+ const ownedDialect = this.#ownedDialect(model);
934
954
  const messages = normalizeMessagesForProvider(llmMessages, model);
935
955
  const tools = ownedDialect
936
956
  ? []
@@ -939,12 +959,11 @@ export class Agent {
939
959
  pruneDescriptions: this.#pruneToolDescriptions,
940
960
  }) ?? []);
941
961
  let context: Context = { systemPrompt, messages, tools };
962
+ // Side requests reuse the main loop's sent definitions without recording their own; attached before the
963
+ // transform too, so a request-size clamp in it measures them.
964
+ context = this.#sentToolDefinitions.withInactiveTools(context);
942
965
  if (this.#transformProviderContext) context = await this.#transformProviderContext(context, model);
943
- // Side requests reuse the main loop's sent definitions without recording their own.
944
- if (context.tools?.length) {
945
- const inactiveTools = this.#sentToolDefinitions.inactiveFor(context.messages, context.tools);
946
- if (inactiveTools) context = { ...context, inactiveTools };
947
- }
966
+ context = this.#sentToolDefinitions.withInactiveTools(context);
948
967
  return context;
949
968
  }
950
969
 
@@ -1653,7 +1672,7 @@ export class Agent {
1653
1672
  promptOptions = imagesOrOptions as AgentPromptOptions | undefined;
1654
1673
  }
1655
1674
 
1656
- await this.#runLoop(msgs, promptOptions);
1675
+ await this.#inLogContext(() => this.#runLoop(msgs, promptOptions));
1657
1676
  }
1658
1677
 
1659
1678
  /**
@@ -1677,7 +1696,12 @@ export class Agent {
1677
1696
  return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
1678
1697
  }
1679
1698
 
1680
- async continue(signal?: AbortSignal) {
1699
+ continue(signal?: AbortSignal): Promise<void> {
1700
+ // Queued-message hooks run before the loop and are this agent's work too.
1701
+ return this.#inLogContext(() => this.#continue(signal));
1702
+ }
1703
+
1704
+ async #continue(signal?: AbortSignal) {
1681
1705
  if (this.#state.isStreaming) {
1682
1706
  throw new AgentBusyError();
1683
1707
  }
@@ -1759,6 +1783,11 @@ export class Agent {
1759
1783
  }
1760
1784
  }
1761
1785
 
1786
+ /** Run `fn` with {@link logContext} on every record it logs. */
1787
+ #inLogContext<T>(fn: () => T): T {
1788
+ return this.logContext ? logger.withContext(this.logContext, fn) : fn();
1789
+ }
1790
+
1762
1791
  /**
1763
1792
  * Run the agent loop.
1764
1793
  * If messages are provided, starts a new conversation turn with those messages.
@@ -1774,7 +1803,6 @@ export class Agent {
1774
1803
  if (!model) throw new Error("No model configured");
1775
1804
 
1776
1805
  let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
1777
- using _ = new EventLoopKeepalive();
1778
1806
  if (!runStateClaimed) {
1779
1807
  const { promise, resolve } = Promise.withResolvers<void>();
1780
1808
  this.#runningPrompt = promise;
@@ -18,7 +18,6 @@ import type {
18
18
  Usage,
19
19
  } from "@oh-my-pi/pi-ai";
20
20
  import * as AIError from "@oh-my-pi/pi-ai/error";
21
- import { resolveAnthropicCompactionEffort } from "@oh-my-pi/pi-ai/providers/anthropic";
22
21
  import {
23
22
  supportsAnthropicCompaction,
24
23
  supportsBedrockConverseCompaction,
@@ -251,6 +250,14 @@ export async function requestAnthropicNativeCompaction(
251
250
  signal: AbortSignal | undefined,
252
251
  options: AnthropicNativeCompactionOptions,
253
252
  ): Promise<AnthropicNativeCompactionResponse> {
253
+ // Pure compaction policy checks must not load the provider; only an actual request needs it.
254
+ const reasoning = isAnthropicMessagesModel(model)
255
+ ? (await import("@oh-my-pi/pi-ai/providers/anthropic")).resolveAnthropicCompactionEffort(
256
+ model,
257
+ request.context.messages,
258
+ request.reasoning,
259
+ )
260
+ : request.reasoning;
254
261
  const response = await instrumentedCompleteSimple(
255
262
  model,
256
263
  request.context,
@@ -258,9 +265,7 @@ export async function requestAnthropicNativeCompaction(
258
265
  apiKey,
259
266
  signal,
260
267
  maxTokens: request.maxTokens,
261
- reasoning: isAnthropicMessagesModel(model)
262
- ? resolveAnthropicCompactionEffort(model, request.context.messages, request.reasoning)
263
- : request.reasoning,
268
+ reasoning,
264
269
  initiatorOverride: options.initiatorOverride,
265
270
  metadata: options.metadata,
266
271
  fetch: options.fetch,
@@ -11,11 +11,9 @@ import type { Api, CodexCompactionContext, FetchImpl, Model, ProviderSessionStat
11
11
  import * as AIError from "@oh-my-pi/pi-ai/error";
12
12
  import { createOpenAICodexCompactionRequestContext } from "@oh-my-pi/pi-ai/providers/openai-codex-compaction";
13
13
  import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
14
- import {
15
- createOpenAICodexCompatibilityMetadata,
16
- openCodexCompactionEventStream,
17
- type OpenAICodexCompactionBody,
18
- type OpenAICodexCompatibilityMetadata,
14
+ import type {
15
+ OpenAICodexCompactionBody,
16
+ OpenAICodexCompatibilityMetadata,
19
17
  } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
20
18
  import {
21
19
  getOpenAIPromptCacheKey,
@@ -276,6 +274,7 @@ export async function requestCompactionV2Streaming(
276
274
  providerSessionState?: Map<string, ProviderSessionState>;
277
275
  codexCompaction?: CodexCompactionContext;
278
276
  preferWebsockets?: boolean;
277
+ codexAccessPrograms?: boolean;
279
278
  },
280
279
  ): Promise<CompactionV2Response> {
281
280
  let fetchImpl: FetchImpl = options?.fetch ?? globalThis.fetch;
@@ -293,18 +292,21 @@ export async function requestCompactionV2Streaming(
293
292
 
294
293
  const retryWait = options?.retryWait ?? ((delayMs: number) => Bun.sleep(delayMs));
295
294
  const isCodexResponses = compactionV2Api(model) === "openai-codex-responses" || model.provider === "openai-codex";
296
- const codexMetadata =
297
- isCodexResponses && !shouldUseCodexProviderTransport(model)
298
- ? createOpenAICodexCompatibilityMetadata({
299
- sessionId: request.sessionId,
300
- providerSessionState: options?.providerSessionState,
301
- requestKind: "compaction",
302
- compaction: createOpenAICodexCompactionRequestContext({
303
- context: options?.codexCompaction,
304
- implementation: "responses_compaction_v2",
305
- }),
306
- })
307
- : undefined;
295
+ let codexMetadata: OpenAICodexCompatibilityMetadata | undefined;
296
+ if (isCodexResponses && !shouldUseCodexProviderTransport(model)) {
297
+ // Metadata generation is request work; importing compaction policy must not load the Codex transport.
298
+ const { createOpenAICodexCompatibilityMetadata } =
299
+ await import("@oh-my-pi/pi-ai/providers/openai-codex-responses");
300
+ codexMetadata = createOpenAICodexCompatibilityMetadata({
301
+ sessionId: request.sessionId,
302
+ providerSessionState: options?.providerSessionState,
303
+ requestKind: "compaction",
304
+ compaction: createOpenAICodexCompactionRequestContext({
305
+ context: options?.codexCompaction,
306
+ implementation: "responses_compaction_v2",
307
+ }),
308
+ });
309
+ }
308
310
  let lastError: Error | undefined;
309
311
 
310
312
  for (let attempt = 0; attempt <= V2_COMPACTION_MAX_RETRIES; attempt++) {
@@ -315,6 +317,7 @@ export async function requestCompactionV2Streaming(
315
317
  providerSessionState: options?.providerSessionState,
316
318
  codexCompaction: options?.codexCompaction,
317
319
  preferWebsockets: options?.preferWebsockets,
320
+ codexAccessPrograms: options?.codexAccessPrograms,
318
321
  });
319
322
  } catch (err) {
320
323
  const error = err instanceof Error ? err : new Error(String(err));
@@ -351,6 +354,7 @@ async function attemptCompactionV2Streaming(
351
354
  providerSessionState?: Map<string, ProviderSessionState>;
352
355
  codexCompaction?: CodexCompactionContext;
353
356
  preferWebsockets?: boolean;
357
+ codexAccessPrograms?: boolean;
354
358
  },
355
359
  ): Promise<CompactionV2Response> {
356
360
  // Faithful to Codex: append the compaction trigger as the final input item
@@ -367,6 +371,8 @@ async function attemptCompactionV2Streaming(
367
371
  }
368
372
 
369
373
  if (shouldUseCodexProviderTransport(model)) {
374
+ // The native transport is only needed when sending a Codex compaction request.
375
+ const { openCodexCompactionEventStream } = await import("@oh-my-pi/pi-ai/providers/openai-codex-responses");
370
376
  const eventStream = await openCodexCompactionEventStream(model, body, {
371
377
  apiKey,
372
378
  signal,
@@ -374,6 +380,7 @@ async function attemptCompactionV2Streaming(
374
380
  sessionId: request.sessionId,
375
381
  providerSessionState: options.providerSessionState,
376
382
  preferWebsockets: options.preferWebsockets,
383
+ codexAccessPrograms: options.codexAccessPrograms,
377
384
  responsesLite: model.useResponsesLite,
378
385
  codexCompaction: createOpenAICodexCompactionRequestContext({
379
386
  context: options.codexCompaction,
@@ -27,12 +27,8 @@ import {
27
27
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
28
28
  import * as AIError from "@oh-my-pi/pi-ai/error";
29
29
  import { createOpenAICodexCompactionRequestContext } from "@oh-my-pi/pi-ai/providers/openai-codex-compaction";
30
- import {
31
- buildTransformedCodexRequestBody,
32
- type OpenAICodexCompactionBody,
33
- } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
30
+ import type { OpenAICodexCompactionBody } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
34
31
  import type { InputItem as CodexInputItem } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
35
- import { convertTools } from "@oh-my-pi/pi-ai/providers/openai-responses";
36
32
  import { buildResponsesInput, resolveOpenAICompatPolicy } from "@oh-my-pi/pi-ai/providers/openai-shared";
37
33
  import { isSurvivingAssistant } from "@oh-my-pi/pi-ai/providers/transform-messages";
38
34
  import { stripOpenAIResponsesOutputOnlyStatusesForReplay } from "@oh-my-pi/pi-ai/utils";
@@ -754,6 +750,8 @@ export interface SummaryOptions {
754
750
  providerSessionState?: Map<string, ProviderSessionState>;
755
751
  /** Whether Codex remote compaction should prefer the provider WebSocket transport. */
756
752
  preferWebsockets?: boolean;
753
+ /** Daybreak Blue request on Codex remote compaction; see `StreamOptions.codexAccessPrograms`. */
754
+ codexAccessPrograms?: boolean;
757
755
  /** Classification shared by every provider request in this logical compaction. */
758
756
  codexCompaction?: CodexCompactionContext;
759
757
  /** Provider-visible tools for remote compaction transports that replay native tool history. */
@@ -1769,6 +1767,7 @@ export async function compact(
1769
1767
  : options?.promptCacheKey,
1770
1768
  providerSessionState: options?.providerSessionState,
1771
1769
  preferWebsockets: options?.preferWebsockets,
1770
+ codexAccessPrograms: options?.codexAccessPrograms,
1772
1771
  codexCompaction: options?.codexCompaction,
1773
1772
  tools: options?.tools,
1774
1773
  fetch: options?.fetch,
@@ -1890,6 +1889,9 @@ export async function compact(
1890
1889
  let codexBody: OpenAICodexCompactionBody | undefined;
1891
1890
  let remoteHistory: Array<Record<string, unknown>>;
1892
1891
  if (isCodexResponsesModel(model)) {
1892
+ // Load the serializer only when building a Codex compaction request, not session policy setup.
1893
+ const { buildTransformedCodexRequestBody } =
1894
+ await import("@oh-my-pi/pi-ai/providers/openai-codex-responses");
1893
1895
  const previousCodexInput: CodexInputItem[] = [];
1894
1896
  for (const item of previousReplacementHistory ?? []) {
1895
1897
  if (!isCodexInputItem(item)) {
@@ -1950,12 +1952,17 @@ export async function compact(
1950
1952
  ? codexBody.instructions
1951
1953
  : ""
1952
1954
  : remoteSystemPrompt.join("\n\n");
1955
+ // Responses serialization is needed only for this native compaction request.
1953
1956
  const tools = codexBody
1954
1957
  ? Array.isArray(codexBody.tools)
1955
1958
  ? codexBody.tools
1956
1959
  : undefined
1957
1960
  : summaryOptions.tools
1958
- ? convertTools(summaryOptions.tools, model.compat.supportsStrictMode, model)
1961
+ ? (await import("@oh-my-pi/pi-ai/providers/openai-responses")).convertTools(
1962
+ summaryOptions.tools,
1963
+ model.compat.supportsStrictMode,
1964
+ model,
1965
+ )
1959
1966
  : undefined;
1960
1967
  const trimmed = trimRemoteCompactionInputToContextWindow(
1961
1968
  remoteHistory,
@@ -1996,6 +2003,7 @@ export async function compact(
1996
2003
  fetch: summaryOptions.fetch,
1997
2004
  providerSessionState: summaryOptions.providerSessionState,
1998
2005
  preferWebsockets: summaryOptions.preferWebsockets,
2006
+ codexAccessPrograms: summaryOptions.codexAccessPrograms,
1999
2007
  codexCompaction: summaryOptions.codexCompaction,
2000
2008
  }),
2001
2009
  { signal },