@oh-my-pi/pi-agent-core 16.5.1 → 17.0.0

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,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.0] - 2026-07-15
6
+
7
+ ### Breaking Changes
8
+
9
+ - Replaced the irc, job, and launch tools with a unified hub tool.
10
+ - Removed the tool discovery system (including the search-tool-bm25 tool) and its associated configuration settings (tools.discoveryMode, tools.essentialOverride, mcp.discoveryMode, and mcp.discoveryDefaultServers).
11
+ - Removed the resolve tool; plan approval and preview actions now use writes to the xd://propose virtual device path.
12
+
13
+ ### Added
14
+
15
+ - Introduced the xd:// virtual device protocol for mounting tools as URLs readable/writable via read/write tools, configurable via the new tools.xdev setting (defaults to true).
16
+ - Added the hub tool, consolidating agent peer messaging, background job control, and supervised long-running processes.
17
+ - Added the edit.enforceSeenLines configuration setting (defaults to false) to optionally reject edits on lines that have not been fully displayed.
18
+ - Added the ToolLoadMode type and an optional satisfies predicate to SoftToolRequirement to support compliance checks against specific invocation shapes (such as writing to a virtual device path).
19
+
20
+ ## [16.5.2] - 2026-07-14
21
+
22
+ ### Fixed
23
+
24
+ - Improved session deadline abort signals to carry structured cancellation reasons, enabling timeout-aware tools to correctly classify deadline cancellations.
25
+ - Fixed an issue where completed tool executions were incorrectly marked as skipped (clobbering their actual results) if a user message was queued while the tool was in flight.
26
+
5
27
  ## [16.5.1] - 2026-07-14
6
28
 
7
29
  ### Fixed
@@ -84,11 +84,6 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
84
84
  /** Names of rules that were injected */
85
85
  injectedRules: string[];
86
86
  }
