@oh-my-pi/pi-agent-core 17.1.5 → 17.1.7

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,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.7] - 2026-07-27
6
+
7
+ ### Changed
8
+
9
+ - `beforeToolCall` now runs during arg-prep in a pre-dispatch prepare phase — on the streamed path before the assistant message's `message_start`/`message_end` are emitted, and always ahead of concurrency resolution, `tool_execution_start`, telemetry span start, and `tool.execute` — instead of inside the already-scheduled execution slot. It receives the resolved `tool` in its context and may return `args` to replace the call's arguments; a replacement is revalidated against the tool schema, written back to the assistant message's tool-call block, and re-resolves argument-dependent interruptibility, making it the single source of truth for history, persistence, provider replay, scheduling, execution events, and `tool.execute`. Argument validation moved into the same prepare phase, so functional `concurrency` resolvers now see validated (and possibly revised) arguments rather than raw pre-validation ones. The hook now receives the run's request abort signal rather than the per-tool signal.
10
+
11
+ ## [17.1.6] - 2026-07-27
12
+
13
+ ### Added
14
+
15
+ - Added a pre-model-call gate: `AgentLoopConfig.beforeModelCall` receives the finalized provider context and run abort signal, and may return `{ stop: true, reason? }` to end the run before the provider is called, so a host can refuse a request it has decided not to pay for (prompt no longer fits, budget boundary crossed, session should hand off). `Agent.setBeforeModelCall` installs the host callback; `Agent.addBeforeModelCall` registers an additional one without displacing it and returns a disposer. A gate-stopped run retains pending soft tool reminders/escalations and an unserved hard tool choice for the next admitted request; deferred choices are revalidated against active tools and cleared with queued session state ([#6543](https://github.com/can1357/oh-my-pi/pull/6543) by [@paralin](https://github.com/paralin)).
16
+
17
+ ### Changed
18
+
19
+ - Input message events (prompt, steering, soft reminders) are now emitted once provider-context preparation succeeds, so a pre-model gate can veto the request before any turn opens; gate-stopped and failed runs still commit their accepted inputs ([#6543](https://github.com/can1357/oh-my-pi/pull/6543) by [@paralin](https://github.com/paralin)).
20
+
5
21
  ## [17.1.5] - 2026-07-27
6
22
 
7
23
  ### Fixed
@@ -2,7 +2,7 @@ import { type ApiKey, type AssistantMessage, type AssistantMessageEvent, type Co
2
2
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
3
3
  import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
4
4
  import type { AppendOnlyContextManager } from "./append-only-context.js";
5
- import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideMessage, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
5
+ import type { AgentBeforeModelCall, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideMessage, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
6
6
  export declare class AgentBusyError extends Error {
7
7
  constructor(message?: string);
8
8
  }
@@ -159,6 +159,8 @@ export interface AgentOptions {
159
159
  abortOnFabricatedToolResult?: boolean;
160
160
  /** Dynamic tool-choice directive (hard {@link ToolChoice} or {@link SoftToolRequirement}), resolved once per turn. */
161
161
  getToolChoice?: () => ToolChoiceDirective | undefined;
162
+ /** Reject a deferred hard choice when its named tool is no longer active. */
163
+ onToolChoiceUnavailable?: () => void;
162
164
  /**
163
165
  * Cursor exec handlers for local tool execution.
164
166
  */
@@ -374,6 +376,19 @@ export declare class Agent {
374
376
  setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
375
377
  setOnBeforeYield(fn: (() => Promise<void> | void) | undefined): void;
376
378
  setOnTurnEnd(fn: ((messages: AgentMessage[], signal?: AbortSignal, context?: AgentTurnEndContext) => Promise<void> | void) | undefined): void;
379
+ /**
380
+ * Install or replace the host pre-model-call gate; pass `undefined` to
381
+ * remove it. Gates are sampled when a run starts: installing the first
382
+ * gate while a run is in flight takes effect on the next run.
383
+ */
384
+ setBeforeModelCall(fn: AgentBeforeModelCall | undefined): void;
385
+ /**
386
+ * Add a pre-model callback without replacing callbacks owned by the host.
387
+ * Returns a disposer that removes only this callback. Like
388
+ * {@link setBeforeModelCall}, the first gate installed while a run is in
389
+ * flight takes effect on the next run.
390
+ */
391
+ addBeforeModelCall(fn: AgentBeforeModelCall): () => void;
377
392
  /**
378
393
  * Provide a source of non-interrupting "aside" messages (e.g. background-job
379
394
  * completions, late LSP diagnostics) drained at each step boundary. Never
@@ -408,6 +423,11 @@ export declare class Agent {
408
423
  followUp(m: AgentMessage): void;
409
424
  clearSteeringQueue(): void;
410
425
  clearFollowUpQueue(): void;
426
+ /**
427
+ * Drop tool-directive state retained across a gate-stopped run: the
428
+ * deferred hard choice and the soft-requirement lifecycle.
429
+ */
430
+ clearDeferredToolDirectives(): void;
411
431
  clearAllQueues(): void;
412
432
  hasQueuedMessages(): boolean;
413
433
  /** Non-consuming view of the pending steering queue (insertion order, newest
@@ -21,6 +21,18 @@ export interface AgentTurnEndContext {
21
21
  /** True when the current tool-loop batch is continuing without yielding to post-turn steering. */
22
22
  willContinue: boolean;
23
23
  }
24
+ export interface AgentPreModelCallStop {
25
+ /** Stop the agent loop before sending the next provider request. */
26
+ stop: true;
27
+ /** Optional owner-facing reason, logged by the loop when it stops. */
28
+ reason?: string;
29
+ }
30
+ export type AgentPreModelCallResult = AgentPreModelCallStop | undefined;
31
+ /**
32
+ * A pre-model-call gate. Return {@link AgentPreModelCallStop} to refuse the
33
+ * request, or nothing to proceed; the signal aborts with the run.
34
+ */
35
+ export type AgentBeforeModelCall = (context: Context, signal?: AbortSignal) => AgentPreModelCallResult | void | Promise<AgentPreModelCallResult | void>;
24
36
  /**
25
37
  * A soft tool requirement: the host wants `toolName` called before the loop
26
38
  * runs other tools or yields, but WITHOUT paying the forced-`toolChoice` cost
@@ -61,6 +73,12 @@ export interface SoftToolRequirement {
61
73
  * (applied verbatim) or a {@link SoftToolRequirement} (remind-then-escalate).
62
74
  */
63
75
  export type ToolChoiceDirective = ToolChoice | SoftToolRequirement;
76
+ /** Mutable soft-requirement lifecycle retained across stopped agent runs. */
77
+ export interface SoftToolRequirementState {
78
+ id?: string;
79
+ forcedToolChoice?: ToolChoice;
80
+ escalations: number;
81
+ }
64
82
  /** True when a {@link ToolChoiceDirective} is a soft requirement, not a hard choice. */
65
83
  export declare function isSoftToolRequirement(directive: ToolChoiceDirective | undefined): directive is SoftToolRequirement;
66
84
  /** Source category for a queued steering interrupt observed without consuming the queue. */
@@ -230,8 +248,21 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
230
248
  /**
231
249
  * Refreshes prompt/tool context from live session state before each model call.
232
250
  * Use this when tool availability or the system prompt can change mid-turn.
251
+ *
252
+ * Runs after pending messages are folded in and before provider conversion.
253
+ * Mutate the agent context here; use `beforeModelCall` to inspect the
254
+ * provider-bound context.
233
255
  */
234
256
  syncContextBeforeModelCall?: (context: AgentContext) => void | Promise<void>;
257
+ /**
258
+ * Asked after the complete provider context has been built, including
259
+ * message conversion, provider transforms, normalized tools, and owned
260
+ * dialect prompt injection. Returning {@link AgentPreModelCallStop} ends
261
+ * the stream without emitting `turn_start`, so no turn is left open and no
262
+ * request is billed. Return nothing to proceed. The signal aborts when
263
+ * the run is canceled or its deadline expires.
264
+ */
265
+ beforeModelCall?: AgentBeforeModelCall;
235
266
  /**
236
267
  * Optional transform applied to tool call arguments before execution.
237
268
  * Use for deobfuscating secrets or rewriting arguments.
@@ -304,6 +335,17 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
304
335
  * to the static `toolChoice`.
305
336
  */
306
337
  getToolChoice?: () => ToolChoiceDirective | undefined;
338
+ /**
339
+ * Soft-requirement lifecycle retained by the host when a pre-model-call
340
+ * gate stops a run before its pending reminder or escalation is served.
341
+ */
342
+ softToolRequirementState?: SoftToolRequirementState;
343
+ /**
344
+ * Notifies the host that the pre-model-call gate stopped the run after a
345
+ * hard tool choice was obtained from {@link getToolChoice} but before it
346
+ * was served, so the host can retain it for the next admitted request.
347
+ */
348
+ onToolChoiceRejected?: () => void;
307
349
  /**
308
350
  * Dynamic reasoning effort override, resolved per LLM call.
309
351
  * When set and returns a value, overrides the static `reasoning` captured
@@ -348,15 +390,22 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
348
390
  */
349
391
  getCwd?: () => string | undefined;
350
392
  /**
351
- * Called after a tool call has been validated and is about to execute.
393
+ * Called once per tool call after argument validation, in call order, before
394
+ * the call is scheduled — ahead of concurrency resolution,
395
+ * `tool_execution_start`, and `tool.execute`. On the streamed path it runs
396
+ * before the assistant message's `message_start`/`message_end` are emitted.
352
397
  *
353
398
  * Return `{ block: true }` to prevent execution. The loop emits an error tool
354
399
  * result instead (using `reason` as the error text, or a default if omitted).
355
400
  *
356
- * Mutating `context.args` in place changes the arguments passed to `tool.execute`
357
- * the loop does **not** re-validate after this hook runs.
401
+ * Return `{ args }` to replace the arguments the call runs with. The
402
+ * replacement is revalidated against the tool schema and written back to the
403
+ * tool-call block, making it the single source of truth: history, execution
404
+ * events, persistence, provider replay, concurrency scheduling, and
405
+ * `tool.execute` all see the revised arguments. Mutating `context.args` in
406
+ * place also survives into execution, but a returned `args` object wins.
358
407
  *
359
- * The hook receives the tool abort signal (`signal`) and is responsible for
408
+ * The hook receives the run's request abort signal and is responsible for
360
409
  * honoring it. Throwing surfaces as a tool-error result and does not abort the
361
410
  * rest of the batch.
362
411
  */
@@ -435,12 +484,16 @@ export type AgentToolCall = Extract<AssistantMessage["content"][number], {
435
484
  * Set `block: true` to prevent the tool from executing. The loop emits an error tool
436
485
  * result instead, using `reason` as the error text (or a default if omitted).
437
486
  *
438
- * Mutating the `args` reference passed in `BeforeToolCallContext` is supported and
439
- * survives into execution the loop does **not** re-validate after this hook runs.
487
+ * Set `args` to replace the tool-call arguments. The replacement is revalidated
488
+ * against the tool schema (a failure surfaces as a validation-error tool result),
489
+ * written back to the tool-call block on the assistant message, and seen by
490
+ * history, scheduling, execution events, and `tool.execute` alike. It is
491
+ * ignored when `block` is true.
440
492
  */
441
493
  export interface BeforeToolCallResult {
442
494
  block?: boolean;
443
495
  reason?: string;
496
+ args?: Record<string, unknown>;
444
497
  }
445
498
  /**
446
499
  * Partial override returned from `afterToolCall`.
@@ -466,9 +519,12 @@ export interface BeforeToolCallContext {
466
519
  assistantMessage: AssistantMessage;
467
520
  /** The raw tool call block from `assistantMessage.content`. */
468
521
  toolCall: AgentToolCall;
522
+ /** The resolved tool the call dispatches to. */
523
+ tool: AgentTool<any>;
469
524
  /**
470
525
  * Validated tool arguments. The same reference is forwarded to `tool.execute`
471
- * (after any `transformToolCallArguments` pass), so in-place mutations stick.
526
+ * (after any `transformToolCallArguments` pass), so in-place mutations stick;
527
+ * a returned `BeforeToolCallResult.args` replaces them entirely.
472
528
  */
473
529
  args: Record<string, unknown>;
474
530
  /** Current agent context at the time the tool call is prepared. */
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": "17.1.5",
4
+ "version": "17.1.7",
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": "17.1.5",
39
- "@oh-my-pi/pi-catalog": "17.1.5",
40
- "@oh-my-pi/pi-natives": "17.1.5",
41
- "@oh-my-pi/pi-utils": "17.1.5",
42
- "@oh-my-pi/pi-wire": "17.1.5",
43
- "@oh-my-pi/snapcompact": "17.1.5",
38
+ "@oh-my-pi/pi-ai": "17.1.7",
39
+ "@oh-my-pi/pi-catalog": "17.1.7",
40
+ "@oh-my-pi/pi-natives": "17.1.7",
41
+ "@oh-my-pi/pi-utils": "17.1.7",
42
+ "@oh-my-pi/pi-wire": "17.1.7",
43
+ "@oh-my-pi/snapcompact": "17.1.7",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {