@oh-my-pi/pi-agent-core 18.13.0 → 18.13.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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.13.1] - 2026-10-27
6
+
7
+ ### Added
8
+
9
+ - Hosts can distinguish safely paused agents from turns still draining with `Agent.isPaused` ([#17073](https://github.com/can1357/oh-my-pi/pull/17073) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy)).
10
+
5
11
  ## [18.13.0] - 2026-10-26
6
12
 
7
13
  ### Added
@@ -240,6 +240,8 @@ export interface AgentPromptOptions {
240
240
  }
241
241
  export declare class Agent {
242
242
  #private;
243
+ /** True while this unfinished turn is safely parked by the process pause gate. */
244
+ get isPaused(): boolean;
243
245
  streamFn: StreamFn;
244
246
  getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
245
247
  /** Prepare actual queue deliveries after dequeue gates; commit runs only while ownership remains valid. */
@@ -23,6 +23,9 @@ export declare class SpeculativeOperationCoordinator {
23
23
  #private;
24
24
  readonly config: SpeculativeToolExecutionConfig;
25
25
  readonly environment?: CoordinatorEnvironment | undefined;
26
+ /** Observer of existing speculative execution draining; no separate work ledger. */
27
+ onIdle?: () => void;
28
+ get isRunning(): boolean;
26
29
  constructor(config: SpeculativeToolExecutionConfig, environment?: CoordinatorEnvironment | undefined);
27
30
  get maxInFlight(): number;
28
31
  get size(): number;
@@ -126,6 +126,8 @@ export interface SteeringQueueState {
126
126
  */
127
127
  export interface AgentLoopConfig extends SimpleStreamOptions {
128
128
  model: Model;
129
+ /** Reports whether this loop has reached a safe process-pause boundary. */
130
+ onPauseChange?: (paused: boolean) => void;
129
131
  /**
130
132
  * When to interrupt tool execution for steering messages.
131
133
  * - "immediate" = cut interruptible waits short and raise the cooperative
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.0",
4
+ "version": "18.13.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.0",
42
- "@oh-my-pi/pi-catalog": "18.13.0",
43
- "@oh-my-pi/pi-natives": "18.13.0",
44
- "@oh-my-pi/pi-utils": "18.13.0",
45
- "@oh-my-pi/pi-wire": "18.13.0",
46
- "@oh-my-pi/snapcompact": "18.13.0",
41
+ "@oh-my-pi/pi-ai": "18.13.1",
42
+ "@oh-my-pi/pi-catalog": "18.13.1",
43
+ "@oh-my-pi/pi-natives": "18.13.1",
44
+ "@oh-my-pi/pi-utils": "18.13.1",
45
+ "@oh-my-pi/pi-wire": "18.13.1",
46
+ "@oh-my-pi/snapcompact": "18.13.1",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.13.0",
50
+ "@oh-my-pi/omptype": "18.13.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"
package/src/agent-loop.ts CHANGED
@@ -110,6 +110,17 @@ import {
110
110
  SPECULATIVE_STREAM_SESSION,
111
111
  } from "./types";
112
112
  import { yieldIfDue } from "./utils/yield";
113
+
114
+ async function waitForProcessPause(config: AgentLoopConfig, signal?: AbortSignal): Promise<void> {
115
+ try {
116
+ while (agentPauseGate.paused && !signal?.aborted) {
117
+ config.onPauseChange?.(true);
118
+ await agentPauseGate.waitUntilResumed(signal);
119
+ }
120
+ } finally {
121
+ config.onPauseChange?.(false);
122
+ }
123
+ }
113
124
  /** Stop-details marker for a provider error after assistant content/tool args already streamed. */
114
125
  export const STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL = "stream_interrupted_after_content";
115
126
 
@@ -1331,7 +1342,7 @@ async function runLoopBody(
1331
1342
  // Park at the turn boundary while the process-wide pause gate is
1332
1343
  // engaged (host /pause). An external abort releases the park so a
1333
1344
  // cancelled run still unwinds while everything else stays frozen.
1334
- if (agentPauseGate.paused) await agentPauseGate.waitUntilResumed(signal);
1345
+ if (agentPauseGate.paused) await waitForProcessPause(config, signal);
1335
1346
 
1336
1347
  // Build the provider-bound context before opening the turn. Queue
1337
1348
  // messages are added now but their events remain deferred until
@@ -1443,6 +1454,7 @@ async function runLoopBody(
1443
1454
  let recovered: HarmonyRecoveredToolCall | undefined;
1444
1455
  let message: AssistantMessage;
1445
1456
  try {
1457
+ if (agentPauseGate.paused) await waitForProcessPause(config, signal);
1446
1458
  message = await streamAssistantResponse(
1447
1459
  currentContext,
1448
1460
  config,
@@ -3367,6 +3379,18 @@ async function executeToolCalls(
3367
3379
  };
3368
3380
  });
3369
3381
 
3382
+ // Derive batch readiness from existing dispatch records, not extra participants.
3383
+ const refreshPauseState = (): void => {
3384
+ config.onPauseChange?.(
3385
+ agentPauseGate.paused &&
3386
+ !signal?.aborted &&
3387
+ !speculationCoordinator?.isRunning &&
3388
+ records.some(record => !record.started && !record.skipped && !record.toolResultMessage) &&
3389
+ !records.some(record => record.started && !record.toolResultMessage),
3390
+ );
3391
+ };
3392
+ if (speculationCoordinator) speculationCoordinator.onIdle = refreshPauseState;
3393
+
3370
3394
  const checkAsideInterrupts = async (): Promise<void> => {
3371
3395
  // Asides only fire once: an interrupt already recorded on interruptState
3372
3396
  // must not re-abort, and (unlike steering) never re-consumes a queue.
@@ -3517,7 +3541,11 @@ async function executeToolCalls(
3517
3541
  // Park before starting this tool while the process-wide pause gate is
3518
3542
  // engaged. Tools already executing are unaffected (pausing never aborts);
3519
3543
  // a batch interrupted mid-pause unwinds via the signal checks below.
3520
- if (agentPauseGate.paused) await agentPauseGate.waitUntilResumed(record.signal);
3544
+ while (agentPauseGate.paused && !record.signal.aborted) {
3545
+ refreshPauseState();
3546
+ await agentPauseGate.waitUntilResumed(record.signal);
3547
+ }
3548
+ config.onPauseChange?.(false);
3521
3549
 
3522
3550
  const { toolCall, tool } = record;
3523
3551
  // Validation (and the beforeToolCall hook) ran in the prepare phase; a
@@ -3851,7 +3879,13 @@ async function executeToolCalls(
3851
3879
  concurrency = concurrencyMode ?? "shared";
3852
3880
  }
3853
3881
  const start = concurrency === "exclusive" ? Promise.all([lastExclusive, ...sharedTasks]) : lastExclusive;
3854
- const task = start.then(() => runTool(record, index));
3882
+ const task = start.then(async () => {
3883
+ try {
3884
+ await runTool(record, index);
3885
+ } finally {
3886
+ refreshPauseState();
3887
+ }
3888
+ });
3855
3889
  tasks.push(task);
3856
3890
  if (concurrency === "exclusive") {
3857
3891
  lastExclusive = task;
@@ -3863,6 +3897,8 @@ async function executeToolCalls(
3863
3897
  try {
3864
3898
  await Promise.allSettled(tasks);
3865
3899
  } finally {
3900
+ if (speculationCoordinator) speculationCoordinator.onIdle = undefined;
3901
+ config.onPauseChange?.(false);
3866
3902
  steeringWatchAbortController.abort();
3867
3903
  await steeringWatchPromise?.catch(() => undefined);
3868
3904
  clearInterval(steeringWatchTimer);
package/src/agent.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  unpairedToolCallTail,
40
40
  } from "./agent-loop";
41
41
  import type { AppendOnlyContextManager } from "./append-only-context";
42
+ import { agentPauseGate } from "./pause";
42
43
  import { isProviderRefusalMessage } from "./replay-policy";
43
44
  import { SentToolDefinitions } from "./sent-tool-definitions";
44
45
  import { Tokenizer } from "./tokenizer";
@@ -474,6 +475,12 @@ export class Agent {
474
475
  #cwdResolver?: () => string | undefined;
475
476
 
476
477
  #runningPrompt?: Promise<void>;
478
+ #isPaused = false;
479
+
480
+ /** True while this unfinished turn is safely parked by the process pause gate. */
481
+ get isPaused(): boolean {
482
+ return this.#isPaused && agentPauseGate.paused && !this.#abortController?.signal.aborted;
483
+ }
477
484
  #resolveRunningPrompt?: () => void;
478
485
  #kimiApiFormat?: "openai" | "anthropic";
479
486
  #preferWebsockets?: boolean;
@@ -1783,6 +1790,7 @@ export class Agent {
1783
1790
  this.#state.isStreaming = true;
1784
1791
  this.#state.streamMessage = null;
1785
1792
  this.#state.error = undefined;
1793
+ this.#isPaused = false;
1786
1794
 
1787
1795
  // Clear Cursor tool result buffer at start of each run
1788
1796
  this.#cursorToolResultBuffer = [];
@@ -1955,6 +1963,9 @@ export class Agent {
1955
1963
  onAssistantMessageEvent: this.#onAssistantMessageEvent,
1956
1964
  onHarmonyLeak: this.#onHarmonyLeak,
1957
1965
  onTurnEnd: (messages, signal, context) => this.#onTurnEnd?.(messages, signal, context),
1966
+ onPauseChange: paused => {
1967
+ this.#isPaused = paused;
1968
+ },
1958
1969
  getToolChoice,
1959
1970
  softToolRequirementState: this.#softToolRequirementState,
1960
1971
  onToolChoiceRejected: () => {
@@ -2205,6 +2216,7 @@ export class Agent {
2205
2216
  this.#emit({ type: "agent_end", messages: [errorMsg] });
2206
2217
  }
2207
2218
  } finally {
2219
+ this.#isPaused = false;
2208
2220
  this.#restoreUndeliveredQueuedMessages(loopAbortController);
2209
2221
  resolveRun?.();
2210
2222
  if (this.#abortController === loopAbortController) {
@@ -239,6 +239,12 @@ export class SpeculativeOperationCoordinator {
239
239
  #closed = false;
240
240
  #admissionsFinalized = false;
241
241
  #running = 0;
242
+ /** Observer of existing speculative execution draining; no separate work ledger. */
243
+ onIdle?: () => void;
244
+
245
+ get isRunning(): boolean {
246
+ return this.#running > 0;
247
+ }
242
248
 
243
249
  constructor(
244
250
  readonly config: SpeculativeToolExecutionConfig,
@@ -955,6 +961,7 @@ export class SpeculativeOperationCoordinator {
955
961
  } finally {
956
962
  this.#running--;
957
963
  this.#drain();
964
+ if (this.#running === 0) this.onIdle?.();
958
965
  }
959
966
  })();
960
967
  }
package/src/types.ts CHANGED
@@ -175,6 +175,9 @@ export interface SteeringQueueState {
175
175
  export interface AgentLoopConfig extends SimpleStreamOptions {
176
176
  model: Model;
177
177
 
178
+ /** Reports whether this loop has reached a safe process-pause boundary. */
179
+ onPauseChange?: (paused: boolean) => void;
180
+
178
181
  /**
179
182
  * When to interrupt tool execution for steering messages.
180
183
  * - "immediate" = cut interruptible waits short and raise the cooperative