87
- export interface MCPToolSelectionEntry extends SessionEntryBase {
88
- type: "mcp_tool_selection";
89
- /** MCP tool names selected for visibility in discovery mode. */
90
- selectedToolNames: string[];
91
- }
92
87
  export interface SessionInitEntry extends SessionEntryBase {
93
88
  type: "session_init";
94
89
  /** Full system prompt sent to the model */
@@ -109,7 +104,7 @@ export interface ModeChangeEntry extends SessionEntryBase {
109
104
  }
110
105
  export interface CustomCompactionSessionEntries {
111
106
  }
112
- export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | ServiceTierChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | TitleChangeEntry | TtsrInjectionEntry | MCPToolSelectionEntry | SessionInitEntry | ModeChangeEntry | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
107
+ export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | ServiceTierChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | TitleChangeEntry | TtsrInjectionEntry | SessionInitEntry | ModeChangeEntry | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
113
108
  export interface ReadonlySessionManager {
114
109
  getBranch(leafId?: string | null): SessionEntry[];
115
110
  getEntry(id: string): SessionEntry | undefined;
@@ -43,6 +43,16 @@ export interface SoftToolRequirement {
43
43
  id: string;
44
44
  /** Tool that must be called before the loop runs other tools or yields. */
45
45
  toolName: string;
46
+ /**
47
+ * Per-call compliance check: a turn satisfies the requirement only when every
48
+ * tool call passes. Defaults to `name === toolName`. Lets a host demand a
49
+ * specific invocation shape (e.g. `write` targeting a virtual device path)
50
+ * instead of any call to `toolName`. Escalation still forces `toolName`.
51
+ */
52
+ satisfies?(toolCall: {
53
+ name: string;
54
+ arguments?: Record<string, unknown>;
55
+ }): boolean;
46
56
  /** Host-owned reminder messages, injected once per `id` activation. */
47
57
  reminder: AgentMessage[];
48
58
  }
@@ -508,6 +518,16 @@ export interface RenderResultOptions {
508
518
  }
509
519
  /** Capability tier a tool exercises. Determines which approval modes auto-approve it. */
510
520
  export type ToolTier = "read" | "write" | "exec";
521
+ /**
522
+ * How an enabled tool is presented to the model. `"essential"` tools are exposed
523
+ * as normal top-level tools. `"discoverable"` tools are removed from the top-level
524
+ * schema and either mounted under `xd://` device URLs (when that transport is
525
+ * active) or surfaced through BM25 tool search — keeping their schemas off every
526
+ * request. Selection (settings, `hidden`, `defaultInactive`, explicit `--tools`,
527
+ * provider availability) decides whether a tool is enabled; `loadMode` only
528
+ * decides how an enabled tool is presented.
529
+ */
530
+ export type ToolLoadMode = "essential" | "discoverable";
511
531
  /**
512
532
  * Per-tool approval declaration.
513
533
  * - bare tier ("read" / "write" / "exec") — static classification.
@@ -536,8 +556,8 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
536
556
  hidden?: boolean;
537
557
  /** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
538
558
  deferrable?: boolean;
539
- /** Built-in tool loading behavior. "essential" loads initially; "discoverable" can be activated by tool search. */
540
- loadMode?: "essential" | "discoverable";
559
+ /** How an enabled tool is presented. See {@link ToolLoadMode}. Omitted is treated as `"essential"` for built-ins; custom-tool adapters normalize omission to `"discoverable"`. */
560
+ loadMode?: ToolLoadMode;
541
561
  /** Short one-line summary used for tool discovery indexes. */
542
562
  summary?: string;
543
563
  /**
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.5.1",
4
+ "version": "17.0.0",
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.5.1",
39
- "@oh-my-pi/pi-catalog": "16.5.1",
40
- "@oh-my-pi/pi-natives": "16.5.1",
41
- "@oh-my-pi/pi-utils": "16.5.1",
42
- "@oh-my-pi/pi-wire": "16.5.1",
43
- "@oh-my-pi/snapcompact": "16.5.1",
38
+ "@oh-my-pi/pi-ai": "17.0.0",
39
+ "@oh-my-pi/pi-catalog": "17.0.0",
40
+ "@oh-my-pi/pi-natives": "17.0.0",
41
+ "@oh-my-pi/pi-utils": "17.0.0",
42
+ "@oh-my-pi/pi-wire": "17.0.0",
43
+ "@oh-my-pi/snapcompact": "17.0.0",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -67,6 +67,7 @@ import type {
67
67
  AgentToolResult,
68
68
  AgentTurnEndContext,
69
69
  AsideMessage,
70
+ SoftToolRequirement,
70
71
  SteeringInterruptSource,
71
72
  SteeringQueueState,
72
73
  StreamFn,
@@ -766,12 +767,13 @@ async function runLoopBody(
766
767
  let deadlineTimer: Timer | undefined;
767
768
  if (config.deadline !== undefined) {
768
769
  const deadlineAbortController = new AbortController();
770
+ const deadlineReason = new DOMException("Deadline exceeded", "TimeoutError");
769
771
  const delay = config.deadline - Date.now();
770
772
  if (delay <= 0) {
771
- deadlineAbortController.abort("Deadline exceeded");
773
+ deadlineAbortController.abort(deadlineReason);
772
774
  } else {
773
775
  deadlineTimer = setTimeout(() => {
774
- deadlineAbortController.abort("Deadline exceeded");
776
+ deadlineAbortController.abort(deadlineReason);
775
777
  }, delay);
776
778
  }
777
779
  signal = signal ? AbortSignal.any([signal, deadlineAbortController.signal]) : deadlineAbortController.signal;
@@ -802,6 +804,7 @@ async function runLoopBody(
802
804
  // getToolChoice is never advanced twice; the flag resets at the message boundary.
803
805
  let hostToolChoice: ToolChoice | undefined;
804
806
  let softRequiredTool: string | undefined;
807
+ let softSatisfies: SoftToolRequirement["satisfies"];
805
808
  let directiveResolvedForTurn = false;
806
809
 
807
810
  // Outer loop: continues when queued follow-up messages arrive after agent would stop
@@ -860,6 +863,7 @@ async function runLoopBody(
860
863
  const softReq = isSoftToolRequirement(directive) ? directive : undefined;
861
864
  hostToolChoice = directive === undefined || isSoftToolRequirement(directive) ? undefined : directive;
862
865
  softRequiredTool = softReq?.toolName;
866
+ softSatisfies = softReq?.satisfies;
863
867
  if (softReq !== undefined) {
864
868
  if (softReq.id !== softRequirementId) {
865
869
  softRequirementId = softReq.id;
@@ -1023,7 +1027,7 @@ async function runLoopBody(
1023
1027
  const calledOnlyRequiredTool =
1024
1028
  softRequiredTool !== undefined &&
1025
1029
  toolCalls.length > 0 &&
1026
- toolCalls.every(toolCall => toolCall.name === softRequiredTool);
1030
+ toolCalls.every(toolCall => softSatisfies?.(toolCall) ?? toolCall.name === softRequiredTool);
1027
1031
  const softGateActive =
1028
1032
  softRequiredTool !== undefined && !hardToolChoiceBlocks(config.toolChoice, softRequiredTool);
1029
1033
  const softNonCompliant = softGateActive && !calledOnlyRequiredTool;
@@ -2132,19 +2136,21 @@ async function executeToolCalls(
2132
2136
 
2133
2137
  const interrupted = interruptState.triggered;
2134
2138
  const perToolAborted = record.signal.aborted;
2135
- const abortedDuringExecution = perToolAborted && isError;
2136
- if (interrupted && perToolAborted && isError) {
2137
- // This tool's own signal fired AND it failed it was cut off before producing
2138
- // a usable result, so report it as skipped.
2139
+ const abortedDuringExecution = perToolAborted && isError && !completedToolExecution;
2140
+ if (interrupted && perToolAborted && isError && !completedToolExecution) {
2141
+ // This tool's own signal fired AND it failed to produce a result: `tool.execute()`
2142
+ // never returned (it threw on the abort), so it was genuinely cut off before
2143
+ // producing usable output. Report it as skipped.
2139
2144
  record.skipped = true;
2140
2145
  emitToolResult(record, createSkippedToolResult(interruptState.source), true);
2141
2146
  } else {
2142
- // No interrupt on this signal, or the tool finished (successfully or with a
2143
- // genuine error) before the interrupt landed. Keep its real result: a completed
2144
- // tool already ran its side effects, so the model must see what actually
2145
- // happened rather than a false "skipped". A peer-IRC interrupt on the batch
2146
- // leaves non-interruptible tools' signals untouched their genuine errors
2147
- // survive here instead of being clobbered into "skipped".
2147
+ // No interrupt on this signal, or the tool finished before the interrupt landed
2148
+ // (`completedToolExecution`) even if the signal aborted around completion. Keep
2149
+ // its real result: a completed tool already ran its side effects, so the model must
2150
+ // see what actually happened (a genuine non-zero exit / error result) rather than a
2151
+ // false "skipped" that discards work the tool performed (#4752). A peer-IRC interrupt
2152
+ // on the batch leaves non-interruptible tools' signals untouched — their genuine
2153
+ // errors survive here too.
2148
2154
  emitToolResult(record, result, isError);
2149
2155
  }
2150
2156
 
@@ -200,7 +200,6 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
200
200
  case "label":
201
201
  case "service_tier_change":
202
202
  case "ttsr_injection":
203
- case "mcp_tool_selection":
204
203
  case "session_init":
205
204
  case "mode_change":
206
205
  return undefined;
@@ -97,12 +97,6 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
97
97
  injectedRules: string[];
98
98
  }
99
99
 
100
- export interface MCPToolSelectionEntry extends SessionEntryBase {
101
- type: "mcp_tool_selection";
102
- /** MCP tool names selected for visibility in discovery mode. */
103
- selectedToolNames: string[];
104
- }
105
-
106
100
  export interface SessionInitEntry extends SessionEntryBase {
107
101
  type: "session_init";
108
102
  /** Full system prompt sent to the model */
@@ -137,7 +131,6 @@ export type SessionEntry =
137
131
  | LabelEntry
138
132
  | TitleChangeEntry
139
133
  | TtsrInjectionEntry
140
- | MCPToolSelectionEntry
141
134
  | SessionInitEntry
142
135
  | ModeChangeEntry
143
136
  | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
package/src/types.ts CHANGED
@@ -68,6 +68,13 @@ export interface SoftToolRequirement {
68
68
  id: string;
69
69
  /** Tool that must be called before the loop runs other tools or yields. */
70
70
  toolName: string;
71
+ /**
72
+ * Per-call compliance check: a turn satisfies the requirement only when every
73
+ * tool call passes. Defaults to `name === toolName`. Lets a host demand a
74
+ * specific invocation shape (e.g. `write` targeting a virtual device path)
75
+ * instead of any call to `toolName`. Escalation still forces `toolName`.
76
+ */
77
+ satisfies?(toolCall: { name: string; arguments?: Record<string, unknown> }): boolean;
71
78
  /** Host-owned reminder messages, injected once per `id` activation. */
72
79
  reminder: AgentMessage[];
73
80
  }
@@ -587,6 +594,17 @@ export interface RenderResultOptions {
587
594
  /** Capability tier a tool exercises. Determines which approval modes auto-approve it. */
588
595
  export type ToolTier = "read" | "write" | "exec";
589
596
 
597
+ /**
598
+ * How an enabled tool is presented to the model. `"essential"` tools are exposed
599
+ * as normal top-level tools. `"discoverable"` tools are removed from the top-level
600
+ * schema and either mounted under `xd://` device URLs (when that transport is
601
+ * active) or surfaced through BM25 tool search — keeping their schemas off every
602
+ * request. Selection (settings, `hidden`, `defaultInactive`, explicit `--tools`,
603
+ * provider availability) decides whether a tool is enabled; `loadMode` only
604
+ * decides how an enabled tool is presented.
605
+ */
606
+ export type ToolLoadMode = "essential" | "discoverable";
607
+
590
608
  /**
591
609
  * Per-tool approval declaration.
592
610
  * - bare tier ("read" / "write" / "exec") — static classification.
@@ -625,8 +643,8 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
625
643
  hidden?: boolean;
626
644
  /** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
627
645
  deferrable?: boolean;
628
- /** Built-in tool loading behavior. "essential" loads initially; "discoverable" can be activated by tool search. */
629
- loadMode?: "essential" | "discoverable";
646
+ /** How an enabled tool is presented. See {@link ToolLoadMode}. Omitted is treated as `"essential"` for built-ins; custom-tool adapters normalize omission to `"discoverable"`. */
647
+ loadMode?: ToolLoadMode;
630
648
  /** Short one-line summary used for tool discovery indexes. */
631
649
  summary?: string;
632
650
  /**