@oh-my-pi/pi-agent-core 16.4.2 → 16.4.4

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.4.3] - 2026-07-11
6
+
7
+ ### Fixed
8
+
9
+ - Fixed an issue where skipped sibling tool results incorrectly reported that a queued user message caused the skip.
10
+
5
11
  ## [16.4.2] - 2026-07-10
6
12
 
7
13
  ### Fixed
@@ -53,6 +53,15 @@ export interface SoftToolRequirement {
53
53
  export type ToolChoiceDirective = ToolChoice | SoftToolRequirement;
54
54
  /** True when a {@link ToolChoiceDirective} is a soft requirement, not a hard choice. */
55
55
  export declare function isSoftToolRequirement(directive: ToolChoiceDirective | undefined): directive is SoftToolRequirement;
56
+ /** Source category for a queued steering interrupt observed without consuming the queue. */
57
+ export type SteeringInterruptSource = "user" | "system" | "unknown";
58
+ /** Non-consuming summary of whether queued steering should interrupt a tool batch. */
59
+ export interface SteeringQueueState {
60
+ /** True when at least one steering message is queued. */
61
+ queued: boolean;
62
+ /** Best-effort origin used only to word synthetic skipped-tool results. */
63
+ source?: SteeringInterruptSource;
64
+ }
56
65
  /**
57
66
  * Configuration for the agent loop.
58
67
  */
@@ -154,10 +163,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
154
163
  * restore queued messages while in-flight tools settle, and an external
155
164
  * abort in that window leaves the queue intact for a post-abort continue.
156
165
  *
166
+ * Returning `true` is treated as user-originated steering for compatibility.
167
+ * Return a {@link SteeringQueueState} when the queue can distinguish system
168
+ * advisories from real user messages.
169
+ *
157
170
  * When omitted, steering never interrupts a running tool batch; queued
158
171
  * messages are still delivered at the next injection boundary.
159
172
  */
160
- hasSteeringMessages?: () => boolean | Promise<boolean>;
173
+ hasSteeringMessages?: () => boolean | SteeringQueueState | Promise<boolean | SteeringQueueState>;
161
174
  /**
162
175
  * Peeks whether IRC messages should interrupt an interruptible waiting tool.
163
176
  *
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": "16.4.2",
4
+ "version": "16.4.4",
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,12 +35,12 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "16.4.2",
39
- "@oh-my-pi/pi-catalog": "16.4.2",
40
- "@oh-my-pi/pi-natives": "16.4.2",
41
- "@oh-my-pi/pi-utils": "16.4.2",
42
- "@oh-my-pi/pi-wire": "16.4.2",
43
- "@oh-my-pi/snapcompact": "16.4.2",
38
+ "@oh-my-pi/pi-ai": "16.4.4",
39
+ "@oh-my-pi/pi-catalog": "16.4.4",
40
+ "@oh-my-pi/pi-natives": "16.4.4",
41
+ "@oh-my-pi/pi-utils": "16.4.4",
42
+ "@oh-my-pi/pi-wire": "16.4.4",
43
+ "@oh-my-pi/snapcompact": "16.4.4",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -66,6 +66,8 @@ import type {
66
66
  AgentToolResult,
67
67
  AgentTurnEndContext,
68
68
  AsideMessage,
69
+ SteeringInterruptSource,
70
+ SteeringQueueState,
69
71
  StreamFn,
70
72
  } from "./types";
71
73
  import { isSoftToolRequirement } from "./types";
@@ -1800,7 +1802,7 @@ async function executeToolCalls(
1800
1802
  const interruptibleSignal: AbortSignal = signal
1801
1803
  ? AbortSignal.any([signal, steeringAbortController.signal, ircAbortController.signal])
1802
1804
  : AbortSignal.any([steeringAbortController.signal, ircAbortController.signal]);
1803
- const interruptState = { triggered: false };
1805
+ const interruptState: { triggered: boolean; source?: SteeringInterruptSource | "irc" } = { triggered: false };
1804
1806
 
1805
1807
  const records = toolCalls.map(toolCall => {
1806
1808
  // Tools emitted via OpenAI's custom-tool path (e.g. `apply_patch` on GPT-5)
@@ -1835,15 +1837,25 @@ async function executeToolCalls(
1835
1837
  // integration only provides getSteeringMessages(), the queue drains at the
1836
1838
  // injection boundary below; polling it here would strand or drop messages.
1837
1839
  let steeringQueued = false;
1840
+ let steeringSource: SteeringInterruptSource | undefined;
1838
1841
  if (hasSteeringMessages) {
1839
- steeringQueued = await hasSteeringMessages();
1842
+ const queuedState = await hasSteeringMessages();
1843
+ if (typeof queuedState === "boolean") {
1844
+ steeringQueued = queuedState;
1845
+ steeringSource = queuedState ? "user" : undefined;
1846
+ } else {
1847
+ const state: SteeringQueueState = queuedState;
1848
+ steeringQueued = state.queued;
1849
+ steeringSource = state.source ?? (state.queued ? "unknown" : undefined);
1850
+ }
1840
1851
  }
1841
1852
  if (steeringQueued) {
1842
- // User steering upgrades an in-flight IRC interrupt: it aborts the
1853
+ // Queued steering upgrades an in-flight IRC interrupt: it aborts the
1843
1854
  // shared signal so foreground tools stop as they do for a user Esc.
1844
1855
  // Idempotent — a second steer poll after the abort is a no-op.
1845
1856
  if (!steeringAbortController.signal.aborted) {
1846
1857
  interruptState.triggered = true;
1858
+ interruptState.source = steeringSource ?? "unknown";
1847
1859
  steeringAbortController.abort();
1848
1860
  }
1849
1861
  return;
@@ -1855,6 +1867,7 @@ async function executeToolCalls(
1855
1867
  // Peer IRC only aborts interruptible waits: a foreground bash / write
1856
1868
  // mid-execution keeps running so we never leave partial side effects.
1857
1869
  interruptState.triggered = true;
1870
+ interruptState.source = "irc";
1858
1871
  ircAbortController.abort();
1859
1872
  }
1860
1873
  };
@@ -2115,7 +2128,7 @@ async function executeToolCalls(
2115
2128
  // This tool's own signal fired AND it failed — it was cut off before producing
2116
2129
  // a usable result, so report it as skipped.
2117
2130
  record.skipped = true;
2118
- emitToolResult(record, createSkippedToolResult(), true);
2131
+ emitToolResult(record, createSkippedToolResult(interruptState.source), true);
2119
2132
  } else {
2120
2133
  // No interrupt on this signal, or the tool finished (successfully or with a
2121
2134
  // genuine error) before the interrupt landed. Keep its real result: a completed
@@ -2209,7 +2222,7 @@ async function executeToolCalls(
2209
2222
  toolName: record.toolCall.name,
2210
2223
  status: "skipped",
2211
2224
  });
2212
- emitToolResult(record, createSkippedToolResult(), true);
2225
+ emitToolResult(record, createSkippedToolResult(interruptState.source), true);
2213
2226
  }
2214
2227
  }
2215
2228
 
@@ -2326,12 +2339,24 @@ function createToolSignalAbortedResult(signal: AbortSignal): AgentToolResult<unk
2326
2339
  };
2327
2340
  }
2328
2341
 
2329
- function createSkippedToolResult(): AgentToolResult<any> {
2342
+ function createSkippedToolResult(source: SteeringInterruptSource | "irc" | undefined): AgentToolResult<any> {
2343
+ let reason = "pending steering message";
2344
+ let blocker = "queued message";
2345
+ if (source === "user") {
2346
+ reason = "queued user message";
2347
+ blocker = "queued message";
2348
+ } else if (source === "system") {
2349
+ reason = "pending system advisory";
2350
+ blocker = "advisory";
2351
+ } else if (source === "irc") {
2352
+ reason = "pending peer interrupt";
2353
+ blocker = "interrupt";
2354
+ }
2330
2355
  return {
2331
2356
  content: [
2332
2357
  {
2333
2358
  type: "text",
2334
- text: "Skipped due to queued user message. Do not count this skipped result as completed work or verification. After the queued message is handled on the next step, retry the skipped tool if it is still needed.",
2359
+ text: `Skipped due to ${reason}. Do not count this skipped result as completed work or verification. After the ${blocker} is handled on the next step, retry the skipped tool if it is still needed.`,
2335
2360
  },
2336
2361
  ],
2337
2362
  details: {},
package/src/agent.ts CHANGED
@@ -1184,7 +1184,19 @@ export class Agent {
1184
1184
  }
1185
1185
  return this.#dequeueSteeringMessages();
1186
1186
  },
1187
- hasSteeringMessages: () => this.#steeringQueue.length > 0,
1187
+ hasSteeringMessages: () => {
1188
+ if (this.#steeringQueue.length === 0) {
1189
+ return { queued: false };
1190
+ }
1191
+ for (const message of this.#steeringQueue) {
1192
+ const role = "role" in message ? message.role : undefined;
1193
+ const attribution = "attribution" in message ? message.attribution : undefined;
1194
+ if (role === "user" && attribution !== "agent") {
1195
+ return { queued: true, source: "user" };
1196
+ }
1197
+ }
1198
+ return { queued: true, source: "system" };
1199
+ },
1188
1200
  hasIrcInterrupts: this.hasIrcInterrupts,
1189
1201
  getFollowUpMessages: async () => this.#dequeueFollowUpMessages(),
1190
1202
  getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
package/src/types.ts CHANGED
@@ -83,6 +83,17 @@ export function isSoftToolRequirement(directive: ToolChoiceDirective | undefined
83
83
  return typeof directive === "object" && directive !== null && (directive as SoftToolRequirement).soft === true;
84
84
  }
85
85
 
86
+ /** Source category for a queued steering interrupt observed without consuming the queue. */
87
+ export type SteeringInterruptSource = "user" | "system" | "unknown";
88
+
89
+ /** Non-consuming summary of whether queued steering should interrupt a tool batch. */
90
+ export interface SteeringQueueState {
91
+ /** True when at least one steering message is queued. */
92
+ queued: boolean;
93
+ /** Best-effort origin used only to word synthetic skipped-tool results. */
94
+ source?: SteeringInterruptSource;
95
+ }
96
+
86
97
  /**
87
98
  * Configuration for the agent loop.
88
99
  */
@@ -194,10 +205,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
194
205
  * restore queued messages while in-flight tools settle, and an external
195
206
  * abort in that window leaves the queue intact for a post-abort continue.
196
207
  *
208
+ * Returning `true` is treated as user-originated steering for compatibility.
209
+ * Return a {@link SteeringQueueState} when the queue can distinguish system
210
+ * advisories from real user messages.
211
+ *
197
212
  * When omitted, steering never interrupts a running tool batch; queued
198
213
  * messages are still delivered at the next injection boundary.
199
214
  */
200
- hasSteeringMessages?: () => boolean | Promise<boolean>;
215
+ hasSteeringMessages?: () => boolean | SteeringQueueState | Promise<boolean | SteeringQueueState>;
201
216
 
202
217
  /**
203
218
  * Peeks whether IRC messages should interrupt an interruptible waiting tool.