@oh-my-pi/pi-agent-core 16.2.1 → 16.2.2

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.2] - 2026-06-27
6
+
7
+ ### Added
8
+
9
+ - Added optional AgentTool.matcherPaths(args) and AgentTool.matcherEntries(args) hooks to allow tools to surface target file paths and isolate file evaluations for path-scoped stream matchers (e.g., when handling multi-file payloads or embedded paths in streamed arguments).
10
+
11
+ ### Removed
12
+
13
+ - Removed support for Pi dialect integration.
14
+
5
15
  ## [16.2.0] - 2026-06-27
6
16
 
7
17
  ### Added
@@ -545,6 +545,28 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
545
545
  * matching.
546
546
  */
547
547
  matcherDigest?: (args: unknown) => string | undefined;
548
+ /**
549
+ * Surface the target file paths a (potentially partial) streamed call would
550
+ * touch, so path-scoped stream matchers (e.g. TTSR `tool:edit(*.ts)` globs)
551
+ * can match without a top-level `path`/`paths` argument. Used for tools whose
552
+ * wire grammar embeds paths inside the streamed payload (hashline section
553
+ * headers, apply_patch envelope markers). Return `undefined` (or an empty
554
+ * array) to fall back to the caller's top-level argument scan.
555
+ */
556
+ matcherPaths?: (args: unknown) => readonly string[] | undefined;
557
+ /**
558
+ * Per-file projection of a (potentially partial) streamed call, pairing each
559
+ * touched file path with the digest of only the lines added to that file.
560
+ * Path-scoped stream matchers (TTSR) evaluate each entry in isolation, so a
561
+ * scoped rule like `tool:edit(*.ts)` never fires on text that actually
562
+ * belongs to a sibling Markdown hunk in a multi-file payload. Takes
563
+ * precedence over {@link matcherDigest} + {@link matcherPaths} when present;
564
+ * returns `undefined` (or empty) to fall back to the combined hooks.
565
+ */
566
+ matcherEntries?: (args: unknown) => readonly {
567
+ path: string;
568
+ digest: string;
569
+ }[] | undefined;
548
570
  /** Capability tier declaration used by approval gates. Omitted means "exec". */
549
571
  approval?: ToolApproval;
550
572
  /** Lines appended after the standard approval prompt header. */
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.2.1",
4
+ "version": "16.2.2",
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.2.1",
39
- "@oh-my-pi/pi-catalog": "16.2.1",
40
- "@oh-my-pi/pi-natives": "16.2.1",
41
- "@oh-my-pi/pi-utils": "16.2.1",
42
- "@oh-my-pi/pi-wire": "16.2.1",
43
- "@oh-my-pi/snapcompact": "16.2.1",
38
+ "@oh-my-pi/pi-ai": "16.2.2",
39
+ "@oh-my-pi/pi-catalog": "16.2.2",
40
+ "@oh-my-pi/pi-natives": "16.2.2",
41
+ "@oh-my-pi/pi-utils": "16.2.2",
42
+ "@oh-my-pi/pi-wire": "16.2.2",
43
+ "@oh-my-pi/snapcompact": "16.2.2",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  renderToolExamples,
26
26
  wrapInbandToolStream,
27
27
  } from "@oh-my-pi/pi-ai/dialect";
