@oh-my-pi/pi-agent-core 18.2.11 → 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,30 @@
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
+
17
+ ## [18.3.0] - 2026-09-24
18
+
19
+ ### Added
20
+
21
+ - Added support for documenting agent tools on demand through the new `AgentTool.docTopics` method.
22
+ - Added `TOOL_INTERRUPT_ABORT_REASON` so interruptible tools can distinguish queued steering, peer messages, or background completions from a full run abort.
23
+
24
+ ### Changed
25
+
26
+ - Improved interrupt handling so tools respect wait mode and can be interrupted when appropriate.
27
+ - Updated Anthropic compaction compatibility with signature verification.
28
+
5
29
  ## [18.2.11] - 2026-09-23
6
30
 
7
31
  ### Fixed
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
 
@@ -293,7 +293,7 @@ MIT License
293
293
  Copyright (c) 2026 Sander Land
294
294
  (measured tokenizer vocabulary data, reconstruction model, and reference
295
295
  implementation: https://github.com/sanderland/ctok)
296
- Copyright (c) 2026 Can Bölük and the Oh My Pi contributors
296
+ Copyright (c) 2026 Can Bölük and the omp contributors
297
297
  Copyright (c) 2026 Stencil Labs, Inc.
298
298
  (Rust implementation and the compact binary vocabulary encoding in
299
299
  crates/pi-natives/src/utok/claude)
@@ -32,6 +32,12 @@ export declare function createToolScopedAbortReason(message: string, toolCallMes
32
32
  * boundary; this reason stops after persisting the completed tool batch.
33
33
  */
34
34
  export declare const TERMINAL_TOOL_RESULT_ABORT_REASON: unique symbol;
35
+ /**
36
+ * Abort reason carried by an interruptible tool's signal when queued steering,
37
+ * a peer IRC, or a background completion cut it short. Lets a wait tell the
38
+ * designed wake path apart from an external/user abort of the run.
39
+ */
40
+ export declare const TOOL_INTERRUPT_ABORT_REASON: unique symbol;
35
41
  export declare function resolveOwnedDialectFromEnv(value: string | undefined): Dialect | undefined;
36
42
  /**
37
43
  * Start an agent loop with a new prompt message.
@@ -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
  */
@@ -1,33 +1,21 @@
1
1
  /**
2
- * Anthropic server-side compaction (`compact-2026-01-12` beta).
2
+ * Anthropic on-demand compaction (`compact-2026-09-04` beta).
3
3
  *
4
- * The compaction request is the live turn's own request shape — same system
5
- * prompt, tools, and message history — plus the `compact_20260112` edit with
6
- * `pause_after_compaction`. The API summarizes the prompt from the already
7
- * cached prefix and stops; the summary arrives as a `compaction` block that
8
- * the provider surfaces as an `anthropicCompaction` payload. The summary is
9
- * plain text, so it doubles as the compaction entry's readable summary for
10
- * every other provider, while the Anthropic provider replays it as a native
11
- * block (the API drops everything that precedes it). The retained tail after
12
- * the cut point is replayed from session entries exactly like a local summary.
4
+ * The request sends only the prefix to summarize, with the live conversation's
5
+ * system prompt, tools and thinking settings. The returned signed block
6
+ * replaces that prefix; the retained tail is replayed from session entries.
13
7
  */
14
8
  import type { AnthropicCompactionPayload, ApiKey, Effort, Message, Model, SimpleStreamOptions, Tool, Usage } from "@oh-my-pi/pi-ai";
15
9
  import { type InstrumentedChatSpanOptions } from "../telemetry.js";
10
+ import type { AgentMessage } from "../types.js";
16
11
  export declare const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
17
- /** The API rejects a `compact_20260112` trigger below this many input tokens. */
18
- export declare const ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS = 50000;
19
- /**
20
- * Smallest context the native lane accepts. The trigger sits at the API
21
- * floor, so a prompt that lands below it is answered instead of compacted;
22
- * the margin over the floor absorbs the difference between the last reported
23
- * context size and the compaction request's own input.
24
- */
25
- export declare const ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS = 55000;
26
12
  /** Summary persisted under {@link ANTHROPIC_COMPACTION_PRESERVE_KEY}. */
27
13
  export interface AnthropicCompactionPreserveData {
28
14
  provider: string;
29
15
  content: string;
30
- /** Opaque provider state the API attached to the block; replayed verbatim. */
16
+ /** Signature attached to an on-demand block; replayed verbatim. */
17
+ signature?: string;
18
+ /** Legacy threshold block state; replay-only. */
31
19
  encryptedContent?: string;
32
20
  /** Harness file metadata (`<files>` section) replayed after the native block. */
33
21
  filesText?: string;
@@ -51,33 +39,17 @@ export declare function withAnthropicCompactionPreserveData(preserveData: Record
51
39
  /** Replay payload for a compaction summary the active model produced natively. */
52
40
  export declare function getAnthropicCompactionPayload(preserveData: Record<string, unknown> | undefined): AnthropicCompactionPayload | undefined;
53
41
  /**
54
- * The retained tail as the model will see it, for the summarization
55
- * instructions: how many of the conversation's final wire messages stay in
56
- * context verbatim, and the role of the first. The compaction request carries
57
- * the whole conversation so the prompt cache the live turn wrote is read, but
58
- * the summary must cover only the history before that tail — the local
59
- * summarizer never sees the tail, and the rebuilt context replays it after the
60
- * summary. Counting mirrors the provider's message conversion (consecutive
61
- * tool results collapse into one user message; developer messages are user
62
- * messages). Structured for the prompt template, which renders the
63
- * singular/plural wording; the description quotes no content: quoting the
64
- * tail would hand the summarizer the very facts it must leave to the tail.
42
+ * Move the existing keep-tail boundary forward until the summary/tail boundary
43
+ * alternates wire roles and no tool call is separated from its result. If no
44
+ * boundary is safe, the request summarizes the entire snapshot (empty tail).
65
45
  */
66
- export interface RetainedTailScope {
67
- count: number;
68
- role: "assistant" | "user";
69
- }
70
- export declare function describeRetainedTail(messages: readonly Message[]): RetainedTailScope | undefined;
71
- /**
72
- * Summarization prompt sent as the edit's `instructions`, which replace the
73
- * API default entirely. The template lays out the retained-tail boundary
74
- * first, so the summary covers only the history the rebuilt context drops,
75
- * then the caller's extra context, the same structure prompt as the local
76
- * summarizer, the caller's focus, and the tool-abstention clause the API
77
- * recommends when tools are defined (a summarization pass that calls a tool
78
- * yields no summary).
79
- */
80
- export declare function buildAnthropicCompactionInstructions(basePrompt: string, customInstructions: string | undefined, extraContext: string | undefined, retainedTail: RetainedTailScope | undefined): string;
46
+ export declare function findAnthropicCompactionCut(messages: readonly (AgentMessage | {
47
+ role: "system";
48
+ content: string;
49
+ timestamp: number;
50
+ })[], initialCut: number): number;
51
+ /** Instructions replace the API default; the request contains only summarized messages. */
52
+ export declare function buildAnthropicCompactionInstructions(basePrompt: string, customInstructions: string | undefined, extraContext: string | undefined): string;
81
53
  export interface AnthropicNativeCompactionRequest {
82
54
  systemPrompt: string[];
83
55
  messages: Message[];
@@ -88,21 +60,19 @@ export interface AnthropicNativeCompactionRequest {
88
60
  }
89
61
  export interface AnthropicNativeCompactionResponse {
90
62
  content: string;
91
- encryptedContent?: string;
63
+ signature: string;
92
64
  usage: Usage;
93
65
  model: string;
94
66
  }
95
67
  export interface AnthropicNativeCompactionOptions extends Pick<SimpleStreamOptions, "initiatorOverride" | "metadata" | "fetch" | "sessionId" | "promptCacheKey" | "providerSessionState" | "maxInFlightRequests">, Pick<InstrumentedChatSpanOptions, "completeImpl" | "telemetry" | "retry"> {
96
68
  }
97
69
  /**
98
- * Run one compaction request and return the summary the API wrote, with the
99
- * opaque `encrypted_content` the API attached for the replay. `completeSimple`
70
+ * Run one compaction request and return the summary and signature the API wrote. `completeSimple`
100
71
  * resolves terminal failures as messages, so their classification is restored
101
72
  * here: an aborted response is an `AbortError` (a cancellation, never a native
102
73
  * failure) and an error response keeps its HTTP status, so auth and timeout
103
74
  * handling downstream classify it the same way as the OpenAI lanes. A response
104
- * without a summary is a native failure — the API answers the prompt instead
105
- * when its input never reached the trigger, and returns an empty block when
106
- * the model called a tool during summarization.
75
+ * without a summary is a native failure, including tool use, refusals and
76
+ * output limits; the configured method order can then choose a fallback.
107
77
  */
108
78
  export declare function requestAnthropicNativeCompaction(model: Model<"anthropic-messages">, apiKey: ApiKey, request: AnthropicNativeCompactionRequest, signal: AbortSignal | undefined, options: AnthropicNativeCompactionOptions): Promise<AnthropicNativeCompactionResponse>;
@@ -297,6 +297,8 @@ export interface CompactionPreparation {
297
297
  turnPrefixMessages: AgentMessage[];
298
298
  /** Messages kept in full after compaction (recent history) */
299
299
  recentMessages: AgentMessage[];
300
+ /** Entry IDs parallel to recentMessages, for an Anthropic-safe keep-tail boundary. */
301
+ recentEntryIds?: string[];
300
302
  /** Whether this is a split turn (cut point in middle of turn) */
301
303
  isSplitTurn: boolean;
302
304
  tokensBefore: number;
@@ -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,13 +2,16 @@ 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";
8
9
  export * from "./run-collector.js";
10
+ export * from "./sent-tool-definitions.js";
9
11
  export * from "./speculative-execution.js";
10
12
  export * from "./telemetry.js";
11
13
  export * from "./thinking.js";
14
+ export * from "./tool-context.js";
12
15
  export * from "./tokenizer.js";
13
16
  export * from "./types.js";
14
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,17 @@
1
+ import type { Message, Tool } from "@oh-my-pi/pi-ai";
2
+ /**
3
+ * Last wire definition this Agent sent for each tool name, so a provider that keeps
4
+ * withdrawn tools declared (Anthropic `tool_removal`) can re-declare them byte-identically.
5
+ * Used by prepareProviderCall and Agent.buildSideRequestContext.
6
+ */
7
+ export declare class SentToolDefinitions {
8
+ #private;
9
+ /** Remember the definitions a request is about to send. */
10
+ record(tools: readonly Tool[]): void;
11
+ /**
12
+ * Definitions for names the latest `requestControls.tools.declared` in `messages` holds
13
+ * that are not in `active`; undefined when none. Names never sent by this Agent are
14
+ * skipped: the provider drops them from the declaration.
15
+ */
16
+ inactiveFor(messages: readonly Message[], active: readonly Tool[]): Tool[] | undefined;
17
+ }
@@ -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;
@@ -3,6 +3,7 @@ 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
5
  import type { AgentRunCoverage, AgentRunSummary } from "./run-collector.js";
6
+ import type { SentToolDefinitions } from "./sent-tool-definitions.js";
6
7
  import type { AgentTelemetryConfig } from "./telemetry.js";
7
8
  /** Stream function - can return sync or Promise for async config lookup */
8
9
  export type StreamFn = (...args: Parameters<typeof streamSimple>) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
@@ -35,6 +36,12 @@ export interface AgentTurnEndContext {
35
36
  message: AgentMessage;
36
37
  /** Tool results produced by this turn, already paired with `message` in the live context. */
37
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[];
38
45
  /** True when the current tool-loop batch is continuing without yielding to post-turn steering. */
39
46
  willContinue: boolean;
40
47
  }
@@ -117,8 +124,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
117
124
  model: Model;
118
125
  /**
119
126
  * When to interrupt tool execution for steering messages.
120
- * - "immediate" = check after each tool call (default)
121
- * - "wait" = defer steering until the current turn completes
127
+ * - "immediate" = cut interruptible waits short and raise the cooperative
128
+ * `steeringSignal` for other running tools (default)
129
+ * - "wait" = let non-interruptible tools finish undisturbed; interruptible
130
+ * waits are still cut short, since they have no work to complete
122
131
  */
123
132
  interruptMode?: "immediate" | "wait";
124
133
  /**
@@ -185,6 +194,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
185
194
  * and provider send.
186
195
  */
187
196
  transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
197
+ /** Remembers sent tool definitions to fill {@link Context.inactiveTools}. */
198
+ sentToolDefinitions?: SentToolDefinitions;
188
199
  /**
189
200
  * Resolves the API key or resolver for the current model before each LLM call.
190
201
  *
@@ -204,8 +215,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
204
215
  /**
205
216
  * Peeks whether steering messages are queued, without consuming them.
206
217
  *
207
- * Polled while a tool batch runs (unless interruptMode is "wait") to decide
208
- * whether to abort in-flight and skip not-yet-started *interruptible* waits;
218
+ * Polled while a tool batch runs (in "wait" mode, only when the batch holds an
219
+ * interruptible tool) to decide whether to abort in-flight and skip
220
+ * not-yet-started *interruptible* waits;
209
221
  * every other already-emitted call still executes and the message injects
210
222
  * at the batch boundary. The queue keeps
211
223
  * owning its messages until the loop reaches the next injection boundary and
@@ -232,7 +244,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
232
244
  * Peeks whether IRC messages should interrupt an interruptible waiting tool.
233
245
  *
234
246
  * Uses the same delivery rules as steering: the poll is non-consuming, only
235
- * runs for interruptible tools, and is ignored when interruptMode is "wait".
247
+ * runs for interruptible tools, and cuts them short even when interruptMode
248
+ * is "wait".
236
249
  * The host owns message injection at the next boundary.
237
250
  */
238
251
  hasIrcInterrupts?: () => boolean | Promise<boolean>;
@@ -241,8 +254,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
241
254
  * process) is queued for aside injection at the next boundary.
242
255
  *
243
256
  * Same rules as {@link hasIrcInterrupts}: non-consuming, only cuts
244
- * *interruptible* waits short, ignored when interruptMode is "wait". Without
245
- * it a completion notice sits behind an hour-long `hub wait` that the agent
257
+ * *interruptible* waits short, in either interruptMode. Without
258
+ * it a completion notice sits behind an hour-long `wait` that the agent
246
259
  * would have abandoned had it seen the notice. Unlike a peer IRC it never
247
260
  * raises {@link ToolCallContext.steeringSignal}: a queued completion must
248
261
  * not push ordinary foreground work (auto-background bash/eval) into the
@@ -277,7 +290,11 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
277
290
  onBeforeYield?: () => Promise<void> | void;
278
291
  /**
279
292
  * Provides tool execution context, resolved per tool call.
280
- * 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.
281
298
  */
282
299
  getToolContext?: (toolCall?: ToolCallContext) => AgentToolContext | undefined;
283
300
  /**
@@ -351,6 +368,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
351
368
  * model's text output back into canonical `toolCall` blocks.
352
369
  */
353
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;
354
377
  /**
355
378
  * When owned (in-band) tool calling is active and the model starts
356
379
  * fabricating a tool result inside its own turn, control how the loop reacts:
@@ -528,6 +551,13 @@ export interface ToolCallContext {
528
551
  * always safe (the message injects at the next batch boundary).
529
552
  */
530
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;
531
561
  }
532
562
  /** A single tool-call content block emitted by an assistant message. */
533
563
  export type AgentToolCall = Extract<AssistantMessage["content"][number], {
@@ -716,11 +746,19 @@ export interface SpeculativeToolExecutionConfig {
716
746
  * written back to the tool-call block on the assistant message, and seen by
717
747
  * history, scheduling, execution events, and `tool.execute` alike. It is
718
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.
719
756
  */
720
757
  export interface BeforeToolCallResult {
721
758
  block?: boolean;
722
759
  reason?: string;
723
760
  args?: Record<string, unknown>;
761
+ additionalContext?: string;
724
762
  }
725
763
  /**
726
764
  * Partial override returned from `afterToolCall`.
@@ -867,6 +905,16 @@ export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprov
867
905
  * Apps can extend via declaration merging.
868
906
  */
869
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;
870
918
  /** Present only while the matching outer tool owns its finalized stream session. */
871
919
  [SPECULATIVE_STREAM_SESSION]?: ToolSpeculationStreamSession;
872
920
  }
@@ -903,6 +951,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
903
951
  loadMode?: ToolLoadMode;
904
952
  /** Short one-line summary used for tool discovery indexes. */
905
953
  summary?: string;
954
+ /**
955
+ * On-demand documentation topics (`topic → markdown`), readable as
956
+ * `xd://<tool>/<topic>`. Lets a tool keep large sub-surfaces out of its
957
+ * description and advertise only a one-line pointer per topic.
958
+ */
959
+ docTopics?(): Readonly<Record<string, string>>;
906
960
  /**
907
961
  * Concurrency mode for tool scheduling when multiple calls are in one turn.
908
962
  * - "shared": can run alongside other shared tools (default)
@@ -926,7 +980,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
926
980
  * cleanly (e.g. `job` poll), so the abort surfaces the tool's current
927
981
  * snapshot rather than corrupting a side effect. Every other call runs to
928
982
  * completion even when steering is queued; the message lands at the next
929
- * batch boundary. Honored only when `interruptMode` is "immediate".
983
+ * batch boundary. Honored in both `interruptMode`s.
930
984
  */
931
985
  interruptible?: boolean | ((args: Partial<Static<TParameters>>) => boolean);
932
986
  /**
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-agent-core",
4
- "version": "18.2.11",
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
- "author": "Stencil Labs, Inc.",
7
+ "author": {
8
+ "name": "Stencil Labs, Inc.",
9
+ "url": "https://stencil.so"
10
+ },
8
11
  "contributors": [
9
12
  "Mario Zechner"
10
13
  ],
@@ -35,16 +38,16 @@
35
38
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
36
39
  },
37
40
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "18.2.11",
39
- "@oh-my-pi/pi-catalog": "18.2.11",
40
- "@oh-my-pi/pi-natives": "18.2.11",
41
- "@oh-my-pi/pi-utils": "18.2.11",
42
- "@oh-my-pi/pi-wire": "18.2.11",
43
- "@oh-my-pi/snapcompact": "18.2.11",
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",
44
47
  "@opentelemetry/api": "^1.9.1"
45
48
  },
46
49
  "devDependencies": {
47
- "@oh-my-pi/omptype": "18.2.11",
50
+ "@oh-my-pi/omptype": "18.3.1",
48
51
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
52
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
53
  "@types/bun": "^1.3.14"