@oh-my-pi/pi-agent-core 16.0.5 → 16.0.6

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
+ ## [16.0.6] - 2026-06-18
6
+
7
+ ### Added
8
+
9
+ - Added `transformAssistantMessage` hook to `AgentOptions` and `Agent` to allow mutating the finalized assistant message before UI emission, context appending, or tool dispatch
10
+
5
11
  ## [16.0.5] - 2026-06-17
6
12
 
7
13
  ### Breaking Changes
@@ -158,6 +158,12 @@ export interface AgentOptions {
158
158
  * message are emitted. See {@link AgentLoopConfig.afterToolCall} for full semantics.
159
159
  */
160
160
  afterToolCall?: AgentLoopConfig["afterToolCall"];
161
+ /**
162
+ * Called once an assistant message is finalized, before it reaches the
163
+ * context, the UI, or tool dispatch. May mutate the message in place (text +
164
+ * tool-call arguments). See {@link AgentLoopConfig.transformAssistantMessage}.
165
+ */
166
+ transformAssistantMessage?: AgentLoopConfig["transformAssistantMessage"];
161
167
  /**
162
168
  * Opt-in OpenTelemetry instrumentation. Passing `{}` enables the loop's
163
169
  * GenAI-semantic-convention spans using the global tracer provider. See
@@ -187,6 +193,11 @@ export declare class Agent {
187
193
  * message emission. Reassign at any time to swap the implementation.
188
194
  */
189
195
  afterToolCall?: AgentLoopConfig["afterToolCall"];
196
+ /**
197
+ * Hook invoked once an assistant message is finalized, before context append,
198
+ * UI emission, and tool dispatch. Reassign at any time to swap the implementation.
199
+ */
200
+ transformAssistantMessage?: AgentLoopConfig["transformAssistantMessage"];
190
201
  constructor(opts?: AgentOptions);
191
202
  /**
192
203
  * Get the current session ID used for provider caching.
@@ -6,5 +6,6 @@ export * from "./proxy";
6
6
  export * from "./run-collector";
7
7
  export * from "./telemetry";
8
8
  export * from "./thinking";
9
+ export * from "./tokenizer";
9
10
  export * from "./types";
10
11
  export * from "./utils/yield";
@@ -0,0 +1 @@
1
+ export declare function countTokens(text: string | string[]): number;
@@ -245,6 +245,19 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
245
245
  * backlog so advice produced during the wait is injected as an aside).
246
246
  */
247
247
  onTurnEnd?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void;
248
+ /**
249
+ * Called once an assistant message is finalized from the model stream, before
250
+ * it is appended to the context, emitted as `message_end`, or its tool calls
251
+ * are validated and dispatched. The hook may mutate the message in place —
252
+ * both its text content and its tool-call arguments — and those edits are seen
253
+ * by the transcript, the UI, and tool execution alike (single source of truth).
254
+ *
255
+ * Used for inline macro expansion: rewriting `@[[runtime.name(args)]]` tokens
256
+ * to host-computed values before anything downstream consumes the message.
257
+ * Runs at most once per assistant message; must not throw (a throw would abort
258
+ * the turn).
259
+ */
260
+ transformAssistantMessage?: (message: AssistantMessage, signal?: AbortSignal) => Promise<void> | void;
248
261
  /**
249
262
  * Called after a tool finishes executing, before `tool_execution_end` and the
250
263
  * tool-result message are emitted.
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.0.5",
4
+ "version": "16.0.6",
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,11 +35,11 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "16.0.5",
39
- "@oh-my-pi/pi-catalog": "16.0.5",
40
- "@oh-my-pi/pi-natives": "16.0.5",
41
- "@oh-my-pi/pi-utils": "16.0.5",
42
- "@oh-my-pi/snapcompact": "16.0.5",
38
+ "@oh-my-pi/pi-ai": "16.0.6",
39
+ "@oh-my-pi/pi-catalog": "16.0.6",
40
+ "@oh-my-pi/pi-natives": "16.0.6",
41
+ "@oh-my-pi/pi-utils": "16.0.6",
42
+ "@oh-my-pi/snapcompact": "16.0.6",
43
43
  "@opentelemetry/api": "^1.9.1"
44
44
  },
45
45
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -5,9 +5,11 @@
5
5
  import {
6
6
  type AssistantMessage,
7
7
  type AssistantMessageEvent,
8
+ arkToWireSchema,
8
9
  type Context,
9
10
  EventStream,
10
11
  isApiKeyResolver,
12
+ isArkSchema,
11
13
  isZodSchema,
12
14
  resolveApiKeyOnce,
13
15
  seedApiKeyResolver,
@@ -566,6 +568,9 @@ export function normalizeTools(
566
568
  if (isZodSchema(parameters)) {
567
569
  const wired = zodToWireSchema(parameters);
568
570
  parameters = injectIntentIntoSchema(wired, intentMode) as TSchema;
571
+ } else if (isArkSchema(parameters)) {
572
+ const wired = arkToWireSchema(parameters);
573
+ parameters = injectIntentIntoSchema(wired, intentMode) as TSchema;
569
574
  } else {
570
575
  parameters = injectIntentIntoSchema(parameters, intentMode) as TSchema;
571
576
  }
@@ -1226,6 +1231,12 @@ async function streamAssistantResponse(
1226
1231
  }
1227
1232
  }
1228
1233
  finalMessage = snapshotAssistantMessage(finalMessage);
1234
+ // Expand inline macros (and any other registered rewrite) on the
1235
+ // finalized message before it reaches the context, the UI, or tool
1236
+ // dispatch — so a single mutation is the source of truth for all three.
1237
+ if (config.transformAssistantMessage) {
1238
+ await config.transformAssistantMessage(finalMessage, requestSignal);
1239
+ }
1229
1240
  if (addedPartial) {
1230
1241
  context.messages[context.messages.length - 1] = finalMessage;
1231
1242
  } else {
@@ -1249,9 +1260,19 @@ async function streamAssistantResponse(
1249
1260
  switch (event.type) {
1250
1261
  case "start":
1251
1262
  partialMessage = event.partial;
1252
- context.messages.push(partialMessage);
1253
- addedPartial = true;
1254
- stream.push({ type: "message_start", message: snapshotAssistantMessage(partialMessage) });
1263
+ if (addedPartial) {
1264
+ context.messages[context.messages.length - 1] = partialMessage;
1265
+ completedToolCallIds.clear();
1266
+ stream.push({
1267
+ type: "message_update",
1268
+ assistantMessageEvent: snapshotAssistantMessageEvent(event),
1269
+ message: snapshotAssistantMessage(partialMessage),
1270
+ });
1271
+ } else {
1272
+ context.messages.push(partialMessage);
1273
+ addedPartial = true;
1274
+ stream.push({ type: "message_start", message: snapshotAssistantMessage(partialMessage) });
1275
+ }
1255
1276
  break;
1256
1277
 
1257
1278
  case "text_start":
package/src/agent.ts CHANGED
@@ -257,6 +257,13 @@ export interface AgentOptions {
257
257
  */
258
258
  afterToolCall?: AgentLoopConfig["afterToolCall"];
259
259
 
260
+ /**
261
+ * Called once an assistant message is finalized, before it reaches the
262
+ * context, the UI, or tool dispatch. May mutate the message in place (text +
263
+ * tool-call arguments). See {@link AgentLoopConfig.transformAssistantMessage}.
264
+ */
265
+ transformAssistantMessage?: AgentLoopConfig["transformAssistantMessage"];
266
+
260
267
  /**
261
268
  * Opt-in OpenTelemetry instrumentation. Passing `{}` enables the loop's
262
269
  * GenAI-semantic-convention spans using the global tracer provider. See
@@ -358,6 +365,11 @@ export class Agent {
358
365
  * message emission. Reassign at any time to swap the implementation.
359
366
  */
360
367
  afterToolCall?: AgentLoopConfig["afterToolCall"];
368
+ /**
369
+ * Hook invoked once an assistant message is finalized, before context append,
370
+ * UI emission, and tool dispatch. Reassign at any time to swap the implementation.
371
+ */
372
+ transformAssistantMessage?: AgentLoopConfig["transformAssistantMessage"];
361
373
 
362
374
  constructor(opts: AgentOptions = {}) {
363
375
  this.#state = { ...this.#state, ...opts.initialState };
@@ -402,6 +414,7 @@ export class Agent {
402
414
  this.#onHarmonyLeak = opts.onHarmonyLeak;
403
415
  this.beforeToolCall = opts.beforeToolCall;
404
416
  this.afterToolCall = opts.afterToolCall;
417
+ this.transformAssistantMessage = opts.transformAssistantMessage;
405
418
  this.#telemetry = opts.telemetry;
406
419
  this.#appendOnlyContext = opts.appendOnlyContext;
407
420
  this.#transformProviderContext = opts.transformProviderContext;
@@ -1051,6 +1064,9 @@ export class Agent {
1051
1064
  appendOnlyContext: this.#appendOnlyContext,
1052
1065
  beforeToolCall: this.beforeToolCall ? (ctx, signal) => this.beforeToolCall?.(ctx, signal) : undefined,
1053
1066
  afterToolCall: this.afterToolCall ? (ctx, signal) => this.afterToolCall?.(ctx, signal) : undefined,
1067
+ transformAssistantMessage: this.transformAssistantMessage
1068
+ ? (message, signal) => this.transformAssistantMessage?.(message, signal)
1069
+ : undefined,
1054
1070
  onAssistantMessageEvent: this.#onAssistantMessageEvent,
1055
1071
  onHarmonyLeak: this.#onHarmonyLeak,
1056
1072
  onTurnEnd: (messages, signal) => this.#onTurnEnd?.(messages, signal),
@@ -20,11 +20,11 @@ import {
20
20
  } from "@oh-my-pi/pi-ai";
21
21
  import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
22
22
  import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
23
- import { countTokens } from "@oh-my-pi/pi-natives";
24
23
  import { logger, prompt } from "@oh-my-pi/pi-utils";
25
24
  import * as snapcompact from "@oh-my-pi/snapcompact";
26
25
  import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
27
26
  import { ThinkingLevel } from "../thinking";
27
+ import { countTokens } from "../tokenizer";
28
28
  import type { AgentMessage } from "../types";
29
29
  import type { CompactionEntry, SessionEntry } from "./entries";
30
30
  import { type ConvertToLlm, createBranchSummaryMessage, createCustomMessage, defaultConvertToLlm } from "./messages";
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { ProviderHttpError } from "@oh-my-pi/pi-ai/errors";
16
- import { parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-responses-shared";
16
+ import { 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 { AssistantMessage, FetchImpl, Message, Model } from "@oh-my-pi/pi-ai/types";
19
19
  import {
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
14
- import { countTokens } from "@oh-my-pi/pi-natives";
14
+ import { countTokens } from "../tokenizer";
15
15
  import type { AgentMessage } from "../types";
16
16
  import { estimateTokens } from "./compaction";
17
17
  import type { CustomMessageEntry, SessionEntry, SessionMessageEntry } from "./entries";
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export * from "./run-collector";
14
14
  export * from "./telemetry";
15
15
  // Thinking selectors
16
16
  export * from "./thinking";
17
+ // Tokenizer choice
18
+ export * from "./tokenizer";
17
19
  // Types
18
20
  export * from "./types";
19
21
  // Yield utilities for Bun event-loop busy-wait prevention
package/src/proxy.ts CHANGED
@@ -210,6 +210,18 @@ function processProxyEvent(
210
210
  ): AssistantMessageEvent | undefined {
211
211
  switch (proxyEvent.type) {
212
212
  case "start":
213
+ partial.content.length = 0;
214
+ partial.usage = {
215
+ input: 0,
216
+ output: 0,
217
+ cacheRead: 0,
218
+ cacheWrite: 0,
219
+ totalTokens: 0,
220
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
221
+ };
222
+ delete (partial as { stopReason?: string }).stopReason;
223
+ delete (partial as { errorMessage?: string }).errorMessage;
224
+ delete (partial as { duration?: number }).duration;
213
225
  return { type: "start", partial };
214
226
 
215
227
  case "text_start":
@@ -0,0 +1,17 @@
1
+ import { countTokens as countTokensNat } from "@oh-my-pi/pi-natives";
2
+
3
+ const accurate = process.env.PI_TOKENIZER_ACCURATE === "1" && Bun.env.NODE_ENV !== "test";
4
+
5
+ function estimateTokens(text: string) {
6
+ return (Buffer.byteLength(text, "utf-8") + 3) >> 2;
7
+ }
8
+
9
+ export function countTokens(text: string | string[]): number {
10
+ if (accurate) {
11
+ return countTokensNat(text);
12
+ } else if (Array.isArray(text)) {
13
+ return text.reduce((sum, t) => sum + estimateTokens(t), 0);
14
+ } else {
15
+ return estimateTokens(text);
16
+ }
17
+ }
package/src/types.ts CHANGED
@@ -293,6 +293,20 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
293
293
  */
294
294
  onTurnEnd?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void;
295
295
 
296
+ /**
297
+ * Called once an assistant message is finalized from the model stream, before
298
+ * it is appended to the context, emitted as `message_end`, or its tool calls
299
+ * are validated and dispatched. The hook may mutate the message in place —
300
+ * both its text content and its tool-call arguments — and those edits are seen
301
+ * by the transcript, the UI, and tool execution alike (single source of truth).
302
+ *
303
+ * Used for inline macro expansion: rewriting `@[[runtime.name(args)]]` tokens
304
+ * to host-computed values before anything downstream consumes the message.
305
+ * Runs at most once per assistant message; must not throw (a throw would abort
306
+ * the turn).
307
+ */
308
+ transformAssistantMessage?: (message: AssistantMessage, signal?: AbortSignal) => Promise<void> | void;
309
+
296
310
  /**
297
311
  * Called after a tool finishes executing, before `tool_execution_end` and the
298
312
  * tool-result message are emitted.