@oh-my-pi/pi-agent-core 18.14.0 → 18.14.2

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,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.14.2] - 2026-10-31
6
+
7
+ ### Added
8
+
9
+ - The soft steering signal a tool sees as `toolCall.steeringSignal` now aborts with the source of the queued message (`"user"`, `"irc"`, …) as its `reason`, so a tool can yield only to the sources it should ([#17537](https://github.com/can1357/oh-my-pi/issues/17537))
10
+
11
+ ### Fixed
12
+
13
+ - Reject incomplete local compaction, turn-prefix, handoff, and branch summaries ending with `stopReason: "length"` instead of using truncated text as complete context. Omit incomplete display-only short summaries without discarding a completed full summary ([#17499](https://github.com/can1357/oh-my-pi/pull/17499) by [@Xytronix](https://github.com/Xytronix)).
14
+
15
+ ## [18.14.1] - 2026-10-31
16
+
17
+ ### Breaking Changes
18
+
19
+ - `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))
20
+
21
+ ### Fixed
22
+
23
+ - Long tool-call intents no longer block tool execution when intent tracing is enabled ([#13140](https://github.com/can1357/oh-my-pi/issues/13140)).
24
+
5
25
  ## [18.14.0] - 2026-10-29
6
26
 
7
27
  ### Breaking Changes
@@ -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. */
@@ -10,6 +10,8 @@ export interface FileOperations {
10
10
  edited: Set<string>;
11
11
  }
12
12
  export declare function createFileOps(): FileOperations;
13
+ /** Error text for a summarization call that hit its output limit (`stopReason: "length"`): partial text is never used. */
14
+ export declare function incompleteOutputError(prefix: string): string;
13
15
  /**
14
16
  * Split a read-tool path into its base path and trailing selector, mirroring the
15
17
  * read tool's own splitter. Single source of the grammar in this package: the
@@ -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
  }
@@ -588,7 +588,8 @@ export interface ToolCallContext {
588
588
  providerMetadata?: ToolCallProviderMetadata;
589
589
  /**
590
590
  * Cooperative steering signal: aborted when a queued user/steering message
591
- * (or an interrupting peer IRC) is detected while this tool batch runs.
591
+ * (or an interrupting peer IRC) is detected while this tool batch runs. Its
592
+ * `reason` names the source: a {@link SteeringInterruptSource} or `"irc"`.
592
593
  * Unlike the hard abort signal it NEVER kills the tool — long-running
593
594
  * tools MAY observe it (via `ctx.toolCall.steeringSignal`) to finish early
594
595
  * or background themselves so the message injects promptly; ignoring it is
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.14.0",
4
+ "version": "18.14.2",
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.14.0",
42
- "@oh-my-pi/pi-catalog": "18.14.0",
43
- "@oh-my-pi/pi-natives": "18.14.0",
44
- "@oh-my-pi/pi-utils": "18.14.0",
45
- "@oh-my-pi/pi-wire": "18.14.0",
46
- "@oh-my-pi/snapcompact": "18.14.0",
41
+ "@oh-my-pi/pi-ai": "18.14.2",
42
+ "@oh-my-pi/pi-catalog": "18.14.2",
43
+ "@oh-my-pi/pi-natives": "18.14.2",
44
+ "@oh-my-pi/pi-utils": "18.14.2",
45
+ "@oh-my-pi/pi-wire": "18.14.2",
46
+ "@oh-my-pi/snapcompact": "18.14.2",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.14.0",
50
+ "@oh-my-pi/omptype": "18.14.2",
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"
package/src/agent-loop.ts CHANGED
@@ -1071,12 +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; anything past this is a tool
1076
- * payload the model put in the wrong field, not an intent.
1077
- */
1078
- const MAX_INTENT_LENGTH = 200;
1079
-
1080
1074
  function extractIntent(args: Record<string, unknown>): { intent?: string; strippedArgs: Record<string, unknown> } {
1081
1075
  const { [INTENT_FIELD]: intent, ...strippedArgs } = args;
1082
1076
  if (typeof intent !== "string") {
@@ -1976,6 +1970,9 @@ async function prepareProviderCall(
1976
1970
  }),
1977
1971
  };
1978
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);
1979
1976
  if (config.transformProviderContext) {
1980
1977
  llmContext = await config.transformProviderContext(llmContext, model);
1981
1978
  }
@@ -1991,10 +1988,9 @@ async function prepareProviderCall(
1991
1988
  };
1992
1989
  }
1993
1990
  // After `transformProviderContext`, so the recorded definitions are exactly what the provider receives.
1994
- if (config.sentToolDefinitions && llmContext.tools) {
1995
- config.sentToolDefinitions.record(llmContext.tools);
1996
- const inactiveTools = config.sentToolDefinitions.inactiveFor(llmContext.messages, llmContext.tools);
1997
- if (inactiveTools) llmContext = { ...llmContext, inactiveTools };
1991
+ if (config.sentToolDefinitions) {
1992
+ if (llmContext.tools) config.sentToolDefinitions.record(llmContext.tools);
1993
+ llmContext = config.sentToolDefinitions.withInactiveTools(llmContext);
1998
1994
  }
1999
1995
  return { model, context: llmContext, promptToolWireTools, ownedDialect };
2000
1996
  }
@@ -3105,11 +3101,6 @@ async function prepareToolCallDispatch(
3105
3101
  ? { intent: undefined, strippedArgs: argsForExecution }
3106
3102
  : extractIntent(toolCall.arguments);
3107
3103
  argsForExecution = strippedArgs;
3108
- if (intent !== undefined && intent.length > MAX_INTENT_LENGTH && tool) {
3109
- entry.args = strippedArgs;
3110
- 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}\`.`;
3111
- continue;
3112
- }
3113
3104
  if (intent) {
3114
3105
  toolCall.intent = intent;
3115
3106
  } else if (typeof tool?.intent === "function") {
@@ -3411,7 +3402,7 @@ async function executeToolCalls(
3411
3402
  // also cascades: the freshly detached job's own completion re-triggers
3412
3403
  // this check for the next command, so millisecond-long commands chain
3413
3404
  // into separate background deliveries (#12869).
3414
- if (source === "irc" && softInterrupts) steeringSoftController.abort();
3405
+ if (source === "irc" && softInterrupts) steeringSoftController.abort("irc");
3415
3406
  };
3416
3407
 
3417
3408
  const checkSteering = async (): Promise<void> => {
@@ -3450,7 +3441,7 @@ async function executeToolCalls(
3450
3441
  interruptState.triggered = true;
3451
3442
  interruptState.source = steeringSource ?? "unknown";
3452
3443
  steeringAbortController.abort(TOOL_INTERRUPT_ABORT_REASON);
3453
- if (softInterrupts) steeringSoftController.abort();
3444
+ if (softInterrupts) steeringSoftController.abort(interruptState.source);
3454
3445
  }
3455
3446
  return;
3456
3447
  }
package/src/agent.ts CHANGED
@@ -959,12 +959,11 @@ export class Agent {
959
959
  pruneDescriptions: this.#pruneToolDescriptions,
960
960
  }) ?? []);
961
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);
962
965
  if (this.#transformProviderContext) context = await this.#transformProviderContext(context, model);
963
- // Side requests reuse the main loop's sent definitions without recording their own.
964
- if (context.tools?.length) {
965
- const inactiveTools = this.#sentToolDefinitions.inactiveFor(context.messages, context.tools);
966
- if (inactiveTools) context = { ...context, inactiveTools };
967
- }
966
+ context = this.#sentToolDefinitions.withInactiveTools(context);
968
967
  return context;
969
968
  }
970
969
 
@@ -28,6 +28,7 @@ import {
28
28
  createFileOps,
29
29
  extractFileOpsFromMessage,
30
30
  type FileOperations,
31
+ incompleteOutputError,
31
32
  SUMMARIZATION_SYSTEM_PROMPT,
32
33
  serializeConversationForSummary,
33
34
  stripReadSelector,
@@ -386,6 +387,9 @@ export async function generateBranchSummary(
386
387
  if (response.stopReason === "error") {
387
388
  return { error: response.errorMessage || "Summarization failed" };
388
389
  }
390
+ if (response.stopReason === "length") {
391
+ return { error: incompleteOutputError("Summarization failed") };
392
+ }
389
393
 
390
394
  let summary = response.content
391
395
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -274,6 +274,7 @@ export async function requestCompactionV2Streaming(
274
274
  providerSessionState?: Map<string, ProviderSessionState>;
275
275
  codexCompaction?: CodexCompactionContext;
276
276
  preferWebsockets?: boolean;
277
+ codexAccessPrograms?: boolean;
277
278
  },
278
279
  ): Promise<CompactionV2Response> {
279
280
  let fetchImpl: FetchImpl = options?.fetch ?? globalThis.fetch;
@@ -316,6 +317,7 @@ export async function requestCompactionV2Streaming(
316
317
  providerSessionState: options?.providerSessionState,
317
318
  codexCompaction: options?.codexCompaction,
318
319
  preferWebsockets: options?.preferWebsockets,
320
+ codexAccessPrograms: options?.codexAccessPrograms,
319
321
  });
320
322
  } catch (err) {
321
323
  const error = err instanceof Error ? err : new Error(String(err));
@@ -352,6 +354,7 @@ async function attemptCompactionV2Streaming(
352
354
  providerSessionState?: Map<string, ProviderSessionState>;
353
355
  codexCompaction?: CodexCompactionContext;
354
356
  preferWebsockets?: boolean;
357
+ codexAccessPrograms?: boolean;
355
358
  },
356
359
  ): Promise<CompactionV2Response> {
357
360
  // Faithful to Codex: append the compaction trigger as the final input item
@@ -377,6 +380,7 @@ async function attemptCompactionV2Streaming(
377
380
  sessionId: request.sessionId,
378
381
  providerSessionState: options.providerSessionState,
379
382
  preferWebsockets: options.preferWebsockets,
383
+ codexAccessPrograms: options.codexAccessPrograms,
380
384
  responsesLite: model.useResponsesLite,
381
385
  codexCompaction: createOpenAICodexCompactionRequestContext({
382
386
  context: options.codexCompaction,
@@ -91,6 +91,7 @@ import {
91
91
  escapeSummaryBoundaryTags,
92
92
  extractFileOpsFromMessage,
93
93
  type FileOperations,
94
+ incompleteOutputError,
94
95
  SUMMARIZATION_SYSTEM_PROMPT,
95
96
  serializeConversationForSummary,
96
97
  stripReadSelector,
@@ -750,6 +751,8 @@ export interface SummaryOptions {
750
751
  providerSessionState?: Map<string, ProviderSessionState>;
751
752
  /** Whether Codex remote compaction should prefer the provider WebSocket transport. */
752
753
  preferWebsockets?: boolean;
754
+ /** Daybreak Blue request on Codex remote compaction; see `StreamOptions.codexAccessPrograms`. */
755
+ codexAccessPrograms?: boolean;
753
756
  /** Classification shared by every provider request in this logical compaction. */
754
757
  codexCompaction?: CodexCompactionContext;
755
758
  /** Provider-visible tools for remote compaction transports that replay native tool history. */
@@ -1073,6 +1076,9 @@ async function summarizeConversationWindow(
1073
1076
  if (response.stopReason === "error") {
1074
1077
  throw createSummarizationError("Summarization failed", response);
1075
1078
  }
1079
+ if (response.stopReason === "length") {
1080
+ throw new Error(incompleteOutputError("Summarization failed"));
1081
+ }
1076
1082
 
1077
1083
  const textContent = response.content
1078
1084
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -1180,6 +1186,9 @@ export async function generateHandoffFromContext(
1180
1186
  if (response.stopReason === "error") {
1181
1187
  throw createSummarizationError("Handoff generation failed", response);
1182
1188
  }
1189
+ if (response.stopReason === "length") {
1190
+ throw new Error(incompleteOutputError("Handoff generation failed"));
1191
+ }
1183
1192
 
1184
1193
  return response.content
1185
1194
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -1303,8 +1312,10 @@ async function generateShortSummary(
1303
1312
  case "error":
1304
1313
  outcome = { kind: "failed", error: response.errorMessage || "Unknown error" };
1305
1314
  break;
1306
- case "stop":
1307
1315
  case "length":
1316
+ outcome = { kind: "failed", error: incompleteOutputError("Short summary failed") };
1317
+ break;
1318
+ case "stop":
1308
1319
  case "toolUse":
1309
1320
  outcome = {
1310
1321
  kind: "text",
@@ -1765,6 +1776,7 @@ export async function compact(
1765
1776
  : options?.promptCacheKey,
1766
1777
  providerSessionState: options?.providerSessionState,
1767
1778
  preferWebsockets: options?.preferWebsockets,
1779
+ codexAccessPrograms: options?.codexAccessPrograms,
1768
1780
  codexCompaction: options?.codexCompaction,
1769
1781
  tools: options?.tools,
1770
1782
  fetch: options?.fetch,
@@ -2000,6 +2012,7 @@ export async function compact(
2000
2012
  fetch: summaryOptions.fetch,
2001
2013
  providerSessionState: summaryOptions.providerSessionState,
2002
2014
  preferWebsockets: summaryOptions.preferWebsockets,
2015
+ codexAccessPrograms: summaryOptions.codexAccessPrograms,
2003
2016
  codexCompaction: summaryOptions.codexCompaction,
2004
2017
  }),
2005
2018
  { signal },
@@ -2431,6 +2444,9 @@ async function generateTurnPrefixSummary(
2431
2444
  if (response.stopReason === "error") {
2432
2445
  throw createSummarizationError("Turn prefix summarization failed", response);
2433
2446
  }
2447
+ if (response.stopReason === "length") {
2448
+ throw new Error(incompleteOutputError("Turn prefix summarization failed"));
2449
+ }
2434
2450
 
2435
2451
  return response.content
2436
2452
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -28,6 +28,11 @@ export function createFileOps(): FileOperations {
28
28
  };
29
29
  }
30
30
 
31
+ /** Error text for a summarization call that hit its output limit (`stopReason: "length"`): partial text is never used. */
32
+ export function incompleteOutputError(prefix: string): string {
33
+ return `${prefix}: model output ended incomplete (stopReason: length)`;
34
+ }
35
+
31
36
  // Read-tool selector grammar, mirrored from the conservative filesystem splitter in
32
37
  // packages/coding-agent/src/tools/path-utils.ts (splitPathAndSel). Keep in sync.
33
38
  // A trailing `:chunk` is a selector only when it is a line-range list
@@ -1,4 +1,4 @@
1
- import type { Message, Tool } from "@oh-my-pi/pi-ai";
1
+ import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
2
2
 
3
3
  /**
4
4
  * Last wire definition this Agent sent for each tool name, so a provider that keeps
@@ -14,11 +14,18 @@ export class SentToolDefinitions {
14
14
  }
15
15
 
16
16
  /**
17
- * Definitions for names the latest `requestControls.tools.declared` in `messages` holds
18
- * that are not in `active`; undefined when none. Names never sent by this Agent are
19
- * skipped: the provider drops them from the declaration.
17
+ * `context` with {@link Context.inactiveTools} set to the definitions for names the latest
18
+ * `requestControls.tools.declared` in its messages holds that are not in its `tools`; cleared when
19
+ * there are none or the request declares no tools. Names never sent by this Agent are skipped: the
20
+ * provider drops them from the declaration.
20
21
  */
21
- inactiveFor(messages: readonly Message[], active: readonly Tool[]): Tool[] | undefined {
22
+ withInactiveTools(context: Context): Context {
23
+ const inactiveTools = context.tools?.length ? this.#inactiveFor(context.messages, context.tools) : undefined;
24
+ if (!inactiveTools && !context.inactiveTools) return context;
25
+ return { ...context, inactiveTools };
26
+ }
27
+
28
+ #inactiveFor(messages: readonly Message[], active: readonly Tool[]): Tool[] | undefined {
22
29
  let declared: readonly string[] | undefined;
23
30
  for (let index = messages.length - 1; index >= 0; index--) {
24
31
  const message = messages[index];
package/src/types.ts CHANGED
@@ -672,7 +672,8 @@ export interface ToolCallContext {
672
672
  providerMetadata?: ToolCallProviderMetadata;
673
673
  /**
674
674
  * Cooperative steering signal: aborted when a queued user/steering message
675
- * (or an interrupting peer IRC) is detected while this tool batch runs.
675
+ * (or an interrupting peer IRC) is detected while this tool batch runs. Its
676
+ * `reason` names the source: a {@link SteeringInterruptSource} or `"irc"`.
676
677
  * Unlike the hard abort signal it NEVER kills the tool — long-running
677
678
  * tools MAY observe it (via `ctx.toolCall.steeringSignal`) to finish early
678
679
  * or background themselves so the message injects promptly; ignoring it is