@oh-my-pi/pi-agent-core 16.0.4 → 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/src/agent.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { isPromise } from "node:util/types";
5
5
  import {
6
- type ApiKeyResolveContext,
6
+ type ApiKey,
7
7
  type AssistantMessage,
8
8
  type AssistantMessageEvent,
9
9
  type Context,
@@ -131,6 +131,8 @@ export interface AgentOptions {
131
131
  * Custom stream function (for proxy backends, etc.). Default uses streamSimple.
132
132
  */
133
133
  streamFn?: StreamFn;
134
+ /** Absolute wall-clock deadline in Unix epoch milliseconds. */
135
+ deadline?: number;
134
136
 
135
137
  /**
136
138
  * Optional session identifier forwarded to LLM providers.
@@ -148,10 +150,10 @@ export interface AgentOptions {
148
150
  providerSessionState?: Map<string, ProviderSessionState>;
149
151
 
150
152
  /**
151
- * Resolves an API key dynamically for each LLM call.
152
- * Useful for expiring tokens (e.g., GitHub Copilot OAuth).
153
+ * Resolves an API key or resolver dynamically for each LLM call.
154
+ * Useful for expiring tokens and model-scoped credential routing.
153
155
  */
154
- getApiKey?: (provider: string, ctx?: ApiKeyResolveContext) => Promise<string | undefined> | string | undefined;
156
+ getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
155
157
 
156
158
  /**
157
159
  * Inspect or replace provider payloads before they are sent.
@@ -255,6 +257,13 @@ export interface AgentOptions {
255
257
  */
256
258
  afterToolCall?: AgentLoopConfig["afterToolCall"];
257
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
+
258
267
  /**
259
268
  * Opt-in OpenTelemetry instrumentation. Passing `{}` enables the loop's
260
269
  * GenAI-semantic-convention spans using the global tracer provider. See
@@ -303,6 +312,7 @@ export class Agent {
303
312
  #followUpMode: "all" | "one-at-a-time";
304
313
  #interruptMode: "immediate" | "wait";
305
314
  #sessionId?: string;
315
+ #deadline?: number;
306
316
  #promptCacheKey?: string;
307
317
  #metadata?: Record<string, unknown>;
308
318
  #metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
@@ -344,7 +354,7 @@ export class Agent {
344
354
  #cursorToolResultBuffer: CursorToolResultEntry[] = [];
345
355
 
346
356
  streamFn: StreamFn;
347
- getApiKey?: (provider: string, ctx?: ApiKeyResolveContext) => Promise<string | undefined> | string | undefined;
357
+ getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
348
358
  /**
349
359
  * Hook invoked after tool arguments are validated and before execution.
350
360
  * Reassign at any time to swap the implementation (e.g. on extension reload).
@@ -355,6 +365,11 @@ export class Agent {
355
365
  * message emission. Reassign at any time to swap the implementation.
356
366
  */
357
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"];
358
373
 
359
374
  constructor(opts: AgentOptions = {}) {
360
375
  this.#state = { ...this.#state, ...opts.initialState };
@@ -368,6 +383,7 @@ export class Agent {
368
383
  this.#interruptMode = opts.interruptMode || "immediate";
369
384
  this.streamFn = opts.streamFn || streamSimple;
370
385
  this.#sessionId = opts.sessionId;
386
+ this.#deadline = opts.deadline;
371
387
  this.#promptCacheKey = opts.promptCacheKey;
372
388
  this.#providerSessionState = opts.providerSessionState;
373
389
  this.#thinkingBudgets = opts.thinkingBudgets;
@@ -398,6 +414,7 @@ export class Agent {
398
414
  this.#onHarmonyLeak = opts.onHarmonyLeak;
399
415
  this.beforeToolCall = opts.beforeToolCall;
400
416
  this.afterToolCall = opts.afterToolCall;
417
+ this.transformAssistantMessage = opts.transformAssistantMessage;
401
418
  this.#telemetry = opts.telemetry;
402
419
  this.#appendOnlyContext = opts.appendOnlyContext;
403
420
  this.#transformProviderContext = opts.transformProviderContext;
@@ -1014,6 +1031,7 @@ export class Agent {
1014
1031
  hideThinkingSummary: this.#hideThinkingSummary,
1015
1032
  interruptMode: this.#interruptMode,
1016
1033
  sessionId: this.#sessionId,
1034
+ deadline: this.#deadline,
1017
1035
  promptCacheKey: this.#promptCacheKey,
1018
1036
  metadata: this.#metadataResolver ? undefined : this.#metadata,
1019
1037
  metadataResolver: this.#metadataResolver,
@@ -1046,6 +1064,9 @@ export class Agent {
1046
1064
  appendOnlyContext: this.#appendOnlyContext,
1047
1065
  beforeToolCall: this.beforeToolCall ? (ctx, signal) => this.beforeToolCall?.(ctx, signal) : undefined,
1048
1066
  afterToolCall: this.afterToolCall ? (ctx, signal) => this.afterToolCall?.(ctx, signal) : undefined,
1067
+ transformAssistantMessage: this.transformAssistantMessage
1068
+ ? (message, signal) => this.transformAssistantMessage?.(message, signal)
1069
+ : undefined,
1049
1070
  onAssistantMessageEvent: this.#onAssistantMessageEvent,
1050
1071
  onHarmonyLeak: this.#onHarmonyLeak,
1051
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
@@ -1,5 +1,5 @@
1
1
  import type {
2
- ApiKeyResolveContext,
2
+ ApiKey,
3
3
  AssistantMessage,
4
4
  AssistantMessageEvent,
5
5
  AssistantMessageEventStream,
@@ -55,6 +55,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
55
55
  */
56
56
  sessionId?: string;
57
57
 
58
+ /** Absolute wall-clock deadline in Unix epoch milliseconds. */
59
+ deadline?: number;
60
+
58
61
  /**
59
62
  * Optional resolver called per LLM request to produce request metadata.
60
63
  * When set, the agent loop evaluates it **after** `getApiKey` resolves the
@@ -117,12 +120,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
117
120
  transformProviderContext?: (context: Context, model: Model) => Context;
118
121
 
119
122
  /**
120
- * Resolves an API key dynamically for each LLM call.
123
+ * Resolves the API key or resolver for the current model before each LLM call.
121
124
  *
122
- * Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire
123
- * during long-running tool execution phases.
125
+ * Returning an ApiKeyResolver lets the stream retry policy refresh or rotate
126
+ * the model-scoped credential after auth/usage-limit errors.
124
127
  */
125
- getApiKey?: (provider: string, ctx?: ApiKeyResolveContext) => Promise<string | undefined> | string | undefined;
128
+ getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
126
129
 
127
130
  /**
128
131
  * Returns steering messages to inject into the conversation mid-run.
@@ -290,6 +293,20 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
290
293
  */
291
294
  onTurnEnd?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<void> | void;
292
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
+
293
310
  /**
294
311
  * Called after a tool finishes executing, before `tool_execution_end` and the
295
312
  * tool-result message are emitted.