28
+ import * as AIError from "@oh-my-pi/pi-ai/error";
28
29
  import {
29
30
  createHarmonyAuditEvent,
30
31
  detectHarmonyLeakInAssistantMessage,
@@ -134,7 +135,6 @@ export function resolveOwnedDialectFromEnv(value: string | undefined): Dialect |
134
135
  case "anthropic":
135
136
  case "deepseek":
136
137
  case "harmony":
137
- case "pi":
138
138
  case "qwen3":
139
139
  case "gemini":
140
140
  case "gemma":
@@ -1557,8 +1557,12 @@ function emitAbortedAssistantMessage(
1557
1557
  requestSignal: AbortSignal | undefined,
1558
1558
  ): AssistantMessage {
1559
1559
  const errorMessage = abortReasonText(requestSignal);
1560
+ const errorId =
1561
+ errorMessage === "Request was aborted"
1562
+ ? AIError.create(AIError.Flag.Abort)
1563
+ : AIError.classify(requestSignal?.reason) || undefined;
1560
1564
  const base: AssistantMessage = partialMessage
1561
- ? { ...partialMessage, stopReason: "aborted", errorMessage }
1565
+ ? { ...partialMessage, stopReason: "aborted", errorMessage, errorId }
1562
1566
  : {
1563
1567
  role: "assistant",
1564
1568
  content: [],
@@ -1575,6 +1579,7 @@ function emitAbortedAssistantMessage(
1575
1579
  },
1576
1580
  stopReason: "aborted",
1577
1581
  errorMessage,
1582
+ errorId,
1578
1583
  timestamp: Date.now(),
1579
1584
  };
1580
1585
  // Only tool calls that reached `toolcall_end` survive abort/error replay. A
@@ -14,12 +14,12 @@ import {
14
14
  type Message,
15
15
  type MessageAttribution,
16
16
  type Model,
17
- ProviderHttpError,
18
17
  type SimpleStreamOptions,
19
18
  type Tool,
20
19
  type Usage,
21
20
  withAuth,
22
21
  } from "@oh-my-pi/pi-ai";
22
+ import { ProviderHttpError } from "@oh-my-pi/pi-ai/error";
23
23
  import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
24
24
  import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
25
25
  import { logger, prompt } from "@oh-my-pi/pi-utils";
@@ -12,7 +12,7 @@
12
12
  * with `{ summary, shortSummary? }`.
13
13
  */
14
14
 
15
- import { ProviderHttpError } from "@oh-my-pi/pi-ai/errors";
15
+ import { ProviderHttpError } from "@oh-my-pi/pi-ai/error";
16
16
  import { parseAzureDeploymentNameMap, parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-shared";
17
17
  import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
18
18
  import type { Api, AssistantMessage, FetchImpl, Message, Model } from "@oh-my-pi/pi-ai/types";
package/src/proxy.ts CHANGED
@@ -13,6 +13,12 @@ import {
13
13
  type StopReason,
14
14
  type ToolCall,
15
15
  } from "@oh-my-pi/pi-ai";
16
+ import {
17
+ clearStreamingPartialJson,
18
+ kStreamingPartialJson,
19
+ type StreamingPartialJsonCarrier,
20
+ setStreamingPartialJson,
21
+ } from "@oh-my-pi/pi-ai/utils/block-symbols";
16
22
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
17
23
  import { parseStreamingJson, readSseJson } from "@oh-my-pi/pi-utils";
18
24
 
@@ -183,8 +189,8 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
183
189
  const errorMessage = error instanceof Error ? error.message : String(error);
184
190
  const reason = options.signal?.aborted ? "aborted" : "error";
185
191
  partial.stopReason = reason;
186
- scrubPartialJson(partial);
187
192
  partial.errorMessage = errorMessage;
193
+ scrubPartialJson(partial);
188
194
  stream.push({
189
195
  type: "error",
190
196
  reason,
@@ -202,14 +208,13 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
202
208
  }
203
209
 
204
210
  /**
205
- * Remove the `partialJson` streaming field from any tool-call content blocks
206
- * that still carry it (e.g. when the stream ended without a `toolcall_end`).
211
+ * Clear the `partialJson` streaming symbol from any tool-call content blocks
212
+ * that still carry it (e.g. when the stream ended without a `toolcall_end`), so
213
+ * the finalized `AssistantMessage` no longer reads as still-streaming.
207
214
  */
208
215
  function scrubPartialJson(partial: AssistantMessage): void {
209
216
  for (const block of partial.content) {
210
- if (block?.type === "toolCall") {
211
- delete (block as ToolCall & { partialJson?: string }).partialJson;
212
- }
217
+ if (block?.type === "toolCall") clearStreamingPartialJson(block);
213
218
  }
214
219
  }
215
220
 
@@ -218,10 +223,10 @@ function scrubPartialJson(partial: AssistantMessage): void {
218
223
  *
219
224
  * Streaming `partialJson` for in-progress tool calls is accumulated in a
220
225
  * side-channel map keyed by `contentIndex` and also written onto the content
221
- * object (as a typed intersection field) so downstream renderers can read it
222
- * during streaming. The field is deleted at `toolcall_end` and scrubbed from
223
- * any remaining blocks at `done`/`error` to guarantee it never leaks into the
224
- * final `AssistantMessage`.
226
+ * object as a symbol-keyed field so downstream renderers can read it
227
+ * during streaming. The field is cleared at `toolcall_end` and scrubbed from any
228
+ * remaining blocks at `done`/`error` so the finalized `AssistantMessage` never
229
+ * reads as still-streaming.
225
230
  */
226
231
  function processProxyEvent(
227
232
  model: Model,
@@ -240,9 +245,10 @@ function processProxyEvent(
240
245
  totalTokens: 0,
241
246
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
242
247
  };
243
- delete (partial as { stopReason?: string }).stopReason;
244
- delete (partial as { errorMessage?: string }).errorMessage;
245
- delete (partial as { duration?: number }).duration;
248
+ partial.errorMessage = undefined;
249
+ partial.errorId = undefined;
250
+ partial.duration = undefined;
251
+ (partial as { stopReason?: string }).stopReason = undefined;
246
252
  return { type: "start", partial };
247
253
 
248
254
  case "text_start":
@@ -315,8 +321,8 @@ function processProxyEvent(
315
321
  id: proxyEvent.id,
316
322
  name: proxyEvent.toolName,
317
323
  arguments: {},
318
- partialJson: "",
319
- } as ToolCall & { partialJson: string };
324
+ [kStreamingPartialJson]: "",
325
+ } as ToolCall & StreamingPartialJsonCarrier;
320
326
  partialJsonByIndex.set(proxyEvent.contentIndex, "");
321
327
  return { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
322
328
  case "toolcall_delta": {
@@ -325,7 +331,7 @@ function processProxyEvent(
325
331
  const acc = (partialJsonByIndex.get(proxyEvent.contentIndex) ?? "") + proxyEvent.delta;
326
332
  partialJsonByIndex.set(proxyEvent.contentIndex, acc);
327
333
  content.arguments = parseStreamingJson(acc) || {};
328
- (content as ToolCall & { partialJson: string }).partialJson = acc;
334
+ setStreamingPartialJson(content, acc);
329
335
  partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity
330
336
  return {
331
337
  type: "toolcall_delta",
@@ -341,7 +347,7 @@ function processProxyEvent(
341
347
  const content = partial.content[proxyEvent.contentIndex];
342
348
  if (content?.type === "toolCall") {
343
349
  partialJsonByIndex.delete(proxyEvent.contentIndex);
344
- delete (content as ToolCall & { partialJson?: string }).partialJson;
350
+ clearStreamingPartialJson(content);
345
351
  return {
346
352
  type: "toolcall_end",
347
353
  contentIndex: proxyEvent.contentIndex,
package/src/types.ts CHANGED
@@ -633,6 +633,27 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
633
633
  */
634
634
  matcherDigest?: (args: unknown) => string | undefined;
635
635
 
636
+ /**
637
+ * Surface the target file paths a (potentially partial) streamed call would
638
+ * touch, so path-scoped stream matchers (e.g. TTSR `tool:edit(*.ts)` globs)
639
+ * can match without a top-level `path`/`paths` argument. Used for tools whose
640
+ * wire grammar embeds paths inside the streamed payload (hashline section
641
+ * headers, apply_patch envelope markers). Return `undefined` (or an empty
642
+ * array) to fall back to the caller's top-level argument scan.
643
+ */
644
+ matcherPaths?: (args: unknown) => readonly string[] | undefined;
645
+
646
+ /**
647
+ * Per-file projection of a (potentially partial) streamed call, pairing each
648
+ * touched file path with the digest of only the lines added to that file.
649
+ * Path-scoped stream matchers (TTSR) evaluate each entry in isolation, so a
650
+ * scoped rule like `tool:edit(*.ts)` never fires on text that actually
651
+ * belongs to a sibling Markdown hunk in a multi-file payload. Takes
652
+ * precedence over {@link matcherDigest} + {@link matcherPaths} when present;
653
+ * returns `undefined` (or empty) to fall back to the combined hooks.
654
+ */
655
+ matcherEntries?: (args: unknown) => readonly { path: string; digest: string }[] | undefined;
656
+
636
657
  /** Capability tier declaration used by approval gates. Omitted means "exec". */
637
658
  approval?: ToolApproval;
638
659