@oh-my-pi/pi-agent-core 16.5.2 → 17.0.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,21 @@
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
+
5
20
  ## [16.5.2] - 2026-07-14
6
21
 
7
22
  ### 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;
@@ -2,7 +2,7 @@
2
2
  * Proxy stream function for apps that route LLM calls through a server.
3
3
  * The server manages auth and proxies requests to LLM providers.
4
4
  */
5
- import { type AssistantMessage, type AssistantMessageEvent, type Context, EventStream, type FetchImpl, type Model, type SimpleStreamOptions, type StopReason } from "@oh-my-pi/pi-ai";
5
+ import { type AssistantMessage, type AssistantMessageEvent, type Context, EventStream, type FetchImpl, type ImageContent, type Model, type SimpleStreamOptions, type StopReason } from "@oh-my-pi/pi-ai";
6
6
  export declare class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
7
7
  constructor();
8
8
  }
@@ -33,6 +33,10 @@ export type ProxyAssistantMessageEvent = {
33
33
  type: "thinking_end";
34
34
  contentIndex: number;
35
35
  contentSignature?: string;
36
+ } | {
37
+ type: "image_end";
38
+ contentIndex: number;
39
+ content: ImageContent;
36
40
  } | {
37
41
  type: "toolcall_start";
38
42
  contentIndex: number;
@@ -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.2",
4
+ "version": "17.0.1",
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.2",
39
- "@oh-my-pi/pi-catalog": "16.5.2",
40
- "@oh-my-pi/pi-natives": "16.5.2",
41
- "@oh-my-pi/pi-utils": "16.5.2",
42
- "@oh-my-pi/pi-wire": "16.5.2",
43
- "@oh-my-pi/snapcompact": "16.5.2",
38
+ "@oh-my-pi/pi-ai": "17.0.1",
39
+ "@oh-my-pi/pi-catalog": "17.0.1",
40
+ "@oh-my-pi/pi-natives": "17.0.1",
41
+ "@oh-my-pi/pi-utils": "17.0.1",
42
+ "@oh-my-pi/pi-wire": "17.0.1",
43
+ "@oh-my-pi/snapcompact": "17.0.1",
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,
@@ -182,6 +183,7 @@ type AssistantToolCallBlock = Extract<AssistantContentBlock, { type: "toolCall"
182
183
  function snapshotAssistantContentBlock(block: AssistantContentBlock): AssistantContentBlock {
183
184
  switch (block.type) {
184
185
  case "text":
186
+ case "image":
185
187
  return { ...block };
186
188
  case "thinking":
187
189
  return { ...block };
@@ -223,6 +225,7 @@ function snapshotAssistantMessageEvent(
223
225
  case "text_start":
224
226
  case "text_delta":
225
227
  case "text_end":
228
+ case "image_end":
226
229
  case "thinking_start":
227
230
  case "thinking_delta":
228
231
  case "thinking_end":
@@ -803,6 +806,7 @@ async function runLoopBody(
803
806
  // getToolChoice is never advanced twice; the flag resets at the message boundary.
804
807
  let hostToolChoice: ToolChoice | undefined;
805
808
  let softRequiredTool: string | undefined;
809
+ let softSatisfies: SoftToolRequirement["satisfies"];
806
810
  let directiveResolvedForTurn = false;
807
811
 
808
812
  // Outer loop: continues when queued follow-up messages arrive after agent would stop
@@ -861,6 +865,7 @@ async function runLoopBody(
861
865
  const softReq = isSoftToolRequirement(directive) ? directive : undefined;
862
866
  hostToolChoice = directive === undefined || isSoftToolRequirement(directive) ? undefined : directive;
863
867
  softRequiredTool = softReq?.toolName;
868
+ softSatisfies = softReq?.satisfies;
864
869
  if (softReq !== undefined) {
865
870
  if (softReq.id !== softRequirementId) {
866
871
  softRequirementId = softReq.id;
@@ -1024,7 +1029,7 @@ async function runLoopBody(
1024
1029
  const calledOnlyRequiredTool =
1025
1030
  softRequiredTool !== undefined &&
1026
1031
  toolCalls.length > 0 &&
1027
- toolCalls.every(toolCall => toolCall.name === softRequiredTool);
1032
+ toolCalls.every(toolCall => softSatisfies?.(toolCall) ?? toolCall.name === softRequiredTool);
1028
1033
  const softGateActive =
1029
1034
  softRequiredTool !== undefined && !hardToolChoiceBlocks(config.toolChoice, softRequiredTool);
1030
1035
  const softNonCompliant = softGateActive && !calledOnlyRequiredTool;
@@ -1522,6 +1527,7 @@ async function streamAssistantResponse(
1522
1527
  case "text_start":
1523
1528
  case "text_delta":
1524
1529
  case "text_end":
1530
+ case "image_end":
1525
1531
  case "thinking_start":
1526
1532
  case "thinking_delta":
1527
1533
  case "thinking_end":
@@ -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/proxy.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  type Context,
9
9
  EventStream,
10
10
  type FetchImpl,
11
+ type ImageContent,
11
12
  type Model,
12
13
  type SimpleStreamOptions,
13
14
  type StopReason,
@@ -47,6 +48,7 @@ export type ProxyAssistantMessageEvent =
47
48
  | { type: "thinking_start"; contentIndex: number }
48
49
  | { type: "thinking_delta"; contentIndex: number; delta: string }
49
50
  | { type: "thinking_end"; contentIndex: number; contentSignature?: string }
51
+ | { type: "image_end"; contentIndex: number; content: ImageContent }
50
52
  | { type: "toolcall_start"; contentIndex: number; id: string; toolName: string }
51
53
  | { type: "toolcall_delta"; contentIndex: number; delta: string }
52
54
  | { type: "toolcall_end"; contentIndex: number }
@@ -315,6 +317,15 @@ function processProxyEvent(
315
317
  throw new Error("Received thinking_end for non-thinking content");
316
318
  }
317
319
 
320
+ case "image_end":
321
+ partial.content[proxyEvent.contentIndex] = proxyEvent.content;
322
+ return {
323
+ type: "image_end",
324
+ contentIndex: proxyEvent.contentIndex,
325
+ content: proxyEvent.content,
326
+ partial,
327
+ };
328
+
318
329
  case "toolcall_start":
319
330
  partial.content[proxyEvent.contentIndex] = {
320
331
  type: "toolCall",
package/src/telemetry.ts CHANGED
@@ -957,6 +957,9 @@ function assistantContentToOtelParts(content: AssistantMessage["content"]): Otel
957
957
  case "text":
958
958
  parts.push({ type: "text", content: part.text });
959
959
  break;
960
+ case "image":
961
+ parts.push({ type: "blob", modality: "image", mime_type: part.mimeType, content: part.data });
962
+ break;
960
963
  case "thinking":
961
964
  parts.push({ type: "reasoning", content: part.thinking });
962
965
  break;
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
  /**