@oh-my-pi/pi-agent-core 18.1.17 → 18.1.19

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,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.19] - 2026-09-12
6
+
7
+ ### Added
8
+
9
+ - Added `Agent.getPendingToolResults()` for reconstructing live displays before buffered tool results are persisted ([#11868](https://github.com/can1357/oh-my-pi/pull/11868) by [@serverinspector](https://github.com/serverinspector)).
10
+ - Added opt-in host authorization and exact-once streamed child execution for discard-safe local reads.
11
+
12
+ ### Changed
13
+
14
+ - `Tool <name> not found` now also suggests mounted `xd://` devices, not just the advertised tool set, via the new `suggestFallbackToolNames` option ([#11516](https://github.com/can1357/oh-my-pi/issues/11516), [#10109](https://github.com/can1357/oh-my-pi/issues/10109) by [@oldschoola](https://github.com/oldschoola)).
15
+
16
+ ### Fixed
17
+
18
+ - Speculative stream sessions are now discarded when a hook or argument transform replaces a call's arguments while keeping its ID, instead of releasing deferred work planned from the original code ([#11889](https://github.com/can1357/oh-my-pi/pull/11889) by [@h4vc](https://github.com/h4vc)).
19
+
20
+ ## [18.1.18] - 2026-09-11
21
+
22
+ ### Added
23
+
24
+ - Anthropic server-side compaction as a `remote` compaction backend: model lines the beta supports (`compat.supportsServerCompaction`, rule-owned in the catalog: Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5) on the official endpoint, resolved the way the provider routes requests, plus Anthropic-compatible routes with `remoteCompaction.enabled`, compact by re-issuing the live turn's own request — same system prompt, tools, and history, so it reads the prompt cache the last turn wrote — with the `compact_20260112` edit paused after the summary and the harness summary prompt as `instructions`. The instructions name where the retained tail begins so the summary covers only the history the rebuilt context drops. The API's summary is stored as the entry text and as `preserveData.anthropicCompaction`, replayed natively on later Anthropic requests and read as plain text by every other provider; the retained tail comes from session entries as with a local summary. Contexts below 55k tokens (the API trigger floor plus margin) keep summarizing locally, and a response without a summary is a native failure, like the OpenAI lanes. An aborted compaction response is the abort (a cancellation, never a native failure) and an error response keeps its HTTP status, so auth and timeout classification match the OpenAI lanes; the block's opaque `encrypted_content` is persisted as `preserveData.anthropicCompaction.encryptedContent` and replayed verbatim.
25
+
26
+ ### Fixed
27
+
28
+ - `compact()` now forwards the caller's `oneshotRetry` opt-out to every summarization oneshot; auto-compaction's outer retry loop no longer multiplies with the inner transient-failure retries.
29
+
5
30
  ## [18.1.17] - 2026-09-10
6
31
 
7
32
  ### Changed
@@ -1,9 +1,9 @@
1
- import { type ApiKey, type AssistantMessage, type AssistantMessageEvent, type Context, type CursorExecHandlers, type CursorToolResultHandler, type Effort, type ImageContent, type Message, type Model, type ProviderSessionState, type ServiceTier, type SimpleStreamOptions, type ThinkingBudgets, type ToolChoice } from "@oh-my-pi/pi-ai";
1
+ import { type ApiKey, type AssistantMessage, type AssistantMessageEvent, type Context, type CursorExecHandlers, type CursorToolResultHandler, type Effort, type ImageContent, type Message, type Model, type ProviderSessionState, type ServiceTier, type SimpleStreamOptions, type ThinkingBudgets, type ToolChoice, type ToolResultMessage } from "@oh-my-pi/pi-ai";
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
5
  import { Tokenizer } from "./tokenizer.js";
6
- import type { AgentBeforeModelCall, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideMessage, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
6
+ import type { AgentBeforeModelCall, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideMessage, SpeculativeToolExecutionConfig, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
7
7
  export declare class AgentBusyError extends Error {
8
8
  constructor(message?: string);
9
9
  }
@@ -135,12 +135,20 @@ export interface AgentOptions {
135
135
  * Use for deobfuscating secrets or rewriting arguments.
136
136
  */
137
137
  transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
138
+ /** Host authorization and telemetry for opt-in speculative tool execution. */
139
+ speculativeToolExecution?: SpeculativeToolExecutionConfig;
138
140
  /**
139
141
  * Resolve a tool call whose name matched no advertised tool. Lets hosts
140
142
  * route calls to tools exposed through side transports (e.g. `xd://`
141
143
  * device mounts) instead of failing with "Tool not found".
142
144
  */
143
- resolveFallbackTool?: (name: string) => AgentTool<any> | undefined;
145
+ resolveFallbackTool?: (name: string, advertised: readonly AgentTool<any>[]) => AgentTool<any> | undefined;
146
+ /**
147
+ * Names routable by {@link resolveFallbackTool} that the advertised set
148
+ * omits (e.g. `xd://` device mounts), used only to suggest a target when a
149
+ * call misses.
150
+ */
151
+ suggestFallbackToolNames?: () => Iterable<string>;
144
152
  /** Enable intent tracing schema injection/stripping in the harness. */
145
153
  intentTracing?: boolean;
146
154
  /**
@@ -449,6 +457,8 @@ export declare class Agent {
449
457
  /** Non-consuming view of the pending follow-up queue. See
450
458
  * {@link peekSteeringQueue}. */
451
459
  peekFollowUpQueue(): readonly AgentMessage[];
460
+ /** Nonblocking snapshot of the latest results, including provisional payloads while transforms are pending. */
461
+ getPendingToolResults(): readonly ToolResultMessage[];
452
462
  get isAborting(): boolean;
453
463
  /**
454
464
  * Remove and return the last steering message from the queue (LIFO).
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Anthropic server-side compaction (`compact-2026-01-12` beta).
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.
13
+ */
14
+ import type { AnthropicCompactionPayload, ApiKey, Effort, Message, Model, SimpleStreamOptions, Tool, Usage } from "@oh-my-pi/pi-ai";
15
+ import { type InstrumentedChatSpanOptions } from "../telemetry.js";
16
+ 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
+ /** Summary persisted under {@link ANTHROPIC_COMPACTION_PRESERVE_KEY}. */
27
+ export interface AnthropicCompactionPreserveData {
28
+ provider: string;
29
+ content: string;
30
+ /** Opaque provider state the API attached to the block; replayed verbatim. */
31
+ encryptedContent?: string;
32
+ /** Harness file metadata (`<files>` section) replayed after the native block. */
33
+ filesText?: string;
34
+ /** Model that wrote the summary. */
35
+ model?: string;
36
+ /** Prompt tokens the compaction request processed, for display. */
37
+ usedTokens?: number;
38
+ }
39
+ /**
40
+ * Whether a model compacts through the Anthropic compaction beta. Model
41
+ * eligibility is catalog policy (`compat.supportsServerCompaction`, the
42
+ * lineage the beta documents); endpoint eligibility is resolved the way the
43
+ * provider routes requests, so a Foundry or `ANTHROPIC_BASE_URL` reroute of a
44
+ * first-party model is excluded unless the route opted in with
45
+ * `remoteCompaction.enabled`.
46
+ */
47
+ export declare function shouldUseAnthropicNativeCompaction(model: Model): model is Model<"anthropic-messages">;
48
+ export declare function getPreservedAnthropicCompactionData(preserveData: Record<string, unknown> | undefined): AnthropicCompactionPreserveData | undefined;
49
+ /** Set or strip the Anthropic compaction slot; a new compaction never inherits a stale summary. */
50
+ export declare function withAnthropicCompactionPreserveData(preserveData: Record<string, unknown> | undefined, compaction: AnthropicCompactionPreserveData | undefined): Record<string, unknown> | undefined;
51
+ /** Replay payload for a compaction summary the active model produced natively. */
52
+ export declare function getAnthropicCompactionPayload(preserveData: Record<string, unknown> | undefined): AnthropicCompactionPayload | undefined;
53
+ /**
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.
65
+ */
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;
81
+ export interface AnthropicNativeCompactionRequest {
82
+ systemPrompt: string[];
83
+ messages: Message[];
84
+ tools?: Tool[];
85
+ instructions: string;
86
+ maxTokens: number;
87
+ reasoning?: Effort;
88
+ }
89
+ export interface AnthropicNativeCompactionResponse {
90
+ content: string;
91
+ encryptedContent?: string;
92
+ usage: Usage;
93
+ model: string;
94
+ }
95
+ export interface AnthropicNativeCompactionOptions extends Pick<SimpleStreamOptions, "initiatorOverride" | "metadata" | "fetch" | "sessionId" | "promptCacheKey" | "providerSessionState" | "maxInFlightRequests">, Pick<InstrumentedChatSpanOptions, "completeImpl" | "telemetry" | "retry"> {
96
+ }
97
+ /**
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`
100
+ * resolves terminal failures as messages, so their classification is restored
101
+ * here: an aborted response is an `AbortError` (a cancellation, never a native
102
+ * failure) and an error response keeps its HTTP status, so auth and timeout
103
+ * 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.
107
+ */
108
+ export declare function requestAnthropicNativeCompaction(model: Model<"anthropic-messages">, apiKey: ApiKey, request: AnthropicNativeCompactionRequest, signal: AbortSignal | undefined, options: AnthropicNativeCompactionOptions): Promise<AnthropicNativeCompactionResponse>;
@@ -65,7 +65,11 @@ export declare const DEFAULT_RESERVE_TOKENS = 16384;
65
65
  */
66
66
  export declare const MAX_SUMMARY_TOKENS = 16384;
67
67
  export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
68
- /** Whether a compaction candidate preserves provider-native transport under the effective settings. */
68
+ /**
69
+ * Whether a compaction candidate preserves provider-native transport under the
70
+ * effective settings: an OpenAI Responses compact route (V1 or streamed V2) or
71
+ * the Anthropic compaction beta.
72
+ */
69
73
  export declare function shouldUseProviderNativeCompaction(model: Model, settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">): boolean;
70
74
  /**
71
75
  * Calculate total context tokens from usage.
@@ -290,6 +294,8 @@ export interface CompactionPreparation {
290
294
  tokensBefore: number;
291
295
  /** Summary from previous compaction, for iterative update */
292
296
  previousSummary?: string;
297
+ /** ISO timestamp of the previous compaction entry, for iterative update */
298
+ previousSummaryTimestamp?: string;
293
299
  /** Preserved opaque compaction payload from the previous compaction, if any. */
294
300
  previousPreserveData?: Record<string, unknown>;
295
301
  /** File operations extracted from messagesToSummarize */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Compaction and summarization utilities.
3
3
  */
4
+ export * from "./anthropic.js";
4
5
  export * from "./branch-summarization.js";
5
6
  export * from "./compaction.js";
6
7
  export * from "./entries.js";
@@ -6,6 +6,7 @@ export * from "./pause.js";
6
6
  export * from "./proxy.js";
7
7
  export * from "./replay-policy.js";
8
8
  export * from "./run-collector.js";
9
+ export * from "./speculative-execution.js";
9
10
  export * from "./telemetry.js";
10
11
  export * from "./thinking.js";
11
12
  export * from "./tokenizer.js";
@@ -0,0 +1,63 @@
1
+ import { type AssistantMessage } from "@oh-my-pi/pi-ai";
2
+ import type { AgentContext, AgentLoopConfig, AgentTool, AgentToolCall, AgentToolResult, SpeculativeChildDefinition, SpeculativeChildHandle, SpeculativeToolExecutionConfig, ToolSpeculationEffect, ToolSpeculationStreamSession } from "./types.js";
3
+ export type SpeculativeRawOutcome = {
4
+ result: AgentToolResult<unknown>;
5
+ isError: boolean;
6
+ };
7
+ export type CoerceToolResult = (raw: unknown) => {
8
+ result: AgentToolResult<unknown>;
9
+ malformed: boolean;
10
+ };
11
+ type CoordinatorEnvironment = {
12
+ context: AgentContext;
13
+ loopConfig: AgentLoopConfig;
14
+ signal?: AbortSignal;
15
+ };
16
+ /** JSON canonicalization shared by assessment and final reconciliation. */
17
+ export declare function canonicalJson(value: unknown, ancestors?: Set<object>): string;
18
+ export declare function createExecutionFingerprint(toolCall: AgentToolCall, executionArgs: Readonly<Record<string, unknown>>, effect: ToolSpeculationEffect): string;
19
+ /** Rejects malformed or ambiguous effects and returns a frozen logical identity. */
20
+ export declare function normalizeSpeculationEffect(effect: unknown): ToolSpeculationEffect | undefined;
21
+ /** One streamed assistant response's invisible speculative-operation lifecycle. */
22
+ export declare class SpeculativeOperationCoordinator {
23
+ #private;
24
+ readonly config: SpeculativeToolExecutionConfig;
25
+ readonly environment?: CoordinatorEnvironment | undefined;
26
+ constructor(config: SpeculativeToolExecutionConfig, environment?: CoordinatorEnvironment | undefined);
27
+ get maxInFlight(): number;
28
+ get size(): number;
29
+ register(_contentIndex: number): void;
30
+ registerStreamSession(toolCallId: string, session: ToolSpeculationStreamSession): boolean;
31
+ streamSession(toolCallId: string): ToolSpeculationStreamSession | undefined;
32
+ takeStreamSession(toolCallId: string): ToolSpeculationStreamSession | undefined;
33
+ discardStreamSession(toolCallId: string, reason: string): Promise<void>;
34
+ finalizeAdmissions(): Promise<void>;
35
+ attach(message: AssistantMessage): void;
36
+ static take(message: AssistantMessage): SpeculativeOperationCoordinator | undefined;
37
+ static discardForMessage(message: AssistantMessage, reason: string, outcome?: "discarded" | "aborted"): void;
38
+ ineligible(toolCall: AgentToolCall, reason: string, source?: "direct" | "eval_shadow", parentToolCallId?: string): false;
39
+ close(reason: string, outcome?: "discarded" | "aborted"): Promise<void>;
40
+ discardAll(reason: string, outcome?: "discarded" | "aborted"): Promise<void>;
41
+ /**
42
+ * Settles queued admissions without releasing host-deferred work. Lets
43
+ * final reconciliation reuse an admission-time transform result instead of
44
+ * running the (possibly stateful) transform a second time. Unlike
45
+ * `finalizeAdmissions` this never starts deferred candidates, so the
46
+ * `beforeToolCall` gate keeps its release semantics.
47
+ */
48
+ settleAdmissions(): Promise<void>;
49
+ /**
50
+ * Admission-time transformed args for a direct candidate whose raw call is
51
+ * unchanged. Returns undefined when there is no usable candidate, in which
52
+ * case the caller recomputes at most once via the transform. Gating on the
53
+ * raw call keeps `beforeToolCall` policy intact: any hook revision changes
54
+ * the raw args and forces a fresh transform plus re-reconciliation.
55
+ */
56
+ directExecutionArgsFor(toolCallId: string, rawArgs: Readonly<Record<string, unknown>>): Record<string, unknown> | undefined;
57
+ reconcileFinalCalls(calls: ReadonlyMap<string, AgentToolCall>): Promise<void>;
58
+ discardChildren(parentToolCallId: string, reason: string): Promise<void>;
59
+ claim(tool: AgentTool | undefined, toolCall: AgentToolCall, args: Record<string, unknown>): Promise<SpeculativeRawOutcome | undefined>;
60
+ admitFinalized(context: AgentContext, toolCall: AgentToolCall, loopConfig: AgentLoopConfig, signal: AbortSignal | undefined): void;
61
+ admit(definition: SpeculativeChildDefinition): Promise<SpeculativeChildHandle | undefined>;
62
+ }
63
+ export {};
@@ -8,6 +8,8 @@ import type { AgentTelemetryConfig } from "./telemetry.js";
8
8
  export type StreamFn = (...args: Parameters<typeof streamSimple>) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
9
9
  /** Called once an aside has been inserted into the agent's live context. */
10
10
  export declare const ASIDE_MESSAGE_COMMIT: unique symbol;
11
+ /** Symbol-keyed handoff for one finalized, tool-owned stream speculation session. */
12
+ export declare const SPECULATIVE_STREAM_SESSION: unique symbol;
11
13
  /** Called when an aside was drained but the agent loop ended before inserting it. */
12
14
  export declare const ASIDE_MESSAGE_DISCARD: unique symbol;
13
15
  export type CommittableAsideMessage = AgentMessage & {
@@ -281,13 +283,33 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
281
283
  * Use for deobfuscating secrets or rewriting arguments.
282
284
  */
283
285
  transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
286
+ /**
287
+ * Opt-in speculative execution for finalized, discard-safe tool calls.
288
+ *
289
+ * Candidates remain invisible until ordinary dispatch commits their result.
290
+ */
291
+ speculativeToolExecution?: SpeculativeToolExecutionConfig;
284
292
  /**
285
293
  * Resolve a tool call whose name matched no advertised tool (including
286
294
  * `customWireName` aliases). Lets hosts route calls to tools they expose
287
295
  * through side transports (e.g. `xd://` device mounts) instead of failing
288
296
  * with "Tool not found". Returning `undefined` keeps the failure.
297
+ *
298
+ * `advertised` is the very snapshot exact-name dispatch just searched — the
299
+ * set offered to THIS request. A host must resolve against it rather than
300
+ * its own live tool state: an MCP `tools/list_changed` mid-stream reassigns
301
+ * the agent's tools, so live state can hold a roster the model never saw,
302
+ * and a name-recovering host would dispatch a tool this request never
303
+ * advertised while exact dispatch still answered from the snapshot.
304
+ */
305
+ resolveFallbackTool?: (name: string, advertised: readonly AgentTool<any>[]) => AgentTool<any> | undefined;
306
+ /**
307
+ * Names reachable through {@link resolveFallbackTool} but absent from the
308
+ * advertised set (e.g. `xd://` device mounts). Consulted only to name a
309
+ * plausible target when a call misses, so a mis-transcribed device call is
310
+ * recoverable; never a dispatch source.
289
311
  */
290
- resolveFallbackTool?: (name: string) => AgentTool<any> | undefined;
312
+ suggestFallbackToolNames?: () => Iterable<string>;
291
313
  /**
292
314
  * Enable intent tracing for tool calls.
293
315
  * When enabled, the harness injects a `string` field into tool schemas sent to the model,
@@ -491,6 +513,178 @@ export interface ToolCallContext {
491
513
  export type AgentToolCall = Extract<AssistantMessage["content"][number], {
492
514
  type: "toolCall";
493
515
  }>;
516
+ export interface SpeculativeResourceAccess {
517
+ scheme: "file";
518
+ path: string;
519
+ access: "read";
520
+ }
521
+ /** Declares an operation whose early execution can be discarded without rollback. */
522
+ export type ToolSpeculationEffect = {
523
+ kind: "pure";
524
+ } | {
525
+ kind: "local_read";
526
+ resources: readonly SpeculativeResourceAccess[];
527
+ };
528
+ export type ToolSpeculationAssessment = {
529
+ eligible: false;
530
+ reason: string;
531
+ } | {
532
+ eligible: true;
533
+ effect: ToolSpeculationEffect;
534
+ };
535
+ /** Immutable finalized call data provided to a tool-owned policy. */
536
+ export interface ToolSpeculationAssessmentContext {
537
+ toolCall: AgentToolCall;
538
+ args: Readonly<Record<string, unknown>>;
539
+ }
540
+ export interface ToolSpeculationExecutionContext extends ToolSpeculationAssessmentContext {
541
+ effect: ToolSpeculationEffect;
542
+ }
543
+ export interface ToolSpeculationCommitContext extends ToolSpeculationExecutionContext {
544
+ physicalOutcome: SpeculativePhysicalOutcome;
545
+ }
546
+ export interface ToolSpeculationDiscardContext extends ToolSpeculationExecutionContext {
547
+ reason: string;
548
+ }
549
+ export interface SpeculativePhysicalOutcome {
550
+ kind: "result";
551
+ result: AgentToolResult<unknown>;
552
+ isError: boolean;
553
+ /** Opaque evidence binding the result to the local bytes actually consumed. */
554
+ evidence?: unknown;
555
+ }
556
+ export interface SpeculativeToolReference {
557
+ name: string;
558
+ approval?: ToolApproval;
559
+ formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
560
+ }
561
+ export interface SpeculativeOperationContext extends ToolSpeculationExecutionContext {
562
+ tool: SpeculativeToolReference;
563
+ candidateId: string;
564
+ source: "direct" | "eval_shadow";
565
+ dependencies: readonly string[];
566
+ }
567
+ export interface SpeculativeCommitContext extends SpeculativeOperationContext {
568
+ physicalOutcome: SpeculativePhysicalOutcome;
569
+ }
570
+ export interface SpeculativeDiscardContext extends SpeculativeOperationContext {
571
+ reason: string;
572
+ }
573
+ export type SpeculativeAuthorization = {
574
+ allowed: false;
575
+ reason: string;
576
+ } | {
577
+ allowed: true;
578
+ deferBeforeToolCall?: boolean;
579
+ };
580
+ export type SpeculativeCommitDecision = {
581
+ kind: "committed";
582
+ result: AgentToolResult<unknown>;
583
+ } | {
584
+ kind: "fallback";
585
+ reason: string;
586
+ } | {
587
+ kind: "failed";
588
+ error: unknown;
589
+ };
590
+ export interface SpeculativeExecutionHost {
591
+ authorize(context: SpeculativeOperationContext): SpeculativeAuthorization | Promise<SpeculativeAuthorization>;
592
+ /**
593
+ * Capture content evidence after admission gates pass but before the
594
+ * candidate executes. The coordinator invokes this immediately before
595
+ * starting speculative execution — which for hook-deferred candidates is
596
+ * after `beforeToolCall` runs — so content inspection never precedes a
597
+ * hook that may block the call. Return false (or throw) to veto the
598
+ * candidate without executing it. Hosts without this hook keep the legacy
599
+ * behavior of capturing during `authorize`.
600
+ */
601
+ captureEvidence?(context: SpeculativeOperationContext): boolean | Promise<boolean>;
602
+ validate?(context: SpeculativeCommitContext): boolean | Promise<boolean>;
603
+ commit?(context: SpeculativeCommitContext, commitDefault: () => Promise<AgentToolResult<unknown>>): Promise<SpeculativeCommitDecision>;
604
+ discard?(context: SpeculativeDiscardContext): void | Promise<void>;
605
+ close?(reason: string): void | Promise<void>;
606
+ }
607
+ export interface ToolSpeculationStreamContext {
608
+ readonly coordinator: SpeculativeOperationSink;
609
+ readonly parentToolCallId: string;
610
+ }
611
+ /** One dependency-aware child operation projected by a streamed outer tool. */
612
+ export interface SpeculativeChildDefinition {
613
+ candidateId: string;
614
+ parentToolCallId: string;
615
+ dependencies: readonly string[];
616
+ toolCall: AgentToolCall;
617
+ tool: AgentTool;
618
+ source: "eval_shadow";
619
+ virtualDurationMs?: number;
620
+ }
621
+ /** Opaque ownership handle returned after agent-core validates and authorizes a child. */
622
+ export interface SpeculativeChildHandle {
623
+ readonly candidateId: string;
624
+ readonly fingerprint: string;
625
+ readonly effect: ToolSpeculationEffect;
626
+ readonly outcome: Promise<SpeculativePhysicalOutcome>;
627
+ commit(actualArgs: Readonly<Record<string, unknown>>): Promise<AgentToolResult<unknown> | undefined>;
628
+ discard(reason: string): Promise<void>;
629
+ }
630
+ export interface ToolSpeculationStreamSession {
631
+ /** True when the tool owns claim routing and does not need an AgentToolContext attachment. */
632
+ readonly contextIndependent?: boolean;
633
+ update(toolCall: AgentToolCall, partialJson?: string): void | Promise<void>;
634
+ finalize(context: ToolSpeculationAssessmentContext): void | Promise<void>;
635
+ commit(): void | Promise<void>;
636
+ discard(reason: string): void | Promise<void>;
637
+ /**
638
+ * Whether the session's streamed plan still authorizes these final
639
+ * arguments. The coordinator discards the session when the finalized call
640
+ * kept its ID but a hook or argument transform replaced its arguments:
641
+ * deferred work planned from the original code must never release.
642
+ * Sessions without this predicate are always retained.
643
+ */
644
+ matchesFinalArgs?(args: Readonly<Record<string, unknown>>): boolean;
645
+ }
646
+ export interface SpeculativeOperationSink {
647
+ readonly maxInFlight: number;
648
+ admit(definition: SpeculativeChildDefinition): Promise<SpeculativeChildHandle | undefined>;
649
+ discardChildren?(parentToolCallId: string, reason: string): void | Promise<void>;
650
+ close(reason: string): void | Promise<void>;
651
+ }
652
+ export interface ToolSpeculationPolicy {
653
+ finalized?: {
654
+ assess(context: ToolSpeculationAssessmentContext): ToolSpeculationAssessment | Promise<ToolSpeculationAssessment>;
655
+ execute(context: ToolSpeculationExecutionContext, signal: AbortSignal): Promise<SpeculativePhysicalOutcome>;
656
+ commit?(context: ToolSpeculationCommitContext, outcome: SpeculativePhysicalOutcome): Promise<AgentToolResult<unknown>>;
657
+ discard?(context: ToolSpeculationDiscardContext): void | Promise<void>;
658
+ };
659
+ stream?: {
660
+ open(context: ToolSpeculationStreamContext): ToolSpeculationStreamSession | Promise<ToolSpeculationStreamSession | undefined>;
661
+ };
662
+ }
663
+ /** Diagnostic information for one speculative operation. */
664
+ export interface SpeculativeToolTelemetry {
665
+ source: "direct" | "eval_shadow";
666
+ candidateId: string;
667
+ parentToolCallId?: string;
668
+ toolName: string;
669
+ effectKind?: ToolSpeculationEffect["kind"];
670
+ candidateStartedAt?: number;
671
+ candidateFinishedAt?: number;
672
+ dispatchReachedAt?: number;
673
+ dependencyCount: number;
674
+ queueMs?: number;
675
+ executionDurationMs?: number;
676
+ overlapMs?: number;
677
+ outcome: "committed" | "discarded" | "ineligible" | "fingerprint_mismatch" | "aborted" | "commit_conflict";
678
+ reason?: string;
679
+ resourceCount: number;
680
+ }
681
+ /** Opt-in configuration for discard-safe speculative tool execution. */
682
+ export interface SpeculativeToolExecutionConfig {
683
+ enabled: boolean;
684
+ maxInFlight?: number;
685
+ host?: SpeculativeExecutionHost;
686
+ onTelemetry?: (event: SpeculativeToolTelemetry) => void;
687
+ }
494
688
  /**
495
689
  * Result returned from `beforeToolCall`.
496
690
  *
@@ -533,7 +727,7 @@ export interface BeforeToolCallContext {
533
727
  /** The raw tool call block from `assistantMessage.content`. */
534
728
  toolCall: AgentToolCall;
535
729
  /** The resolved tool the call dispatches to. */
536
- tool: AgentTool<any>;
730
+ tool: AgentTool;
537
731
  /**
538
732
  * Validated tool arguments. The same reference is forwarded to `tool.execute`
539
733
  * (after any `transformToolCallArguments` pass), so in-place mutations stick;
@@ -653,6 +847,8 @@ export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprov
653
847
  * Apps can extend via declaration merging.
654
848
  */
655
849
  export interface AgentToolContext {
850
+ /** Present only while the matching outer tool owns its finalized stream session. */
851
+ [SPECULATIVE_STREAM_SESSION]?: ToolSpeculationStreamSession;
656
852
  }
657
853
  export type AgentToolExecFn<TParameters extends TSchema = TSchema, TDetails = any, TTheme = unknown> = (this: AgentTool<TParameters, TDetails, TTheme>, toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>, context?: AgentToolContext) => Promise<AgentToolResult<TDetails, TParameters>>;
658
854
  /** Live receiver for a tool call's streamed arguments (see AgentTool.openArgStream). */
@@ -693,6 +889,11 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
693
889
  * - function: resolved per call from the (raw, pre-validation) arguments
694
890
  */
695
891
  concurrency?: "shared" | "exclusive" | ((args: Partial<Static<TParameters>>) => "shared" | "exclusive");
892
+ /**
893
+ * Declares the bounded, validated effect of a finalized call that may execute
894
+ * before ordinary dispatch commits its result.
895
+ */
896
+ speculation?: ToolSpeculationPolicy;
696
897
  /** If true, argument validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. */
697
898
  lenientArgValidation?: boolean;
698
899
  /**
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.1.17",
4
+ "version": "18.1.19",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,16 +35,16 @@
35
35
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "18.1.17",
39
- "@oh-my-pi/pi-catalog": "18.1.17",
40
- "@oh-my-pi/pi-natives": "18.1.17",
41
- "@oh-my-pi/pi-utils": "18.1.17",
42
- "@oh-my-pi/pi-wire": "18.1.17",
43
- "@oh-my-pi/snapcompact": "18.1.17",
38
+ "@oh-my-pi/pi-ai": "18.1.19",
39
+ "@oh-my-pi/pi-catalog": "18.1.19",
40
+ "@oh-my-pi/pi-natives": "18.1.19",
41
+ "@oh-my-pi/pi-utils": "18.1.19",
42
+ "@oh-my-pi/pi-wire": "18.1.19",
43
+ "@oh-my-pi/snapcompact": "18.1.19",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@oh-my-pi/omptype": "18.1.17",
47
+ "@oh-my-pi/omptype": "18.1.19",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
50
  "@types/bun": "^1.3.14"