@oh-my-pi/pi-agent-core 18.11.1 → 18.11.3

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,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.11.3] - 2026-10-16
6
+
7
+ ### Added
8
+
9
+ - `prepareQueuedMessages` receives which queue (`steering` or `followUp`) a batch came from ([#16026](https://github.com/can1357/oh-my-pi/pull/16026))
10
+
11
+ ## [18.11.2] - 2026-10-16
12
+
13
+ ### Added
14
+
15
+ - `agent_start` carries `resumedToolCallIds` when a continued run first executes the calls of an unpaired assistant tail
16
+
17
+ ### Fixed
18
+
19
+ - Cancelling an `ask` after a background job finishes now reports the user cancellation instead of telling the model to retry a skipped tool ([#15985](https://github.com/can1357/oh-my-pi/issues/15985))
20
+
5
21
  ## [18.11.1] - 2026-10-15
6
22
 
7
23
  ### Added
@@ -12,8 +12,10 @@ export interface QueuedMessagePreparation {
12
12
  /** Append context after the originals; undefined stops this attempt, retaining originals unless explicitly removed. */
13
13
  commit(): readonly AgentMessage[] | undefined;
14
14
  }
15
+ /** Which agent queue a batch came from: steering amends the running task, follow-ups start the next one. */
16
+ export type QueuedMessageQueue = "steering" | "followUp";
15
17
  /** Prepare an exclusively claimed batch. Undefined delivers unchanged; the signal also aborts when the claim is cancelled. */
16
- export type PrepareQueuedMessages = (messages: readonly AgentMessage[], signal: AbortSignal) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
18
+ export type PrepareQueuedMessages = (messages: readonly AgentMessage[], signal: AbortSignal, queue: QueuedMessageQueue) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
17
19
  /** Called once an aside has been inserted into the agent's live context. */
18
20
  export declare const ASIDE_MESSAGE_COMMIT: unique symbol;
19
21
  /** Symbol-keyed handoff for one finalized, tool-owned stream speculation session. */
@@ -1106,6 +1108,11 @@ export interface AgentContext {
1106
1108
  */
1107
1109
  export type AgentEvent = {
1108
1110
  type: "agent_start";
1111
+ /**
1112
+ * Calls of the unpaired assistant tail a continued run executes before its
1113
+ * first model request. They run without a new assistant `message_end`.
1114
+ */
1115
+ resumedToolCallIds?: string[];
1109
1116
  } | {
1110
1117
  type: "agent_end";
1111
1118
  messages: AgentMessage[];
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.11.1",
4
+ "version": "18.11.3",
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.11.1",
42
- "@oh-my-pi/pi-catalog": "18.11.1",
43
- "@oh-my-pi/pi-natives": "18.11.1",
44
- "@oh-my-pi/pi-utils": "18.11.1",
45
- "@oh-my-pi/pi-wire": "18.11.1",
46
- "@oh-my-pi/snapcompact": "18.11.1",
41
+ "@oh-my-pi/pi-ai": "18.11.3",
42
+ "@oh-my-pi/pi-catalog": "18.11.3",
43
+ "@oh-my-pi/pi-natives": "18.11.3",
44
+ "@oh-my-pi/pi-utils": "18.11.3",
45
+ "@oh-my-pi/pi-wire": "18.11.3",
46
+ "@oh-my-pi/snapcompact": "18.11.3",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.11.1",
50
+ "@oh-my-pi/omptype": "18.11.3",
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
@@ -700,7 +700,17 @@ export function agentLoopContinue(
700
700
  const newMessages: AgentMessage[] = [];
701
701
  const currentContext: AgentContext = { ...context, messages: [...context.messages] };
702
702
 
703
- stream.push({ type: "agent_start" });
703
+ const resumeTail = unpairedToolCallTail(currentContext.messages);
704
+ stream.push(
705
+ resumeTail
706
+ ? {
707
+ type: "agent_start",
708
+ resumedToolCallIds: resumeTail.content.flatMap(block =>
709
+ block.type === "toolCall" ? [block.id] : [],
710
+ ),
711
+ }
712
+ : { type: "agent_start" },
713
+ );
704
714
 
705
715
  await runLoop(currentContext, newMessages, config, signal, stream, streamFn);
706
716
  } catch (err) {
@@ -3642,20 +3652,17 @@ async function executeToolCalls(
3642
3652
  const interrupted = interruptState.triggered;
3643
3653
  const perToolAborted = record.signal.aborted;
3644
3654
  const abortedDuringExecution = perToolAborted && isError && !completedToolExecution;
3645
- if (interrupted && abortedDuringExecution) {
3646
- // This tool's own signal fired AND it failed to produce a result. The
3647
- // execution may already have performed partial work before throwing on
3648
- // abort, so preserve that distinction in the placeholder metadata.
3655
+ if (record.interruptible && interrupted && abortedDuringExecution) {
3656
+ // Queued steering/asides abort only interruptible tools. An external
3657
+ // user abort can also fail a non-interruptible tool (e.g. cancelled
3658
+ // ask); keep its actual error instead of attributing it to the queue.
3649
3659
  record.skipped = true;
3650
3660
  emitToolResult(record, createSkippedToolResult(interruptState.source, executionStarted), true);
3651
3661
  } else {
3652
- // No interrupt on this signal, or the tool finished before the interrupt landed
3653
- // (`completedToolExecution`) — even if the signal aborted around completion. Keep
3654
- // its real result: a completed tool already ran its side effects, so the model must
3655
- // see what actually happened (a genuine non-zero exit / error result) rather than a
3656
- // false "skipped" that discards work the tool performed (#4752). A peer-IRC interrupt
3657
- // on the batch leaves non-interruptible tools' signals untouched — their genuine
3658
- // errors survive here too.
3662
+ // A tool that finished before the interrupt landed (`completedToolExecution`)
3663
+ // keeps its real result even if its signal aborted around completion:
3664
+ // the model must see the work it actually performed (#4752).
3665
+ // Non-interruptible tools also retain errors from external user aborts.
3659
3666
  emitToolResult(record, result, isError);
3660
3667
  }
3661
3668
 
package/src/agent.ts CHANGED
@@ -60,6 +60,7 @@ import type {
60
60
  AgentTurnEndContext,
61
61
  AsideMessage,
62
62
  PrepareQueuedMessages,
63
+ QueuedMessageQueue,
63
64
  SpeculativeToolExecutionConfig,
64
65
  StreamFn,
65
66
  ToolCallContext,
@@ -385,8 +386,6 @@ interface CursorToolResultEntry {
385
386
  additionalContext?: string;
386
387
  }
387
388
 
388
- type QueuedMessageQueue = "steering" | "followUp";
389
-
390
389
  interface LiveSteeredEntry {
391
390
  message: AgentMessage;
392
391
  controller: AbortController | undefined;
@@ -1009,7 +1008,7 @@ export class Agent {
1009
1008
  this.#queuedMessageClaims[queue] = claim;
1010
1009
  const preparationSignal = AbortSignal.any([signal, claim.controller.signal]);
1011
1010
  try {
1012
- const preparation = await prepare(messages, preparationSignal);
1011
+ const preparation = await prepare(messages, preparationSignal, queue);
1013
1012
  signal.throwIfAborted();
1014
1013
  if (preparationSignal.aborted || this.#queuedMessageClaims[queue] !== claim) return [];
1015
1014
  const additional = preparation?.commit();
@@ -42,6 +42,7 @@ import {
42
42
  getOpenAIResponsesHistoryPayload,
43
43
  normalizeResponsesToolCallId,
44
44
  stripOpenAIResponsesOutputOnlyStatusesForReplay,
45
+ truncateResponseItemId,
45
46
  } from "@oh-my-pi/pi-ai/utils";
46
47
  import { captureOpenAIHttpError } from "@oh-my-pi/pi-ai/utils/openai-http";
47
48
  import { isBedrockOpenAIUrl } from "@oh-my-pi/pi-catalog/hosts";
@@ -713,8 +714,8 @@ export function buildOpenAiNativeHistory(
713
714
  let msgId = parsedSignature?.id;
714
715
  if (!msgId) {
715
716
  msgId = `msg_${msgIndex}`;
716
- } else if (msgId.length > 64) {
717
- msgId = `msg_${Bun.hash(msgId).toString(36)}`;
717
+ } else {
718
+ msgId = truncateResponseItemId(msgId, "msg");
718
719
  }
719
720
  input.push({
720
721
  type: "message",
package/src/types.ts CHANGED
@@ -38,10 +38,14 @@ export interface QueuedMessagePreparation {
38
38
  commit(): readonly AgentMessage[] | undefined;
39
39
  }
40
40
 
41
+ /** Which agent queue a batch came from: steering amends the running task, follow-ups start the next one. */
42
+ export type QueuedMessageQueue = "steering" | "followUp";
43
+
41
44
  /** Prepare an exclusively claimed batch. Undefined delivers unchanged; the signal also aborts when the claim is cancelled. */
42
45
  export type PrepareQueuedMessages = (
43
46
  messages: readonly AgentMessage[],
44
47
  signal: AbortSignal,
48
+ queue: QueuedMessageQueue,
45
49
  ) => QueuedMessagePreparation | undefined | Promise<QueuedMessagePreparation | undefined>;
46
50
 
47
51
  /** Called once an aside has been inserted into the agent's live context. */
@@ -1252,7 +1256,14 @@ export interface AgentContext {
1252
1256
  */
1253
1257
  export type AgentEvent =
1254
1258
  // Agent lifecycle
1255
- | { type: "agent_start" }
1259
+ | {
1260
+ type: "agent_start";
1261
+ /**
1262
+ * Calls of the unpaired assistant tail a continued run executes before its
1263
+ * first model request. They run without a new assistant `message_end`.
1264
+ */
1265
+ resumedToolCallIds?: string[];
1266
+ }
1256
1267
  | {
1257
1268
  type: "agent_end";
1258
1269
  messages: AgentMessage[];