@oh-my-pi/pi-agent-core 18.3.0 → 18.3.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,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.3.1] - 2026-09-25
6
+
7
+ ### Added
8
+
9
+ - Added live steering support for Codex WebSocket transports, allowing users to provide input while a response is in progress.
10
+ - Added passive tool-call context support, allowing hooks and tools to supply additional context for subsequent model processing.
11
+ - Improved context-window handling by automatically adjusting output-token limits and supporting models that truncate output at the context-window limit.
12
+
13
+ ### Changed
14
+
15
+ - Improved prompt token counting for requests with anchored prefixes by using provider-reported usage and limiting local estimation to new message content.
16
+
5
17
  ## [18.3.0] - 2026-09-24
6
18
 
7
19
  ### Added
package/README.md CHANGED
@@ -154,8 +154,9 @@ const agent = new Agent({
154
154
  // Dynamic model-scoped API key resolution (for expiring OAuth tokens)
155
155
  getApiKey: async (model) => tokenForModel(model),
156
156
 
157
- // Tool execution context (late-bound UI/session access)
158
- getToolContext: () => ({ /* app-defined */ }),
157
+ // Tool execution context (late-bound UI/session access). Surface the loop's
158
+ // passive-context sink so tools can call ctx.addAdditionalContext(...).
159
+ getToolContext: toolCall => ({ addAdditionalContext: toolCall?.addAdditionalContext /* app-defined */ }),
159
160
  });
