@oh-my-pi/pi-agent-core 18.2.6 → 18.2.7

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.
@@ -335,43 +335,6 @@ This license allows the work and adaptations of it to be shared and used
335
335
  commercially, as long as it is attributed to Poppy Works. The font is bundled
336
336
  here (crates/pi-natives/src/fonts/Silver.ttf) as a CJK/Unicode bitmap fallback.
337
337
 
338
- -------------------------------------------------------------------------------
339
- packages/utils/src/vendor/mermaid-ascii/NOTICE
340
-
341
- This directory contains an in-house Mermaid-diagram-to-ASCII renderer adapted
342
- from beautiful-mermaid (https://github.com/lukilabs/beautiful-mermaid), used
343
- under the MIT License.
344
-
345
- Copyright (c) 2026 Craft Docs
346
-
347
- Only the ASCII rendering pipeline is ported (flowchart/state, sequence, class,
348
- ER, and xychart diagrams); the SVG renderer and its `elkjs` graph-layout
349
- dependency, the browser entry point, and the SVG theme/style modules were
350
- dropped. Terminal display width is reimplemented on `Bun.stringWidth`, and
351
- inline label formatting (HTML tags, markdown emphasis) is reduced to plain text
352
- for ASCII output. Layout and edge-routing logic is preserved faithfully so
353
- ASCII output matches the upstream package.
354
-
355
- MIT License
356
-
357
- Permission is hereby granted, free of charge, to any person obtaining a copy
358
- of this software and associated documentation files (the "Software"), to deal
359
- in the Software without restriction, including without limitation the rights
360
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
361
- copies of the Software, and to permit persons to whom the Software is
362
- furnished to do so, subject to the following conditions:
363
-
364
- The above copyright notice and this permission notice shall be included in all
365
- copies or substantial portions of the Software.
366
-
367
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
368
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
369
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
370
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
371
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
372
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
373
- SOFTWARE.
374
-
375
338
  -------------------------------------------------------------------------------
376
339
  packages/coding-agent/src/markit/NOTICE
377
340
 
@@ -256,6 +256,11 @@ export declare class Agent {
256
256
  * Hook that peeks whether interrupting IRC asides are queued for the next boundary.
257
257
  */
258
258
  hasIrcInterrupts?: AgentLoopConfig["hasIrcInterrupts"];
259
+ /**
260
+ * Hook that peeks whether background completions (jobs, supervised processes)
261
+ * are queued for the next boundary.
262
+ */
263
+ hasBackgroundCompletions?: AgentLoopConfig["hasBackgroundCompletions"];
259
264
  constructor(opts?: AgentOptions);
260
265
  /**
261
266
  * Get the current session ID used for provider caching.
@@ -236,6 +236,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
236
236
  * The host owns message injection at the next boundary.
237
237
  */
238
238
  hasIrcInterrupts?: () => boolean | Promise<boolean>;
239
+ /**
240
+ * Peeks whether a background completion (finished job, exited supervised
241
+ * process) is queued for aside injection at the next boundary.
242
+ *
243
+ * Same rules as {@link hasIrcInterrupts}: non-consuming, only cuts
244
+ * *interruptible* waits short, ignored when interruptMode is "wait". Without
245
+ * it a completion notice sits behind an hour-long `hub wait` that the agent
246
+ * would have abandoned had it seen the notice.
247
+ */
248
+ hasBackgroundCompletions?: () => boolean | Promise<boolean>;
239
249
  /**
240
250
  * Returns follow-up messages to process after the agent would otherwise stop.
241
251
  *
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.2.6",
4
+ "version": "18.2.7",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,16 +35,16 @@
35
35
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "18.2.6",
39
- "@oh-my-pi/pi-catalog": "18.2.6",
40
- "@oh-my-pi/pi-natives": "18.2.6",
41
- "@oh-my-pi/pi-utils": "18.2.6",
42
- "@oh-my-pi/pi-wire": "18.2.6",
43
- "@oh-my-pi/snapcompact": "18.2.6",
38
+ "@oh-my-pi/pi-ai": "18.2.7",
39
+ "@oh-my-pi/pi-catalog": "18.2.7",
40
+ "@oh-my-pi/pi-natives": "18.2.7",
41
+ "@oh-my-pi/pi-utils": "18.2.7",
42
+ "@oh-my-pi/pi-wire": "18.2.7",
43
+ "@oh-my-pi/snapcompact": "18.2.7",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@oh-my-pi/omptype": "18.2.6",
47
+ "@oh-my-pi/omptype": "18.2.7",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
50
  "@types/bun": "^1.3.14"
package/src/agent-loop.ts CHANGED
@@ -2839,6 +2839,7 @@ async function executeToolCalls(
2839
2839
  const {
2840
2840
  hasSteeringMessages,
2841
2841
  hasIrcInterrupts,
2842
+ hasBackgroundCompletions,
2842
2843
  interruptMode = "immediate",
2843
2844
  getToolContext,
2844
2845
 
@@ -2876,7 +2877,7 @@ async function executeToolCalls(
2876
2877
  const interruptibleSignal: AbortSignal = signal
2877
2878
  ? AbortSignal.any([signal, steeringAbortController.signal, ircAbortController.signal])
2878
2879
  : AbortSignal.any([steeringAbortController.signal, ircAbortController.signal]);
2879
- const interruptState: { triggered: boolean; source?: SteeringInterruptSource | "irc" } = { triggered: false };
2880
+ const interruptState: { triggered: boolean; source?: AsideInterruptSource } = { triggered: false };
2880
2881
 
2881
2882
  // Streamed messages were prepared (validation + `beforeToolCall`) before
2882
2883
  // `message_end`, so hook revisions are already part of the message; anything
@@ -2927,19 +2928,22 @@ async function executeToolCalls(
2927
2928
  };
2928
2929
  });
2929
2930
 
2930
- const checkIrcInterrupts = async (): Promise<void> => {
2931
- // IRC only fires once: a peer interrupt already recorded on interruptState
2931
+ const checkAsideInterrupts = async (): Promise<void> => {
2932
+ // Asides only fire once: an interrupt already recorded on interruptState
2932
2933
  // must not re-abort, and (unlike steering) never re-consumes a queue.
2933
2934
  if (!shouldInterruptImmediately || signal?.aborted || interruptState.triggered) return;
2934
- if (hasIrcInterrupts && (await hasIrcInterrupts())) {
2935
- // Peer IRC hard-aborts interruptible waits only; foreground tools keep
2936
- // running (no partial side effects) but get the cooperative soft
2937
- // signal so backgroundable work can step aside for the peer message.
2938
- interruptState.triggered = true;
2939
- interruptState.source = "irc";
2940
- ircAbortController.abort();
2941
- steeringSoftController.abort();
2942
- }
2935
+ // Peer IRC and background completions (finished jobs, exited supervised
2936
+ // processes) hard-abort interruptible waits only; foreground tools keep
2937
+ // running (no partial side effects) but get the cooperative soft signal
2938
+ // so backgroundable work can step aside for the queued notice.
2939
+ let source: AsideInterruptSource | undefined;
2940
+ if (hasIrcInterrupts && (await hasIrcInterrupts())) source = "irc";
2941
+ else if (hasBackgroundCompletions && (await hasBackgroundCompletions())) source = "background";
2942
+ if (!source || interruptState.triggered) return;
2943
+ interruptState.triggered = true;
2944
+ interruptState.source = source;
2945
+ ircAbortController.abort();
2946
+ steeringSoftController.abort();
2943
2947
  };
2944
2948
 
2945
2949
  const checkSteering = async (): Promise<void> => {
@@ -2980,7 +2984,7 @@ async function executeToolCalls(
2980
2984
  }
2981
2985
  return;
2982
2986
  }
2983
- await checkIrcInterrupts();
2987
+ await checkAsideInterrupts();
2984
2988
  };
2985
2989
 
2986
2990
  const emitToolResult = (record: (typeof records)[number], result: AgentToolResult<any>, isError: boolean): void => {
@@ -3283,8 +3287,8 @@ async function executeToolCalls(
3283
3287
  // and soft-signals cooperative tools (auto-background bash), so the boundary
3284
3288
  // dequeue below injects the message promptly. Gated on immediate-interrupt
3285
3289
  // mode; checkSteering is idempotent (no-op once triggered).
3286
- const watchSteeringWhileRunning =
3287
- shouldInterruptImmediately && (hasSteeringMessages !== undefined || hasIrcInterrupts !== undefined);
3290
+ const hasAsidePeek = hasIrcInterrupts !== undefined || hasBackgroundCompletions !== undefined;
3291
+ const watchSteeringWhileRunning = shouldInterruptImmediately && (hasSteeringMessages !== undefined || hasAsidePeek);
3288
3292
  const eventDrivenSteeringWatch =
3289
3293
  watchSteeringWhileRunning && config.waitForSteeringMessages !== undefined && hasSteeringMessages !== undefined;
3290
3294
  const steeringWatchAbortController = new AbortController();
@@ -3323,13 +3327,13 @@ async function executeToolCalls(
3323
3327
  }
3324
3328
  })()
3325
3329
  : undefined;
3326
- // IRC interrupt records have a separate session-owned queue and no wake
3327
- // callback. Keep its established timer fallback when that queue is present;
3328
- // system steering uses the event-driven path above and does not poll.
3330
+ // IRC interrupt records and background completions live in session-owned
3331
+ // queues with no wake callback. Keep the timer fallback when either peek is
3332
+ // present; system steering uses the event-driven path above and does not poll.
3329
3333
  const steeringWatchTimer =
3330
- watchSteeringWhileRunning && (!eventDrivenSteeringWatch || hasIrcInterrupts !== undefined)
3334
+ watchSteeringWhileRunning && (!eventDrivenSteeringWatch || hasAsidePeek)
3331
3335
  ? setInterval(
3332
- () => void (eventDrivenSteeringWatch ? checkIrcInterrupts() : checkSteering()),
3336
+ () => void (eventDrivenSteeringWatch ? checkAsideInterrupts() : checkSteering()),
3333
3337
  STEERING_INTERRUPT_POLL_MS,
3334
3338
  )
3335
3339
  : undefined;
@@ -3538,8 +3542,11 @@ function createToolSignalAbortedResult(signal: AbortSignal): AgentToolResult<unk
3538
3542
  };
3539
3543
  }
3540
3544
 
3545
+ /** Origin of a mid-batch interrupt: queued steering, a peer IRC, or a background completion notice. */
3546
+ type AsideInterruptSource = SteeringInterruptSource | "irc" | "background";
3547
+
3541
3548
  function createSkippedToolResult(
3542
- source: SteeringInterruptSource | "irc" | undefined,
3549
+ source: AsideInterruptSource | undefined,
3543
3550
  executionStarted: boolean,
3544
3551
  ): AgentToolResult<SyntheticToolResultDetails | InterruptedToolResultDetails> {
3545
3552
  let reason = "pending steering message";
@@ -3556,6 +3563,9 @@ function createSkippedToolResult(
3556
3563
  } else if (source === "irc") {
3557
3564
  reason = "pending peer interrupt";
3558
3565
  blocker = "interrupt";
3566
+ } else if (source === "background") {
3567
+ reason = "a queued background completion (job or supervised process)";
3568
+ blocker = "completion notice";
3559
3569
  }
3560
3570
  return {
3561
3571
  content: [
package/src/agent.ts CHANGED
@@ -486,6 +486,11 @@ export class Agent {
486
486
  * Hook that peeks whether interrupting IRC asides are queued for the next boundary.
487
487
  */
488
488
  hasIrcInterrupts?: AgentLoopConfig["hasIrcInterrupts"];
489
+ /**
490
+ * Hook that peeks whether background completions (jobs, supervised processes)
491
+ * are queued for the next boundary.
492
+ */
493
+ hasBackgroundCompletions?: AgentLoopConfig["hasBackgroundCompletions"];
489
494
 
490
495
  constructor(opts: AgentOptions = {}) {
491
496
  this.#state = { ...this.#state, ...opts.initialState };
@@ -1660,6 +1665,7 @@ export class Agent {
1660
1665
  },
1661
1666
  waitForSteeringMessages: signal => this.#waitForSteeringMessages(signal),
1662
1667
  hasIrcInterrupts: this.hasIrcInterrupts,
1668
+ hasBackgroundCompletions: this.hasBackgroundCompletions,
1663
1669
  getFollowUpMessages: signal => this.#dequeueFollowUpMessagesAfterHooks(signal ?? loopSignal),
1664
1670
  getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
1665
1671
  onBeforeYield: () => this.#onBeforeYield?.(),
@@ -24,7 +24,7 @@ import type {
24
24
  Usage,
25
25
  } from "@oh-my-pi/pi-ai";
26
26
  import * as AIError from "@oh-my-pi/pi-ai/error";
27
- import { supportsAnthropicCompaction } from "@oh-my-pi/pi-ai/providers/anthropic";
27
+ import { supportsAnthropicCompaction } from "@oh-my-pi/pi-ai/providers/anthropic-compaction";
28
28
  import { isRecord, prompt } from "@oh-my-pi/pi-utils";
29
29
  import { type InstrumentedChatSpanOptions, instrumentedCompleteSimple } from "../telemetry";
30
30
  import anthropicCompactionInstructionsPrompt from "./prompts/anthropic-compaction-instructions.md" with { type: "text" };
@@ -9,13 +9,13 @@
9
9
 
10
10
  import type { Api, CodexCompactionContext, FetchImpl, Model, ProviderSessionState } from "@oh-my-pi/pi-ai";
11
11
  import * as AIError from "@oh-my-pi/pi-ai/error";
12
+ import { createOpenAICodexCompactionRequestContext } from "@oh-my-pi/pi-ai/providers/openai-codex-compaction";
12
13
  import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
13
14
  import {
14
- createOpenAICodexCompactionRequestContext,
15
15
  createOpenAICodexCompatibilityMetadata,
16
+ openCodexCompactionEventStream,
16
17
  type OpenAICodexCompactionBody,
17
18
  type OpenAICodexCompatibilityMetadata,
18
- openCodexCompactionEventStream,
19
19
  } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
20
20
  import {
21
21
  getOpenAIPromptCacheKey,
@@ -25,9 +25,9 @@ import {
25
25
  } from "@oh-my-pi/pi-ai";
26
26
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
27
27
  import * as AIError from "@oh-my-pi/pi-ai/error";
28
+ import { createOpenAICodexCompactionRequestContext } from "@oh-my-pi/pi-ai/providers/openai-codex-compaction";
28
29
  import {
29
30
  buildTransformedCodexRequestBody,
30
- createOpenAICodexCompactionRequestContext,
31
31
  type OpenAICodexCompactionBody,
32
32
  } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
33
33
  import type { InputItem as CodexInputItem } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
@@ -16,12 +16,10 @@
16
16
  */
17
17
 
18
18
  import { ProviderHttpError } from "@oh-my-pi/pi-ai/error";
19
+ import { getCodexAttestationHeader } from "@oh-my-pi/pi-ai/providers/openai-codex-attestation";
20
+ import { createOpenAICodexCompactionRequestContext } from "@oh-my-pi/pi-ai/providers/openai-codex-compaction";
19
21
  import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
20
- import {
21
- createOpenAICodexCompactionRequestContext,
22
- createOpenAICodexCompatibilityMetadata,
23
- getCodexAttestationHeader,
24
- } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
22
+ import { createOpenAICodexCompatibilityMetadata } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
25
23
  import {
26
24
  encodeResponsesToolResultOutput,
27
25
  hoistInterleavedResponsesToolBatchMessages,
package/src/types.ts CHANGED
@@ -293,6 +293,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
293
293
  * The host owns message injection at the next boundary.
294
294
  */
295
295
  hasIrcInterrupts?: () => boolean | Promise<boolean>;
296
+ /**
297
+ * Peeks whether a background completion (finished job, exited supervised
298
+ * process) is queued for aside injection at the next boundary.
299
+ *
300
+ * Same rules as {@link hasIrcInterrupts}: non-consuming, only cuts
301
+ * *interruptible* waits short, ignored when interruptMode is "wait". Without
302
+ * it a completion notice sits behind an hour-long `hub wait` that the agent
303
+ * would have abandoned had it seen the notice.
304
+ */
305
+ hasBackgroundCompletions?: () => boolean | Promise<boolean>;
296
306
 
297
307
  /**
298
308
  * Returns follow-up messages to process after the agent would otherwise stop.