160
161
  ```
161
162
 
@@ -78,8 +78,8 @@ below.
78
78
  uutils coreutils (https://github.com/uutils/coreutils), MIT
79
79
  --------------------------------------------------------------------------------
80
80
  Covers: base32, base64, basename, cat, cksum (shared checksum machinery),
81
- b2sum, md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, comm, cut,
82
- date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nproc, paste,
81
+ b2sum, md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, comm, cp,
82
+ cut, date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nproc, paste,
83
83
  printenv, readlink, realpath, rm, seq, sort, stat, tac, tail, tee, touch, tr,
84
84
  truncate, uname, uniq, wc, whoami, yes.
85
85
 
@@ -159,6 +159,11 @@ export interface AgentOptions {
159
159
  pruneToolDescriptions?: boolean;
160
160
  /** Owned tool-calling dialect. Undefined keeps provider-native tool calling. */
161
161
  dialect?: Dialect;
162
+ /**
163
+ * Per-request owned-dialect resolver, consulted with the model being requested.
164
+ * Authoritative when set (like {@link serviceTierResolver}): replaces {@link dialect}.
165
+ */
166
+ dialectResolver?: (model: Model) => Dialect | undefined;
162
167
  /**
163
168
  * When owned tool calling is active and the model fabricates a tool result
164
169
  * mid-turn: `true` (default) aborts the provider request immediately; `false`
@@ -360,6 +365,15 @@ export declare class Agent {
360
365
  set serviceTierResolver(value: ((model: Model) => ServiceTier | undefined) | undefined);
361
366
  get hideThinkingSummary(): boolean | undefined;
362
367
  set hideThinkingSummary(value: boolean | undefined);
368
+ /** Strip tool descriptions from provider-bound specs; read per request. */
369
+ get pruneToolDescriptions(): boolean;
370
+ set pruneToolDescriptions(value: boolean);
371
+ /** Inject/strip the intent field on tool calls; applies from the next prompt run. */
372
+ get intentTracing(): boolean;
373
+ set intentTracing(value: boolean);
374
+ /** Abort the provider request on a fabricated tool result; applies from the next prompt run. */
375
+ get abortOnFabricatedToolResult(): boolean | undefined;
376
+ set abortOnFabricatedToolResult(value: boolean | undefined);
363
377
  /**
364
378
  * Get the current max retry delay in milliseconds.
365
379
  */
@@ -45,6 +45,13 @@ export interface CompactionSummaryMessage {
45
45
  images?: ImageContent[];
46
46
  /** Post-pass dead-end warning attached to this compaction (progress guard). */
47
47
  warning?: string;
48
+ /**
49
+ * Thinking-binding rewrite marker when it must differ from `timestamp`: a
50
+ * natively replayed summary predates it before the retained tail so that
51
+ * tail's bound thinking stays valid. `timestamp` remains the commit time,
52
+ * which is what invalidates the tail's pre-compaction usage reports.
53
+ */
54
+ historyRewriteAt?: number;
48
55
  timestamp: number;
49
56
  }
50
57
  export type CoreCompactionMessage = CustomMessage | HookMessage | BranchSummaryMessage | CompactionSummaryMessage;
@@ -79,6 +86,8 @@ export interface CompactionSummaryMessageOptions {
79
86
  method?: string;
80
87
  /** Estimated context tokens after the rewrite, for display alongside `tokensBefore`. */
81
88
  tokensAfter?: number;
89
+ /** See {@link CompactionSummaryMessage.historyRewriteAt}. */
90
+ historyRewriteAt?: number;
82
91
  }
83
92
  export declare function createCompactionSummaryMessage(summary: string, tokensBefore: number, timestamp: string, options?: CompactionSummaryMessageOptions): CompactionSummaryMessage;
84
93
  export declare function createCustomMessage(customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details: unknown | undefined, timestamp: string, attribution?: MessageAttribution): CustomMessage;
@@ -17,7 +17,7 @@
17
17
  * - Not `aborted` / `error`: those turns report partial or zero usage.
18
18
  * - `hasContextTokenUsage(usage)`: the report must carry usable context numbers.
19
19
  */
20
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
20
+ import type { AssistantMessage, Message } from "@oh-my-pi/pi-ai";
21
21
  import type { Tokenizer } from "../tokenizer.js";
22
22
  import type { AgentMessage } from "../types.js";
23
23
  /** A provider usage report that accounts for a prefix of the transcript. */
@@ -46,6 +46,19 @@ export declare function isTranscriptUsageAnchor(message: AgentMessage): message
46
46
  * summarized away describes a prompt that is no longer sent.
47
47
  */
48
48
  export declare function findTranscriptUsageAnchor(messages: readonly AgentMessage[], fromIndex?: number): TranscriptUsageAnchor | undefined;
49
+ /**
50
+ * Newest assistant turn in a provider request's `messages` whose usage still
51
+ * describes the prefix it sits on, or `undefined` when none does.
52
+ *
53
+ * Request contexts carry no compaction index, so staleness is read from the
54
+ * rewrite markers themselves: a compaction/branch summary or pruned tool result
55
+ * (`prunedAt`) replaced text that every report made at or before the rewrite
56
+ * already counted. A summary's rewrite time is its `timestamp` (commit time);
57
+ * its `historyRewriteAt` may be predated before a natively replayed retained
58
+ * tail so that tail's bound thinking survives, but the tail's usage still
59
+ * counted the summarized prefix.
60
+ */
61
+ export declare function findRequestUsageAnchor(messages: readonly Message[]): TranscriptUsageAnchor | undefined;
49
62
  /** Options for {@link estimateTranscriptTokens}. */
50
63
  export interface TranscriptTokenOptions {
51
64
  /**
@@ -2,6 +2,7 @@ export * from "./agent.js";
2
2
  export * from "./agent-loop.js";
3
3
  export * from "./append-only-context.js";
4
4
  export * from "./compaction.js";
5
+ export * from "./output-budget.js";
5
6
  export * from "./pause.js";
6
7
  export * from "./proxy.js";
7
8
  export * from "./replay-policy.js";
@@ -10,6 +11,7 @@ export * from "./sent-tool-definitions.js";
10
11
  export * from "./speculative-execution.js";
11
12
  export * from "./telemetry.js";
12
13
  export * from "./thinking.js";
14
+ export * from "./tool-context.js";
13
15
  export * from "./tokenizer.js";
14
16
  export * from "./types.js";
15
17
  export * from "./utils/yield.js";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Agent side of provider live steering ({@link LiveSteering}).
3
+ *
4
+ * A provider that can put user input into the response it is streaming (OpenAI
5
+ * Responses `response.steer`) pulls queued steering through a
6
+ * {@link LiveSteeringChannel}. The loop records what the provider accepted right
7
+ * after that response, so the transcript matches what the model saw; anything
8
+ * it declined is injected at the next boundary like ordinary steering.
9
+ */
10
+ import type { LiveSteerClaim, LiveSteering, UserMessage } from "@oh-my-pi/pi-ai";
11
+ import type { AgentMessage } from "./types.js";
12
+ /** Steering-queue access for one provider call, supplied by the agent loop. */
13
+ export interface LiveSteeringQueue {
14
+ /** Resolves once steering is queued or `signal` aborts; never consumes. */
15
+ wait(signal: AbortSignal): Promise<void>;
16
+ /** Dequeues the next steering batch. */
17
+ take(signal: AbortSignal): Promise<AgentMessage[]>;
18
+ /**
19
+ * Provider view of `messages` appended to the in-flight call's context, or
20
+ * `undefined` when that view is not purely user messages.
21
+ */
22
+ toProvider(messages: AgentMessage[], signal: AbortSignal): Promise<UserMessage[] | undefined>;
23
+ }
24
+ /** One provider call's {@link LiveSteering} source. */
25
+ export declare class LiveSteeringChannel implements LiveSteering {
26
+ #private;
27
+ /** Steering the provider delivered into the in-flight response, in queue order. */
28
+ readonly accepted: AgentMessage[];
29
+ /** Steering taken from the queue but not delivered; always queued after {@link accepted}. */
30
+ readonly deferred: AgentMessage[];
31
+ constructor(queue: LiveSteeringQueue);
32
+ wait(signal: AbortSignal): Promise<void>;
33
+ claim(signal: AbortSignal): Promise<LiveSteerClaim | undefined>;
34
+ }
@@ -0,0 +1,43 @@
1
+ import type { Context, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Tokenizer } from "./tokenizer.js";
3
+ /** Smallest output cap {@link fitOutputTokensToContextWindow} will request. */
4
+ export declare const MIN_FITTED_OUTPUT_TOKENS = 1024;
5
+ /**
6
+ * Output cap for a request, so prompt plus output stays inside the model's
7
+ * context window.
8
+ *
9
+ * Chat Completions-style providers (DeepSeek, OpenAI, vLLM, ...) reject a
10
+ * request whose prompt tokens plus `max_tokens` exceed the window. Every
11
+ * request asks for `model.maxTokens` of output by default, so without this a
12
+ * large model output cap (DeepSeek V4: ~384k of a ~1M window) makes every
13
+ * request fail once the prompt passes window minus output cap, long before
14
+ * compaction triggers, and side turns (`/btw`, recaps) have no overflow
15
+ * recovery at all.
16
+ *
17
+ * The prompt size is the provider's own report from the newest trustworthy
18
+ * assistant turn (see {@link findRequestUsageAnchor}) plus a local count of
19
+ * only the messages appended after it; the whole context is counted locally
20
+ * only when no turn can anchor (fresh or freshly rewritten context).
21
+ *
22
+ * Returns `maxTokens` unchanged when the requested cap already fits, the
23
+ * model declares no window, the host ends generation at the window itself
24
+ * instead of rejecting the request (`stops-output-at-context-window`, e.g.
25
+ * Claude 4.5+ on the Claude API), or nothing would be requested (including an
26
+ * OpenRouter-hosted model with no caller cap: the transport omits the catalog
27
+ * default there so each upstream self-caps, and a fitted value would turn into
28
+ * an explicit cap that filters upstreams). Otherwise returns
29
+ * the remaining room (never below {@link MIN_FITTED_OUTPUT_TOKENS}); a
30
+ * prompt that fills the whole window still overflows and is left to the
31
+ * caller's compaction. Near a full window the floor means a turn can stop on
32
+ * `length` instead of failing with a 400.
33
+ *
34
+ * Lives here, not next to the default in pi-ai's `mapOptionsForApi`, because
35
+ * pi-ai has no tokenizer; callers apply it in their `streamFn` (coding-agent
36
+ * does so in its shared settings-aware wrapper).
37
+ *
38
+ * Not fixed: Anthropic budget-thinking transports raise `max_tokens` back to
39
+ * at least the thinking budget plus a fallback buffer downstream
40
+ * (`ensureMaxTokensForThinking`), so a fitted cap below that is overridden
41
+ * and the request can still exceed the window as before.
42
+ */
43
+ export declare function fitOutputTokensToContextWindow(model: Model, context: Context, maxTokens: number | undefined, tokenizer: Tokenizer): number | undefined;
@@ -0,0 +1,31 @@
1
+ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
2
+ import type { AgentMessage } from "./types.js";
3
+ /**
4
+ * Symbol-keyed carrier for passive context reported by a tool executed outside
5
+ * the agent loop (Cursor exec-channel dispatch). The executor attaches the
6
+ * joined context to the {@link ToolResultMessage} it returns; `Agent` reads it
7
+ * when the provider hands the result back and injects it after the buffered
8
+ * results. Symbol keys never serialize, so the context cannot leak into the
9
+ * persisted tool result.
10
+ */
11
+ export declare const TOOL_RESULT_ADDITIONAL_CONTEXT: unique symbol;
12
+ /** A tool result optionally carrying {@link TOOL_RESULT_ADDITIONAL_CONTEXT}. */
13
+ export type ToolResultWithAdditionalContext = ToolResultMessage & {
14
+ [TOOL_RESULT_ADDITIONAL_CONTEXT]?: string;
15
+ };
16
+ /**
17
+ * True for a passive-context value worth delivering: a string with at least
18
+ * one non-whitespace character. Shared by every producer and aggregation site
19
+ * so blank values never produce a developer message.
20
+ */
21
+ export declare function isNonBlankContext(value: unknown): value is string;
22
+ /**
23
+ * Join passive context values in order, dropping blanks. Returns undefined
24
+ * when nothing remains.
25
+ */
26
+ export declare function joinAdditionalContext(values: Iterable<string | undefined>): string | undefined;
27
+ /**
28
+ * Build the developer message that carries passive tool context to the next
29
+ * provider request. Emitted after the tool results it belongs to.
30
+ */
31
+ export declare function createAdditionalContextMessage(text: string): AgentMessage;
@@ -36,6 +36,12 @@ export interface AgentTurnEndContext {
36
36
  message: AgentMessage;
37
37
  /** Tool results produced by this turn, already paired with `message` in the live context. */
38
38
  toolResults: ToolResultMessage[];
39
+ /**
40
+ * Passive model-visible messages appended after the tool results at this
41
+ * boundary. The agent loop always sends an array (possibly empty);
42
+ * absent is equivalent to empty for hosts that construct the context.
43
+ */
44
+ additionalMessages?: AgentMessage[];
39
45
  /** True when the current tool-loop batch is continuing without yielding to post-turn steering. */
40
46
  willContinue: boolean;
41
47
  }
@@ -284,7 +290,11 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
284
290
  onBeforeYield?: () => Promise<void> | void;
285
291
  /**
286
292
  * Provides tool execution context, resolved per tool call.
287
- * Use for late-bound UI or session state access.
293
+ * Use for late-bound UI or session state access. The loop passes the tool
294
+ * call's {@link ToolCallContext}; hosts that support passive tool context
295
+ * surface its `addAdditionalContext` sink as
296
+ * {@link AgentToolContext.addAdditionalContext}. The returned object is
297
+ * handed to the tool as-is.
288
298
  */
289
299
  getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
290
300
  /**
@@ -358,6 +368,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
358
368
  * model's text output back into canonical `toolCall` blocks.
359
369
  */
360
370
  dialect?: Dialect;
371
+ /**
372
+ * Per-call owned-dialect resolver, read once per LLM call with the model
373
+ * being requested. Authoritative when set: its return value (including
374
+ * `undefined` = native tool calling) replaces the static {@link dialect}.
375
+ */
376
+ getDialect?: (model: Model) => Dialect | undefined;
361
377
  /**
362
378
  * When owned (in-band) tool calling is active and the model starts
363
379
  * fabricating a tool result inside its own turn, control how the loop reacts:
@@ -535,6 +551,13 @@ export interface ToolCallContext {
535
551
  * always safe (the message injects at the next batch boundary).
536
552
  */
537
553
  steeringSignal?: AbortSignal;
554
+ /**
555
+ * Loop-owned sink for passive context reported while this call executes.
556
+ * Values join the call's context at the batch boundary and are injected
557
+ * after the batch's tool results, in assistant tool-call order, before the
558
+ * next provider request. Blank values are ignored.
559
+ */
560
+ addAdditionalContext?: (context: string) => void;
538
561
  }
539
562
  /** A single tool-call content block emitted by an assistant message. */
540
563
  export type AgentToolCall = Extract<AssistantMessage["content"][number], {
@@ -723,11 +746,19 @@ export interface SpeculativeToolExecutionConfig {
723
746
  * written back to the tool-call block on the assistant message, and seen by
724
747
  * history, scheduling, execution events, and `tool.execute` alike. It is
725
748
  * ignored when `block` is true.
749
+ *
750
+ * Set `additionalContext` to attach passive model-visible context to this call.
751
+ * Non-empty values from a tool batch are injected in assistant tool-call order
752
+ * after every result settles and before the next provider request. It is
753
+ * dropped when the call is blocked or skipped, or when its final result is an
754
+ * error (including an approval denial raised by the tool's own gate). Within a
755
+ * call it follows any context the tool reported during execution.
726
756
  */
727
757
  export interface BeforeToolCallResult {
728
758
  block?: boolean;
729
759
  reason?: string;
730
760
  args?: Record<string, unknown>;
761
+ additionalContext?: string;
731
762
  }
732
763
  /**
733
764
  * Partial override returned from `afterToolCall`.
@@ -874,6 +905,16 @@ export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprov
874
905
  * Apps can extend via declaration merging.
875
906
  */
876
907
  export interface AgentToolContext {
908
+ /**
909
+ * Attach trusted, agent-authored instructions to the next provider request.
910
+ * The host emits them after tool results with developer/system priority where
911
+ * the selected transport supports it. Do not use this channel for raw tool
912
+ * output, retrieved documents, web content, or other untrusted data; return
913
+ * those through the ordinary tool result instead. Hosts populate it from
914
+ * {@link ToolCallContext.addAdditionalContext} (or their own collector for
915
+ * calls dispatched outside the loop); absent when the host has no sink.
916
+ */
917
+ addAdditionalContext?(context: string): void;
877
918
  /** Present only while the matching outer tool owns its finalized stream session. */
878
919
  [SPECULATIVE_STREAM_SESSION]?: ToolSpeculationStreamSession;
879
920
  }
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": "18.3.0",
4
+ "version": "18.3.1",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -38,16 +38,16 @@
38
38
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
39
39
  },
40
40
  "dependencies": {
41
- "@oh-my-pi/pi-ai": "18.3.0",
42
- "@oh-my-pi/pi-catalog": "18.3.0",
43
- "@oh-my-pi/pi-natives": "18.3.0",
44
- "@oh-my-pi/pi-utils": "18.3.0",
45
- "@oh-my-pi/pi-wire": "18.3.0",
46
- "@oh-my-pi/snapcompact": "18.3.0",
41
+ "@oh-my-pi/pi-ai": "18.3.1",
42
+ "@oh-my-pi/pi-catalog": "18.3.1",
43
+ "@oh-my-pi/pi-natives": "18.3.1",
44
+ "@oh-my-pi/pi-utils": "18.3.1",
45
+ "@oh-my-pi/pi-wire": "18.3.1",
46
+ "@oh-my-pi/snapcompact": "18.3.1",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.3.0",
50
+ "@oh-my-pi/omptype": "18.3.1",
51
51
  "@opentelemetry/context-async-hooks": "^2.9.0",
52
52
  "@opentelemetry/sdk-trace-base": "^2.9.0",
53
53
  "@types/bun": "^1.3.14"