@oh-my-pi/pi-agent-core 18.2.11 → 18.3.0
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 +12 -0
- package/THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/types/agent-loop.d.ts +6 -0
- package/dist/types/compaction/anthropic.d.ts +22 -52
- package/dist/types/compaction/compaction.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/sent-tool-definitions.d.ts +17 -0
- package/dist/types/types.d.ts +21 -8
- package/package.json +12 -9
- package/src/agent-loop.ts +55 -25
- package/src/agent.ts +8 -0
- package/src/compaction/anthropic.ts +60 -103
- package/src/compaction/compaction.ts +58 -48
- package/src/compaction/prompts/anthropic-compaction-instructions.md +2 -6
- package/src/index.ts +2 -0
- package/src/sent-tool-definitions.ts +40 -0
- package/src/types.ts +22 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.3.0] - 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added support for documenting agent tools on demand through the new `AgentTool.docTopics` method.
|
|
10
|
+
- Added `TOOL_INTERRUPT_ABORT_REASON` so interruptible tools can distinguish queued steering, peer messages, or background completions from a full run abort.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Improved interrupt handling so tools respect wait mode and can be interrupted when appropriate.
|
|
15
|
+
- Updated Anthropic compaction compatibility with signature verification.
|
|
16
|
+
|
|
5
17
|
## [18.2.11] - 2026-09-23
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/THIRD-PARTY-NOTICES.txt
CHANGED
|
@@ -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
|
|
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.
|
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Anthropic
|
|
2
|
+
* Anthropic on-demand compaction (`compact-2026-09-04` beta).
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* prompt, tools
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
105
|
-
*
|
|
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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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 "./sent-tool-definitions.js";
|
|
9
10
|
export * from "./speculative-execution.js";
|
|
10
11
|
export * from "./telemetry.js";
|
|
11
12
|
export * from "./thinking.js";
|
|
@@ -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
|
+
}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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>;
|
|
@@ -117,8 +118,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
117
118
|
model: Model;
|
|
118
119
|
/**
|
|
119
120
|
* When to interrupt tool execution for steering messages.
|
|
120
|
-
* - "immediate" =
|
|
121
|
-
*
|
|
121
|
+
* - "immediate" = cut interruptible waits short and raise the cooperative
|
|
122
|
+
* `steeringSignal` for other running tools (default)
|
|
123
|
+
* - "wait" = let non-interruptible tools finish undisturbed; interruptible
|
|
124
|
+
* waits are still cut short, since they have no work to complete
|
|
122
125
|
*/
|
|
123
126
|
interruptMode?: "immediate" | "wait";
|
|
124
127
|
/**
|
|
@@ -185,6 +188,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
185
188
|
* and provider send.
|
|
186
189
|
*/
|
|
187
190
|
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
191
|
+
/** Remembers sent tool definitions to fill {@link Context.inactiveTools}. */
|
|
192
|
+
sentToolDefinitions?: SentToolDefinitions;
|
|
188
193
|
/**
|
|
189
194
|
* Resolves the API key or resolver for the current model before each LLM call.
|
|
190
195
|
*
|
|
@@ -204,8 +209,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
204
209
|
/**
|
|
205
210
|
* Peeks whether steering messages are queued, without consuming them.
|
|
206
211
|
*
|
|
207
|
-
* Polled while a tool batch runs (
|
|
208
|
-
* whether to abort in-flight and skip
|
|
212
|
+
* Polled while a tool batch runs (in "wait" mode, only when the batch holds an
|
|
213
|
+
* interruptible tool) to decide whether to abort in-flight and skip
|
|
214
|
+
* not-yet-started *interruptible* waits;
|
|
209
215
|
* every other already-emitted call still executes and the message injects
|
|
210
216
|
* at the batch boundary. The queue keeps
|
|
211
217
|
* owning its messages until the loop reaches the next injection boundary and
|
|
@@ -232,7 +238,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
232
238
|
* Peeks whether IRC messages should interrupt an interruptible waiting tool.
|
|
233
239
|
*
|
|
234
240
|
* Uses the same delivery rules as steering: the poll is non-consuming, only
|
|
235
|
-
* runs for interruptible tools, and
|
|
241
|
+
* runs for interruptible tools, and cuts them short even when interruptMode
|
|
242
|
+
* is "wait".
|
|
236
243
|
* The host owns message injection at the next boundary.
|
|
237
244
|
*/
|
|
238
245
|
hasIrcInterrupts?: () => boolean | Promise<boolean>;
|
|
@@ -241,8 +248,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
241
248
|
* process) is queued for aside injection at the next boundary.
|
|
242
249
|
*
|
|
243
250
|
* Same rules as {@link hasIrcInterrupts}: non-consuming, only cuts
|
|
244
|
-
* *interruptible* waits short,
|
|
245
|
-
* it a completion notice sits behind an hour-long `
|
|
251
|
+
* *interruptible* waits short, in either interruptMode. Without
|
|
252
|
+
* it a completion notice sits behind an hour-long `wait` that the agent
|
|
246
253
|
* would have abandoned had it seen the notice. Unlike a peer IRC it never
|
|
247
254
|
* raises {@link ToolCallContext.steeringSignal}: a queued completion must
|
|
248
255
|
* not push ordinary foreground work (auto-background bash/eval) into the
|
|
@@ -903,6 +910,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
903
910
|
loadMode?: ToolLoadMode;
|
|
904
911
|
/** Short one-line summary used for tool discovery indexes. */
|
|
905
912
|
summary?: string;
|
|
913
|
+
/**
|
|
914
|
+
* On-demand documentation topics (`topic → markdown`), readable as
|
|
915
|
+
* `xd://<tool>/<topic>`. Lets a tool keep large sub-surfaces out of its
|
|
916
|
+
* description and advertise only a one-line pointer per topic.
|
|
917
|
+
*/
|
|
918
|
+
docTopics?(): Readonly<Record<string, string>>;
|
|
906
919
|
/**
|
|
907
920
|
* Concurrency mode for tool scheduling when multiple calls are in one turn.
|
|
908
921
|
* - "shared": can run alongside other shared tools (default)
|
|
@@ -926,7 +939,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
926
939
|
* cleanly (e.g. `job` poll), so the abort surfaces the tool's current
|
|
927
940
|
* snapshot rather than corrupting a side effect. Every other call runs to
|
|
928
941
|
* completion even when steering is queued; the message lands at the next
|
|
929
|
-
* batch boundary. Honored
|
|
942
|
+
* batch boundary. Honored in both `interruptMode`s.
|
|
930
943
|
*/
|
|
931
944
|
interruptible?: boolean | ((args: Partial<Static<TParameters>>) => boolean);
|
|
932
945
|
/**
|
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.
|
|
4
|
+
"version": "18.3.0",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
|
-
"author":
|
|
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.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.
|
|
42
|
-
"@oh-my-pi/pi-wire": "18.
|
|
43
|
-
"@oh-my-pi/snapcompact": "18.
|
|
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",
|
|
44
47
|
"@opentelemetry/api": "^1.9.1"
|
|
45
48
|
},
|
|
46
49
|
"devDependencies": {
|
|
47
|
-
"@oh-my-pi/omptype": "18.
|
|
50
|
+
"@oh-my-pi/omptype": "18.3.0",
|
|
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"
|
package/src/agent-loop.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import {
|
|
6
6
|
type AssistantMessage,
|
|
7
7
|
type AssistantMessageEvent,
|
|
8
|
+
type ApiKeyResolution,
|
|
8
9
|
type ComputerAction,
|
|
9
10
|
type ComputerSafetyCheck,
|
|
10
11
|
type Context,
|
|
@@ -164,6 +165,13 @@ export function createToolScopedAbortReason(
|
|
|
164
165
|
*/
|
|
165
166
|
export const TERMINAL_TOOL_RESULT_ABORT_REASON = Symbol.for("pi-agent-core.terminal-tool-result");
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Abort reason carried by an interruptible tool's signal when queued steering,
|
|
170
|
+
* a peer IRC, or a background completion cut it short. Lets a wait tell the
|
|
171
|
+
* designed wake path apart from an external/user abort of the run.
|
|
172
|
+
*/
|
|
173
|
+
export const TOOL_INTERRUPT_ABORT_REASON = Symbol.for("pi-agent-core.tool-interrupt");
|
|
174
|
+
|
|
167
175
|
const STEERING_INTERRUPT_POLL_MS = 250;
|
|
168
176
|
|
|
169
177
|
class HarmonyLeakInterruption extends Error {
|
|
@@ -1758,6 +1766,12 @@ async function prepareProviderCall(
|
|
|
1758
1766
|
tools: undefined,
|
|
1759
1767
|
};
|
|
1760
1768
|
}
|
|
1769
|
+
// After `transformProviderContext`, so the recorded definitions are exactly what the provider receives.
|
|
1770
|
+
if (config.sentToolDefinitions && llmContext.tools) {
|
|
1771
|
+
config.sentToolDefinitions.record(llmContext.tools);
|
|
1772
|
+
const inactiveTools = config.sentToolDefinitions.inactiveFor(llmContext.messages, llmContext.tools);
|
|
1773
|
+
if (inactiveTools) llmContext = { ...llmContext, inactiveTools };
|
|
1774
|
+
}
|
|
1761
1775
|
return { model, context: llmContext, promptToolWireTools, ownedDialect };
|
|
1762
1776
|
}
|
|
1763
1777
|
|
|
@@ -1814,8 +1828,13 @@ async function streamAssistantResponse(
|
|
|
1814
1828
|
? providerAbortSignals[0]!
|
|
1815
1829
|
: AbortSignal.any(providerAbortSignals);
|
|
1816
1830
|
const requestApiKey = (config.getApiKey ? await config.getApiKey(model) : undefined) ?? config.apiKey;
|
|
1817
|
-
|
|
1818
|
-
const
|
|
1831
|
+
let resolvedCredential: ApiKeyResolution;
|
|
1832
|
+
const resolvedApiKey = await resolveApiKeyOnce(requestApiKey, finalRequestSignal, resolved => {
|
|
1833
|
+
resolvedCredential = resolved;
|
|
1834
|
+
});
|
|
1835
|
+
const apiKey = isApiKeyResolver(requestApiKey)
|
|
1836
|
+
? seedApiKeyResolver(resolvedCredential ?? resolvedApiKey, requestApiKey)
|
|
1837
|
+
: requestApiKey;
|
|
1819
1838
|
|
|
1820
1839
|
// Re-resolve metadata after credential selection so the per-request value
|
|
1821
1840
|
// reflects the credential actually used, not the snapshot from AgentLoopConfig construction.
|
|
@@ -2866,7 +2885,10 @@ async function executeToolCalls(
|
|
|
2866
2885
|
const emittedToolResults: ToolResultMessage[] = [];
|
|
2867
2886
|
const toolCallInfos = toolCalls.map(call => ({ id: call.id, name: call.name }));
|
|
2868
2887
|
const batchId = `${assistantMessage.timestamp ?? Date.now()}_${toolCalls[0]?.id ?? "batch"}`;
|
|
2869
|
-
|
|
2888
|
+
// `interruptMode: "wait"` only spares side-effecting work: interruptible
|
|
2889
|
+
// waits are always cut short, since a pure wait has nothing to finish and
|
|
2890
|
+
// would otherwise sit out its full window with a message already queued.
|
|
2891
|
+
const softInterrupts = interruptMode !== "wait";
|
|
2870
2892
|
const steeringAbortController = new AbortController();
|
|
2871
2893
|
const ircAbortController = new AbortController();
|
|
2872
2894
|
// Cooperative channel: aborted when queued steering (or an interrupting
|
|
@@ -2875,7 +2897,7 @@ async function executeToolCalls(
|
|
|
2875
2897
|
// backgrounds itself so the message injects promptly — but it never kills
|
|
2876
2898
|
// anything; ignoring it is always safe.
|
|
2877
2899
|
const steeringSoftController = new AbortController();
|
|
2878
|
-
// Interruptible tools (pure waits:
|
|
2900
|
+
// Interruptible tools (pure waits: wait, vibe) observe steering +
|
|
2879
2901
|
// external + IRC aborts. Every other tool sees ONLY the external signal:
|
|
2880
2902
|
// neither queued steering nor a peer IRC ever hard-kills a partially
|
|
2881
2903
|
// side-effecting foreground tool (e.g. `bash`) — those get the cooperative
|
|
@@ -2939,9 +2961,10 @@ async function executeToolCalls(
|
|
|
2939
2961
|
// Asides only fire once: an interrupt already recorded on interruptState
|
|
2940
2962
|
// must not re-abort, and (unlike steering) never re-consumes a queue.
|
|
2941
2963
|
// A completion-triggered record is the exception — it leaves the
|
|
2942
|
-
// cooperative signal down, so keep polling until a peer IRC escalates
|
|
2943
|
-
|
|
2944
|
-
if (
|
|
2964
|
+
// cooperative signal down, so keep polling until a peer IRC escalates
|
|
2965
|
+
// (only when soft interrupts are enabled; otherwise nothing is left).
|
|
2966
|
+
if (signal?.aborted) return;
|
|
2967
|
+
if (interruptState.triggered && (!softInterrupts || steeringSoftController.signal.aborted)) return;
|
|
2945
2968
|
// Peer IRC and background completions (finished jobs, exited supervised
|
|
2946
2969
|
// processes) hard-abort interruptible waits only; foreground tools keep
|
|
2947
2970
|
// running (no partial side effects).
|
|
@@ -2953,7 +2976,7 @@ async function executeToolCalls(
|
|
|
2953
2976
|
if (!interruptState.triggered) {
|
|
2954
2977
|
interruptState.triggered = true;
|
|
2955
2978
|
interruptState.source = source;
|
|
2956
|
-
ircAbortController.abort();
|
|
2979
|
+
ircAbortController.abort(TOOL_INTERRUPT_ABORT_REASON);
|
|
2957
2980
|
}
|
|
2958
2981
|
// Only an urgent aside raises the cooperative signal that makes
|
|
2959
2982
|
// backgroundable foreground work (auto-background bash/eval) detach
|
|
@@ -2963,16 +2986,14 @@ async function executeToolCalls(
|
|
|
2963
2986
|
// also cascades: the freshly detached job's own completion re-triggers
|
|
2964
2987
|
// this check for the next command, so millisecond-long commands chain
|
|
2965
2988
|
// into separate background deliveries (#12869).
|
|
2966
|
-
if (source !== "background") steeringSoftController.abort();
|
|
2989
|
+
if (source !== "background" && softInterrupts) steeringSoftController.abort();
|
|
2967
2990
|
};
|
|
2968
2991
|
|
|
2969
2992
|
const checkSteering = async (): Promise<void> => {
|
|
2970
2993
|
// `signal` (external/user abort) is checked separately from the internal
|
|
2971
2994
|
// abort controllers: once the run is externally aborted it is unwinding
|
|
2972
2995
|
// and the interrupt would be redundant.
|
|
2973
|
-
if (
|
|
2974
|
-
return;
|
|
2975
|
-
}
|
|
2996
|
+
if (signal?.aborted) return;
|
|
2976
2997
|
// Mid-batch steering detection must be non-consuming. If a direct
|
|
2977
2998
|
// integration only provides getSteeringMessages(), the queue drains at the
|
|
2978
2999
|
// injection boundary below; polling it here would strand or drop messages.
|
|
@@ -2990,8 +3011,9 @@ async function executeToolCalls(
|
|
|
2990
3011
|
}
|
|
2991
3012
|
}
|
|
2992
3013
|
if (steeringQueued) {
|
|
2993
|
-
// Queued steering hard-aborts only interruptible waits and
|
|
2994
|
-
//
|
|
3014
|
+
// Queued steering hard-aborts only interruptible waits and (unless
|
|
3015
|
+
// interruptMode is "wait") raises the cooperative soft signal for
|
|
3016
|
+
// everything else: the boundary dequeue
|
|
2995
3017
|
// below injects the message as soon as running tools finish (or
|
|
2996
3018
|
// background themselves), and not-yet-started interruptible waits
|
|
2997
3019
|
// are skipped. Idempotent — a second steer poll after the abort is
|
|
@@ -2999,8 +3021,8 @@ async function executeToolCalls(
|
|
|
2999
3021
|
if (!steeringAbortController.signal.aborted) {
|
|
3000
3022
|
interruptState.triggered = true;
|
|
3001
3023
|
interruptState.source = steeringSource ?? "unknown";
|
|
3002
|
-
steeringAbortController.abort();
|
|
3003
|
-
steeringSoftController.abort();
|
|
3024
|
+
steeringAbortController.abort(TOOL_INTERRUPT_ABORT_REASON);
|
|
3025
|
+
if (softInterrupts) steeringSoftController.abort();
|
|
3004
3026
|
}
|
|
3005
3027
|
return;
|
|
3006
3028
|
}
|
|
@@ -3050,7 +3072,7 @@ async function executeToolCalls(
|
|
|
3050
3072
|
|
|
3051
3073
|
const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
|
|
3052
3074
|
// A pending interrupt preempts not-yet-started *interruptible* waits so
|
|
3053
|
-
// the message injects promptly instead of sitting out a `
|
|
3075
|
+
// the message injects promptly instead of sitting out a `wait`.
|
|
3054
3076
|
// Non-interruptible work is never skipped, whatever the source: the
|
|
3055
3077
|
// expensive part — generating the call — is already paid, the tool
|
|
3056
3078
|
// itself is cheap, and a skip only makes the model re-emit the same
|
|
@@ -3312,10 +3334,12 @@ async function executeToolCalls(
|
|
|
3312
3334
|
// detection hard-aborts interruptible waits (running or not yet started),
|
|
3313
3335
|
// and steering/IRC additionally soft-signal cooperative tools
|
|
3314
3336
|
// (auto-background bash), so the boundary dequeue below injects the message
|
|
3315
|
-
// promptly.
|
|
3316
|
-
// (no-op once triggered).
|
|
3337
|
+
// promptly. In "wait" mode only a batch holding an interruptible wait needs
|
|
3338
|
+
// the watch; checkSteering is idempotent (no-op once triggered).
|
|
3317
3339
|
const hasAsidePeek = hasIrcInterrupts !== undefined || hasBackgroundCompletions !== undefined;
|
|
3318
|
-
const watchSteeringWhileRunning =
|
|
3340
|
+
const watchSteeringWhileRunning =
|
|
3341
|
+
(softInterrupts || records.some(record => record.interruptible)) &&
|
|
3342
|
+
(hasSteeringMessages !== undefined || hasAsidePeek);
|
|
3319
3343
|
const eventDrivenSteeringWatch =
|
|
3320
3344
|
watchSteeringWhileRunning && config.waitForSteeringMessages !== undefined && hasSteeringMessages !== undefined;
|
|
3321
3345
|
const steeringWatchAbortController = new AbortController();
|
|
@@ -3349,11 +3373,17 @@ async function executeToolCalls(
|
|
|
3349
3373
|
() => false,
|
|
3350
3374
|
);
|
|
3351
3375
|
if (!(await Promise.race([steeringChecked, watchAbortedFalse]))) return;
|
|
3352
|
-
// Stop once
|
|
3353
|
-
//
|
|
3354
|
-
// leaves
|
|
3355
|
-
// afterwards must still
|
|
3356
|
-
|
|
3376
|
+
// Stop once nothing is left to escalate: the cooperative signal
|
|
3377
|
+
// is up, or (without soft interrupts) the waits are cut. A
|
|
3378
|
+
// completion-only trigger leaves the soft signal down, so keep
|
|
3379
|
+
// watching: a genuine steer arriving afterwards must still
|
|
3380
|
+
// reach foreground tools.
|
|
3381
|
+
if (
|
|
3382
|
+
steeringWatchSignal.aborted ||
|
|
3383
|
+
steeringSoftController.signal.aborted ||
|
|
3384
|
+
(!softInterrupts && interruptState.triggered)
|
|
3385
|
+
)
|
|
3386
|
+
return;
|
|
3357
3387
|
if (!(await Promise.race([steeringQueued, watchAbortedFalse]))) return;
|
|
3358
3388
|
}
|
|
3359
3389
|
})()
|
package/src/agent.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "./agent-loop";
|
|
39
39
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
40
40
|
import { isProviderRefusalMessage } from "./replay-policy";
|
|
41
|
+
import { SentToolDefinitions } from "./sent-tool-definitions";
|
|
41
42
|
import { Tokenizer, tokenizerEncodingForModel } from "./tokenizer";
|
|
42
43
|
import type {
|
|
43
44
|
AgentBeforeModelCall,
|
|
@@ -389,6 +390,7 @@ export class Agent {
|
|
|
389
390
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
390
391
|
#transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
391
392
|
#transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
393
|
+
#sentToolDefinitions = new SentToolDefinitions();
|
|
392
394
|
#steeringQueue: AgentMessage[] = [];
|
|
393
395
|
#followUpQueue: AgentMessage[] = [];
|
|
394
396
|
#queuedMessageClaims: Partial<Record<QueuedMessageQueue, QueuedMessageClaim>> = {};
|
|
@@ -837,6 +839,11 @@ export class Agent {
|
|
|
837
839
|
}) ?? []);
|
|
838
840
|
let context: Context = { systemPrompt, messages, tools };
|
|
839
841
|
if (this.#transformProviderContext) context = await this.#transformProviderContext(context, model);
|
|
842
|
+
// Side requests reuse the main loop's sent definitions without recording their own.
|
|
843
|
+
if (context.tools?.length) {
|
|
844
|
+
const inactiveTools = this.#sentToolDefinitions.inactiveFor(context.messages, context.tools);
|
|
845
|
+
if (inactiveTools) context = { ...context, inactiveTools };
|
|
846
|
+
}
|
|
840
847
|
return context;
|
|
841
848
|
}
|
|
842
849
|
|
|
@@ -1579,6 +1586,7 @@ export class Agent {
|
|
|
1579
1586
|
preferWebsockets: this.#preferWebsockets,
|
|
1580
1587
|
convertToLlm: this.#convertToLlm,
|
|
1581
1588
|
transformProviderContext: this.#transformProviderContext,
|
|
1589
|
+
sentToolDefinitions: this.#sentToolDefinitions,
|
|
1582
1590
|
transformContext: this.#transformContext,
|
|
1583
1591
|
onPayload: this.#onPayload,
|
|
1584
1592
|
onResponse: this.#onResponse,
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Anthropic
|
|
2
|
+
* Anthropic on-demand compaction (`compact-2026-09-04` beta).
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* prompt, tools
|
|
6
|
-
*
|
|
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
|
|
|
15
9
|
import type {
|
|
16
10
|
AnthropicCompactionPayload,
|
|
17
11
|
ApiKey,
|
|
18
|
-
AssistantMessage,
|
|
19
12
|
Effort,
|
|
20
13
|
Message,
|
|
21
14
|
Model,
|
|
@@ -27,26 +20,18 @@ import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
|
27
20
|
import { supportsAnthropicCompaction } from "@oh-my-pi/pi-ai/providers/anthropic-compaction";
|
|
28
21
|
import { isRecord, prompt } from "@oh-my-pi/pi-utils";
|
|
29
22
|
import { type InstrumentedChatSpanOptions, instrumentedCompleteSimple } from "../telemetry";
|
|
23
|
+
import type { AgentMessage } from "../types";
|
|
30
24
|
import anthropicCompactionInstructionsPrompt from "./prompts/anthropic-compaction-instructions.md" with { type: "text" };
|
|
31
25
|
|
|
32
26
|
export const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
|
|
33
27
|
|
|
34
|
-
/** The API rejects a `compact_20260112` trigger below this many input tokens. */
|
|
35
|
-
export const ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS = 50_000;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Smallest context the native lane accepts. The trigger sits at the API
|
|
39
|
-
* floor, so a prompt that lands below it is answered instead of compacted;
|
|
40
|
-
* the margin over the floor absorbs the difference between the last reported
|
|
41
|
-
* context size and the compaction request's own input.
|
|
42
|
-
*/
|
|
43
|
-
export const ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS = 55_000;
|
|
44
|
-
|
|
45
28
|
/** Summary persisted under {@link ANTHROPIC_COMPACTION_PRESERVE_KEY}. */
|
|
46
29
|
export interface AnthropicCompactionPreserveData {
|
|
47
30
|
provider: string;
|
|
48
31
|
content: string;
|
|
49
|
-
/**
|
|
32
|
+
/** Signature attached to an on-demand block; replayed verbatim. */
|
|
33
|
+
signature?: string;
|
|
34
|
+
/** Legacy threshold block state; replay-only. */
|
|
50
35
|
encryptedContent?: string;
|
|
51
36
|
/** Harness file metadata (`<files>` section) replayed after the native block. */
|
|
52
37
|
filesText?: string;
|
|
@@ -82,6 +67,9 @@ export function getPreservedAnthropicCompactionData(
|
|
|
82
67
|
return {
|
|
83
68
|
provider: candidate.provider,
|
|
84
69
|
content: candidate.content,
|
|
70
|
+
...(typeof candidate.signature === "string" && candidate.signature.length > 0
|
|
71
|
+
? { signature: candidate.signature }
|
|
72
|
+
: {}),
|
|
85
73
|
...(typeof candidate.encryptedContent === "string" && candidate.encryptedContent.length > 0
|
|
86
74
|
? { encryptedContent: candidate.encryptedContent }
|
|
87
75
|
: {}),
|
|
@@ -118,93 +106,63 @@ export function getAnthropicCompactionPayload(
|
|
|
118
106
|
type: "anthropicCompaction",
|
|
119
107
|
provider: preserved.provider,
|
|
120
108
|
content: preserved.content,
|
|
109
|
+
...(preserved.signature ? { signature: preserved.signature } : {}),
|
|
121
110
|
...(preserved.encryptedContent ? { encryptedContent: preserved.encryptedContent } : {}),
|
|
122
111
|
...(preserved.filesText ? { filesText: preserved.filesText } : {}),
|
|
123
112
|
};
|
|
124
113
|
}
|
|
125
114
|
|
|
126
115
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* the whole conversation so the prompt cache the live turn wrote is read, but
|
|
131
|
-
* the summary must cover only the history before that tail — the local
|
|
132
|
-
* summarizer never sees the tail, and the rebuilt context replays it after the
|
|
133
|
-
* summary. Counting mirrors the provider's message conversion (consecutive
|
|
134
|
-
* tool results collapse into one user message; developer messages are user
|
|
135
|
-
* messages). Structured for the prompt template, which renders the
|
|
136
|
-
* singular/plural wording; the description quotes no content: quoting the
|
|
137
|
-
* tail would hand the summarizer the very facts it must leave to the tail.
|
|
116
|
+
* Move the existing keep-tail boundary forward until the summary/tail boundary
|
|
117
|
+
* alternates wire roles and no tool call is separated from its result. If no
|
|
118
|
+
* boundary is safe, the request summarizes the entire snapshot (empty tail).
|
|
138
119
|
*/
|
|
139
|
-
export
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const isToolResult = message.role === "toolResult";
|
|
151
|
-
if (!(isToolResult && previousWasToolResult)) count += 1;
|
|
152
|
-
previousWasToolResult = isToolResult;
|
|
120
|
+
export function findAnthropicCompactionCut(
|
|
121
|
+
messages: readonly (AgentMessage | { role: "system"; content: string; timestamp: number })[],
|
|
122
|
+
initialCut: number,
|
|
123
|
+
): number {
|
|
124
|
+
let calls: Map<string, number> | undefined;
|
|
125
|
+
for (let i = 0; i < messages.length; i++) {
|
|
126
|
+
const message = messages[i];
|
|
127
|
+
if (message.role !== "assistant") continue;
|
|
128
|
+
for (const block of message.content) {
|
|
129
|
+
if (block.type === "toolCall") (calls ??= new Map()).set(block.id, i);
|
|
130
|
+
}
|
|
153
131
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
count += 1;
|
|
132
|
+
let lastResult: Int32Array | undefined;
|
|
133
|
+
if (calls) {
|
|
134
|
+
lastResult = new Int32Array(messages.length);
|
|
135
|
+
for (let i = 0; i < messages.length; i++) {
|
|
136
|
+
const message = messages[i];
|
|
137
|
+
if (message.role !== "toolResult") continue;
|
|
138
|
+
const callIndex = calls.get(message.toolCallId);
|
|
139
|
+
if (callIndex !== undefined) lastResult[callIndex] = i;
|
|
140
|
+
}
|
|
164
141
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
switch (block.type) {
|
|
176
|
-
case "text":
|
|
177
|
-
return block.text.trim().length > 0;
|
|
178
|
-
case "toolCall":
|
|
179
|
-
case "anthropicServerTool":
|
|
180
|
-
return true;
|
|
181
|
-
case "thinking":
|
|
182
|
-
return block.thinking.trim().length > 0 || (block.thinkingSignature ?? "").trim().length > 0;
|
|
183
|
-
default:
|
|
184
|
-
return false;
|
|
142
|
+
let protectedThrough = -1;
|
|
143
|
+
for (let i = 0; i < initialCut; i++) protectedThrough = Math.max(protectedThrough, lastResult?.[i] ?? -1);
|
|
144
|
+
for (let cut = initialCut; cut < messages.length; cut++) {
|
|
145
|
+
protectedThrough = Math.max(protectedThrough, lastResult?.[cut - 1] ?? -1);
|
|
146
|
+
if (cut <= protectedThrough) continue;
|
|
147
|
+
const first = messages[cut];
|
|
148
|
+
if (first.role === "system" || first.role === "developer") continue;
|
|
149
|
+
const previous = messages[cut - 1];
|
|
150
|
+
if (!previous) continue;
|
|
151
|
+
if ((previous.role === "assistant") !== (first.role === "assistant")) return cut;
|
|
185
152
|
}
|
|
153
|
+
return messages.length;
|
|
186
154
|
}
|
|
187
155
|
|
|
188
|
-
/**
|
|
189
|
-
* Summarization prompt sent as the edit's `instructions`, which replace the
|
|
190
|
-
* API default entirely. The template lays out the retained-tail boundary
|
|
191
|
-
* first, so the summary covers only the history the rebuilt context drops,
|
|
192
|
-
* then the caller's extra context, the same structure prompt as the local
|
|
193
|
-
* summarizer, the caller's focus, and the tool-abstention clause the API
|
|
194
|
-
* recommends when tools are defined (a summarization pass that calls a tool
|
|
195
|
-
* yields no summary).
|
|
196
|
-
*/
|
|
156
|
+
/** Instructions replace the API default; the request contains only summarized messages. */
|
|
197
157
|
export function buildAnthropicCompactionInstructions(
|
|
198
158
|
basePrompt: string,
|
|
199
159
|
customInstructions: string | undefined,
|
|
200
160
|
extraContext: string | undefined,
|
|
201
|
-
retainedTail: RetainedTailScope | undefined,
|
|
202
161
|
): string {
|
|
203
162
|
return prompt.render(anthropicCompactionInstructionsPrompt, {
|
|
204
163
|
basePrompt,
|
|
205
164
|
customInstructions,
|
|
206
165
|
extraContext,
|
|
207
|
-
retainedTail,
|
|
208
166
|
});
|
|
209
167
|
}
|
|
210
168
|
|
|
@@ -219,7 +177,7 @@ export interface AnthropicNativeCompactionRequest {
|
|
|
219
177
|
|
|
220
178
|
export interface AnthropicNativeCompactionResponse {
|
|
221
179
|
content: string;
|
|
222
|
-
|
|
180
|
+
signature: string;
|
|
223
181
|
usage: Usage;
|
|
224
182
|
model: string;
|
|
225
183
|
}
|
|
@@ -239,15 +197,13 @@ export interface AnthropicNativeCompactionOptions
|
|
|
239
197
|
Pick<InstrumentedChatSpanOptions, "completeImpl" | "telemetry" | "retry"> {}
|
|
240
198
|
|
|
241
199
|
/**
|
|
242
|
-
* Run one compaction request and return the summary the API wrote
|
|
243
|
-
* opaque `encrypted_content` the API attached for the replay. `completeSimple`
|
|
200
|
+
* Run one compaction request and return the summary and signature the API wrote. `completeSimple`
|
|
244
201
|
* resolves terminal failures as messages, so their classification is restored
|
|
245
202
|
* here: an aborted response is an `AbortError` (a cancellation, never a native
|
|
246
203
|
* failure) and an error response keeps its HTTP status, so auth and timeout
|
|
247
204
|
* handling downstream classify it the same way as the OpenAI lanes. A response
|
|
248
|
-
* without a summary is a native failure
|
|
249
|
-
*
|
|
250
|
-
* the model called a tool during summarization.
|
|
205
|
+
* without a summary is a native failure, including tool use, refusals and
|
|
206
|
+
* output limits; the configured method order can then choose a fallback.
|
|
251
207
|
*/
|
|
252
208
|
export async function requestAnthropicNativeCompaction(
|
|
253
209
|
model: Model<"anthropic-messages">,
|
|
@@ -271,11 +227,7 @@ export async function requestAnthropicNativeCompaction(
|
|
|
271
227
|
promptCacheKey: options.promptCacheKey,
|
|
272
228
|
providerSessionState: options.providerSessionState,
|
|
273
229
|
maxInFlightRequests: options.maxInFlightRequests,
|
|
274
|
-
anthropicCompaction: {
|
|
275
|
-
triggerInputTokens: ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS,
|
|
276
|
-
pauseAfterCompaction: true,
|
|
277
|
-
instructions: request.instructions,
|
|
278
|
-
},
|
|
230
|
+
anthropicCompaction: { instructions: request.instructions },
|
|
279
231
|
},
|
|
280
232
|
{
|
|
281
233
|
telemetry: options.telemetry,
|
|
@@ -294,16 +246,21 @@ export async function requestAnthropicNativeCompaction(
|
|
|
294
246
|
: new AIError.ProviderHttpError(message, response.errorStatus);
|
|
295
247
|
}
|
|
296
248
|
const payload = response.providerPayload;
|
|
297
|
-
if (
|
|
249
|
+
if (
|
|
250
|
+
response.stopDetails?.type !== "compaction" ||
|
|
251
|
+
payload?.type !== "anthropicCompaction" ||
|
|
252
|
+
!payload.content ||
|
|
253
|
+
!payload.signature
|
|
254
|
+
) {
|
|
298
255
|
throw new Error(
|
|
299
256
|
response.stopDetails?.type === "compaction"
|
|
300
|
-
? "Anthropic compaction returned no summary"
|
|
257
|
+
? "Anthropic compaction returned no signed summary"
|
|
301
258
|
: "Anthropic compaction response carried no compaction block",
|
|
302
259
|
);
|
|
303
260
|
}
|
|
304
261
|
return {
|
|
305
262
|
content: payload.content,
|
|
306
|
-
|
|
263
|
+
signature: payload.signature,
|
|
307
264
|
usage: response.usage,
|
|
308
265
|
model: response.model,
|
|
309
266
|
};
|
|
@@ -43,9 +43,8 @@ import { ThinkingLevel } from "../thinking";
|
|
|
43
43
|
import { Tokenizer } from "../tokenizer";
|
|
44
44
|
import type { AgentMessage } from "../types";
|
|
45
45
|
import {
|
|
46
|
-
ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS,
|
|
47
46
|
buildAnthropicCompactionInstructions,
|
|
48
|
-
|
|
47
|
+
findAnthropicCompactionCut,
|
|
49
48
|
getPreservedAnthropicCompactionData,
|
|
50
49
|
requestAnthropicNativeCompaction,
|
|
51
50
|
shouldUseAnthropicNativeCompaction,
|
|
@@ -1252,6 +1251,8 @@ export interface CompactionPreparation {
|
|
|
1252
1251
|
turnPrefixMessages: AgentMessage[];
|
|
1253
1252
|
/** Messages kept in full after compaction (recent history) */
|
|
1254
1253
|
recentMessages: AgentMessage[];
|
|
1254
|
+
/** Entry IDs parallel to recentMessages, for an Anthropic-safe keep-tail boundary. */
|
|
1255
|
+
recentEntryIds?: string[];
|
|
1255
1256
|
/** Whether this is a split turn (cut point in middle of turn) */
|
|
1256
1257
|
isSplitTurn: boolean;
|
|
1257
1258
|
tokensBefore: number;
|
|
@@ -1380,16 +1381,22 @@ export function prepareCompaction(
|
|
|
1380
1381
|
}
|
|
1381
1382
|
}
|
|
1382
1383
|
} else if (previousCompaction) {
|
|
1383
|
-
// Local summaries exclude the retained tail, whose
|
|
1384
|
-
// the compaction record.
|
|
1385
|
-
// backwards: advisor snapshots put all retained messages after the summary
|
|
1386
|
-
// and may carry a keep ID from their previous, differently indexed snapshot.
|
|
1384
|
+
// Local and Anthropic summaries exclude the retained tail, whose
|
|
1385
|
+
// original entries precede the compaction record.
|
|
1387
1386
|
for (let i = resetBoundaryIndex + 1; i < prevCompactionIndex; i++) {
|
|
1388
1387
|
if (pathEntries[i].id === previousCompaction.firstKeptEntryId) {
|
|
1389
1388
|
boundaryStart = i;
|
|
1390
1389
|
break;
|
|
1391
1390
|
}
|
|
1392
1391
|
}
|
|
1392
|
+
if (previousCompaction.firstKeptEntryId === "" && previousCompaction.providerReplayThroughEntryId) {
|
|
1393
|
+
// An empty snapshot tail can still have turns appended during
|
|
1394
|
+
// background compaction; those start after the summarized snapshot.
|
|
1395
|
+
const snapshotIdx = pathEntries.findIndex(
|
|
1396
|
+
entry => entry.id === previousCompaction.providerReplayThroughEntryId,
|
|
1397
|
+
);
|
|
1398
|
+
if (snapshotIdx >= 0 && snapshotIdx < prevCompactionIndex) boundaryStart = snapshotIdx + 1;
|
|
1399
|
+
}
|
|
1393
1400
|
}
|
|
1394
1401
|
|
|
1395
1402
|
// Keep original IDs beside the converted messages so estimation, cutting,
|
|
@@ -1437,6 +1444,11 @@ export function prepareCompaction(
|
|
|
1437
1444
|
return undefined;
|
|
1438
1445
|
}
|
|
1439
1446
|
|
|
1447
|
+
const recentEntryIds: string[] = [];
|
|
1448
|
+
for (let i = cutPoint.firstKeptEntryIndex; i < compactionEntries.length; i++) {
|
|
1449
|
+
recentEntryIds.push(compactionEntries[i].id);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1440
1452
|
// Extract file operations from messages and previous compaction
|
|
1441
1453
|
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
|
1442
1454
|
|
|
@@ -1452,6 +1464,7 @@ export function prepareCompaction(
|
|
|
1452
1464
|
messagesToSummarize,
|
|
1453
1465
|
turnPrefixMessages,
|
|
1454
1466
|
recentMessages,
|
|
1467
|
+
recentEntryIds,
|
|
1455
1468
|
isSplitTurn: cutPoint.isSplitTurn,
|
|
1456
1469
|
tokensBefore,
|
|
1457
1470
|
previousSummary: previousCompaction?.summary,
|
|
@@ -1579,6 +1592,7 @@ export async function compact(
|
|
|
1579
1592
|
messagesToSummarize,
|
|
1580
1593
|
turnPrefixMessages,
|
|
1581
1594
|
recentMessages,
|
|
1595
|
+
recentEntryIds,
|
|
1582
1596
|
isSplitTurn,
|
|
1583
1597
|
tokensBefore,
|
|
1584
1598
|
previousSummary,
|
|
@@ -1826,35 +1840,19 @@ export async function compact(
|
|
|
1826
1840
|
}
|
|
1827
1841
|
}
|
|
1828
1842
|
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
// real text, persisted both as the entry summary and as the native replay
|
|
1832
|
-
// payload. A context below the API's trigger floor cannot compact remotely
|
|
1833
|
-
// and takes the local summarizer instead — an eligibility boundary, not a
|
|
1834
|
-
// failure.
|
|
1843
|
+
// On-demand compaction summarizes only the prefix; the tail is never sent
|
|
1844
|
+
// to this request and is replayed after the returned signed block.
|
|
1835
1845
|
let nativeSummary: string | undefined;
|
|
1836
|
-
let
|
|
1846
|
+
let nativeSignature: string | undefined;
|
|
1847
|
+
let nativeFirstKeptEntryId = firstKeptEntryId;
|
|
1837
1848
|
let nativeUsedTokens: number | undefined;
|
|
1838
|
-
if (
|
|
1839
|
-
!usedRemoteCompaction &&
|
|
1840
|
-
settings.remoteEnabled !== false &&
|
|
1841
|
-
shouldUseAnthropicNativeCompaction(model) &&
|
|
1842
|
-
tokensBefore >= ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS
|
|
1843
|
-
) {
|
|
1849
|
+
if (!usedRemoteCompaction && settings.remoteEnabled !== false && shouldUseAnthropicNativeCompaction(model)) {
|
|
1844
1850
|
const previousNative = getPreservedAnthropicCompactionData(previousPreserveData);
|
|
1845
|
-
// Lead with the previous summary
|
|
1846
|
-
// natively when this provider wrote it, as text otherwise.
|
|
1847
|
-
//
|
|
1848
|
-
//
|
|
1849
|
-
//
|
|
1850
|
-
// The rewrite marker must precede every message this request replays —
|
|
1851
|
-
// summarized history and retained tail alike — exactly like the live
|
|
1852
|
-
// context rebuild predates its tail. A previous compaction's commit
|
|
1853
|
-
// timestamp is newer than re-retained or re-summarized turns, so
|
|
1854
|
-
// reusing it would strip their bound thinking only in this request,
|
|
1855
|
-
// diverging from the cached live prefix (and possibly dropping below
|
|
1856
|
-
// the trigger). Manually built preparations with no input at all fall
|
|
1857
|
-
// back to the current time.
|
|
1851
|
+
// Lead with the previous summary as the live context renders it:
|
|
1852
|
+
// natively when this provider wrote it, as text otherwise.
|
|
1853
|
+
// A prior snapcompact archive is already merged into that summary
|
|
1854
|
+
// text. Predate the rewrite marker before all replayed messages so
|
|
1855
|
+
// retained thinking stays bound to the original prefix.
|
|
1858
1856
|
const firstReplayed = messagesToSummarize[0] ?? turnPrefixMessages[0] ?? recentMessages[0];
|
|
1859
1857
|
const previousSummaryAt =
|
|
1860
1858
|
firstReplayed !== undefined ? new Date(firstReplayed.timestamp - 1).toISOString() : new Date().toISOString();
|
|
@@ -1866,6 +1864,7 @@ export async function compact(
|
|
|
1866
1864
|
type: "anthropicCompaction",
|
|
1867
1865
|
provider: previousNative.provider,
|
|
1868
1866
|
content: previousNative.content,
|
|
1867
|
+
...(previousNative.signature ? { signature: previousNative.signature } : {}),
|
|
1869
1868
|
...(previousNative.encryptedContent
|
|
1870
1869
|
? { encryptedContent: previousNative.encryptedContent }
|
|
1871
1870
|
: {}),
|
|
@@ -1874,29 +1873,40 @@ export async function compact(
|
|
|
1874
1873
|
: undefined,
|
|
1875
1874
|
})
|
|
1876
1875
|
: undefined;
|
|
1877
|
-
const
|
|
1878
|
-
const
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1876
|
+
const allMessages = [...messagesToSummarize, ...turnPrefixMessages, ...recentMessages];
|
|
1877
|
+
const originalCut = messagesToSummarize.length + turnPrefixMessages.length;
|
|
1878
|
+
const nativeCut = findAnthropicCompactionCut(allMessages, originalCut);
|
|
1879
|
+
// Hand-built preparations lacking entry IDs cannot move their persisted
|
|
1880
|
+
// boundary into the tail. Summarizing all is still safe.
|
|
1881
|
+
const safeCut =
|
|
1882
|
+
nativeCut > originalCut && nativeCut < allMessages.length && !recentEntryIds?.[nativeCut - originalCut]
|
|
1883
|
+
? allMessages.length
|
|
1884
|
+
: nativeCut;
|
|
1885
|
+
nativeFirstKeptEntryId =
|
|
1886
|
+
safeCut === allMessages.length
|
|
1887
|
+
? ""
|
|
1888
|
+
: safeCut === originalCut
|
|
1889
|
+
? firstKeptEntryId
|
|
1890
|
+
: (recentEntryIds?.[safeCut - originalCut] ?? "");
|
|
1891
|
+
for (let i = originalCut; i < safeCut; i++) extractFileOpsFromMessage(allMessages[i], fileOps);
|
|
1892
|
+
const messages = (summaryOptions.convertToLlm ?? defaultConvertToLlm)([
|
|
1893
|
+
...(previousSummaryMessage ? [previousSummaryMessage] : []),
|
|
1894
|
+
...allMessages.slice(0, safeCut),
|
|
1895
|
+
]);
|
|
1887
1896
|
try {
|
|
1888
1897
|
const remote = await requestAnthropicNativeCompaction(
|
|
1889
1898
|
model,
|
|
1890
1899
|
apiKey,
|
|
1891
1900
|
{
|
|
1892
|
-
|
|
1901
|
+
// The live prompt, not the local summarizer's synthetic system
|
|
1902
|
+
// prompt: kept thinking remains valid only under identical controls.
|
|
1903
|
+
systemPrompt: summaryOptions.remoteSystemPrompt ?? [],
|
|
1893
1904
|
messages,
|
|
1894
1905
|
tools: summaryOptions.tools,
|
|
1895
1906
|
instructions: buildAnthropicCompactionInstructions(
|
|
1896
1907
|
summaryOptions.promptOverride ?? SUMMARIZATION_PROMPT,
|
|
1897
1908
|
customInstructions,
|
|
1898
1909
|
formatAdditionalContext(summaryOptions.extraContext).trim() || undefined,
|
|
1899
|
-
describeRetainedTail(retainedTail),
|
|
1900
1910
|
),
|
|
1901
1911
|
maxTokens: Math.min(Math.floor(0.8 * reserveTokens), MAX_SUMMARY_TOKENS),
|
|
1902
1912
|
reasoning: resolveCompactionEffort(model, summaryOptions.thinkingLevel),
|
|
@@ -1915,7 +1925,7 @@ export async function compact(
|
|
|
1915
1925
|
},
|
|
1916
1926
|
);
|
|
1917
1927
|
nativeSummary = remote.content;
|
|
1918
|
-
|
|
1928
|
+
nativeSignature = remote.signature;
|
|
1919
1929
|
nativeUsedTokens = calculatePromptTokens(remote.usage);
|
|
1920
1930
|
usedRemoteCompaction = true;
|
|
1921
1931
|
} catch (err) {
|
|
@@ -2004,7 +2014,7 @@ export async function compact(
|
|
|
2004
2014
|
summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
|
|
2005
2015
|
if (nativeSummary !== undefined) {
|
|
2006
2016
|
// The replayed block stays byte-identical to the API's summary so it
|
|
2007
|
-
// matches
|
|
2017
|
+
// matches its signature. The harness file lists above travel
|
|
2008
2018
|
// separately: the converter replaces the summary message with the
|
|
2009
2019
|
// block and skips its text, so they would otherwise be invisible to
|
|
2010
2020
|
// this provider. Every other provider keeps reading the entry text.
|
|
@@ -2012,7 +2022,7 @@ export async function compact(
|
|
|
2012
2022
|
preserveData = withAnthropicCompactionPreserveData(preserveData, {
|
|
2013
2023
|
provider: model.provider,
|
|
2014
2024
|
content: nativeSummary,
|
|
2015
|
-
...(
|
|
2025
|
+
...(nativeSignature ? { signature: nativeSignature } : {}),
|
|
2016
2026
|
...(filesText ? { filesText } : {}),
|
|
2017
2027
|
model: model.id,
|
|
2018
2028
|
usedTokens: nativeUsedTokens,
|
|
@@ -2034,7 +2044,7 @@ export async function compact(
|
|
|
2034
2044
|
return {
|
|
2035
2045
|
summary,
|
|
2036
2046
|
shortSummary,
|
|
2037
|
-
firstKeptEntryId,
|
|
2047
|
+
firstKeptEntryId: nativeSummary !== undefined ? nativeFirstKeptEntryId : firstKeptEntryId,
|
|
2038
2048
|
tokensBefore,
|
|
2039
2049
|
details: { readFiles, modifiedFiles } as CompactionDetails,
|
|
2040
2050
|
preserveData: finalPreserveData,
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
SCOPE: The conversation's final {{#when retainedTail.count "==" 1}}{{retainedTail.role}} message stays{{else}}{{retainedTail.count}} messages, starting with a {{retainedTail.role}} message, stay{{/when}} in context verbatim after your summary. Summarize ONLY the history before those messages. You MUST NOT restate anything from those final messages — the reader sees them right after the summary — and you MUST treat them as the most recent state when describing progress and next steps.
|
|
3
|
-
{{else}}
|
|
4
|
-
SCOPE: The conversation above is the transcript to summarize. The API replaces everything before your summary with it, so nothing you leave out survives into the next context window.
|
|
5
|
-
{{/if}}
|
|
1
|
+
The conversation above is the complete history to summarize. The API replaces every message in this request with your summary; no message you leave out survives.
|
|
6
2
|
|
|
7
3
|
{{#if extraContext}}
|
|
8
4
|
{{extraContext}}
|
|
@@ -14,4 +10,4 @@ SCOPE: The conversation above is the transcript to summarize. The API replaces e
|
|
|
14
10
|
Additional focus: {{customInstructions}}
|
|
15
11
|
{{/if}}
|
|
16
12
|
|
|
17
|
-
You MUST NOT call any tools while writing the summary; respond with the summary text only.
|
|
13
|
+
You MUST NOT call any tools while writing the summary; respond with the summary text only.
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export * from "./proxy";
|
|
|
14
14
|
export * from "./replay-policy";
|
|
15
15
|
// Run-level telemetry collector + aggregators
|
|
16
16
|
export * from "./run-collector";
|
|
17
|
+
// Tool definitions remembered for Anthropic inactive-tool re-declaration
|
|
18
|
+
export * from "./sent-tool-definitions";
|
|
17
19
|
// Speculative execution coordinator
|
|
18
20
|
export * from "./speculative-execution";
|
|
19
21
|
// Telemetry
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Message, Tool } from "@oh-my-pi/pi-ai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Last wire definition this Agent sent for each tool name, so a provider that keeps
|
|
5
|
+
* withdrawn tools declared (Anthropic `tool_removal`) can re-declare them byte-identically.
|
|
6
|
+
* Used by prepareProviderCall and Agent.buildSideRequestContext.
|
|
7
|
+
*/
|
|
8
|
+
export class SentToolDefinitions {
|
|
9
|
+
#byName = new Map<string, Tool>();
|
|
10
|
+
|
|
11
|
+
/** Remember the definitions a request is about to send. */
|
|
12
|
+
record(tools: readonly Tool[]): void {
|
|
13
|
+
for (const tool of tools) this.#byName.set(tool.name, tool);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Definitions for names the latest `requestControls.tools.declared` in `messages` holds
|
|
18
|
+
* that are not in `active`; undefined when none. Names never sent by this Agent are
|
|
19
|
+
* skipped: the provider drops them from the declaration.
|
|
20
|
+
*/
|
|
21
|
+
inactiveFor(messages: readonly Message[], active: readonly Tool[]): Tool[] | undefined {
|
|
22
|
+
let declared: readonly string[] | undefined;
|
|
23
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
24
|
+
const message = messages[index];
|
|
25
|
+
if (message?.role === "assistant" && message.requestControls?.tools) {
|
|
26
|
+
declared = message.requestControls.tools.declared;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!declared) return undefined;
|
|
31
|
+
const activeNames = new Set(active.map(tool => tool.name));
|
|
32
|
+
const inactive: Tool[] = [];
|
|
33
|
+
for (const name of declared) {
|
|
34
|
+
if (activeNames.has(name)) continue;
|
|
35
|
+
const tool = this.#byName.get(name);
|
|
36
|
+
if (tool) inactive.push(tool);
|
|
37
|
+
}
|
|
38
|
+
return inactive.length > 0 ? inactive : undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
|
24
24
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
25
25
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
26
26
|
import type { AgentRunCoverage, AgentRunSummary } from "./run-collector";
|
|
27
|
+
import type { SentToolDefinitions } from "./sent-tool-definitions";
|
|
27
28
|
import type { AgentTelemetryConfig } from "./telemetry";
|
|
28
29
|
|
|
29
30
|
/** Stream function - can return sync or Promise for async config lookup */
|
|
@@ -163,8 +164,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
163
164
|
|
|
164
165
|
/**
|
|
165
166
|
* When to interrupt tool execution for steering messages.
|
|
166
|
-
* - "immediate" =
|
|
167
|
-
*
|
|
167
|
+
* - "immediate" = cut interruptible waits short and raise the cooperative
|
|
168
|
+
* `steeringSignal` for other running tools (default)
|
|
169
|
+
* - "wait" = let non-interruptible tools finish undisturbed; interruptible
|
|
170
|
+
* waits are still cut short, since they have no work to complete
|
|
168
171
|
*/
|
|
169
172
|
interruptMode?: "immediate" | "wait";
|
|
170
173
|
|
|
@@ -238,6 +241,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
238
241
|
*/
|
|
239
242
|
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
240
243
|
|
|
244
|
+
/** Remembers sent tool definitions to fill {@link Context.inactiveTools}. */
|
|
245
|
+
sentToolDefinitions?: SentToolDefinitions;
|
|
246
|
+
|
|
241
247
|
/**
|
|
242
248
|
* Resolves the API key or resolver for the current model before each LLM call.
|
|
243
249
|
*
|
|
@@ -259,8 +265,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
259
265
|
/**
|
|
260
266
|
* Peeks whether steering messages are queued, without consuming them.
|
|
261
267
|
*
|
|
262
|
-
* Polled while a tool batch runs (
|
|
263
|
-
* whether to abort in-flight and skip
|
|
268
|
+
* Polled while a tool batch runs (in "wait" mode, only when the batch holds an
|
|
269
|
+
* interruptible tool) to decide whether to abort in-flight and skip
|
|
270
|
+
* not-yet-started *interruptible* waits;
|
|
264
271
|
* every other already-emitted call still executes and the message injects
|
|
265
272
|
* at the batch boundary. The queue keeps
|
|
266
273
|
* owning its messages until the loop reaches the next injection boundary and
|
|
@@ -289,7 +296,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
289
296
|
* Peeks whether IRC messages should interrupt an interruptible waiting tool.
|
|
290
297
|
*
|
|
291
298
|
* Uses the same delivery rules as steering: the poll is non-consuming, only
|
|
292
|
-
* runs for interruptible tools, and
|
|
299
|
+
* runs for interruptible tools, and cuts them short even when interruptMode
|
|
300
|
+
* is "wait".
|
|
293
301
|
* The host owns message injection at the next boundary.
|
|
294
302
|
*/
|
|
295
303
|
hasIrcInterrupts?: () => boolean | Promise<boolean>;
|
|
@@ -298,8 +306,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
298
306
|
* process) is queued for aside injection at the next boundary.
|
|
299
307
|
*
|
|
300
308
|
* Same rules as {@link hasIrcInterrupts}: non-consuming, only cuts
|
|
301
|
-
* *interruptible* waits short,
|
|
302
|
-
* it a completion notice sits behind an hour-long `
|
|
309
|
+
* *interruptible* waits short, in either interruptMode. Without
|
|
310
|
+
* it a completion notice sits behind an hour-long `wait` that the agent
|
|
303
311
|
* would have abandoned had it seen the notice. Unlike a peer IRC it never
|
|
304
312
|
* raises {@link ToolCallContext.steeringSignal}: a queued completion must
|
|
305
313
|
* not push ordinary foreground work (auto-background bash/eval) into the
|
|
@@ -1034,6 +1042,12 @@ export interface AgentTool<
|
|
|
1034
1042
|
loadMode?: ToolLoadMode;
|
|
1035
1043
|
/** Short one-line summary used for tool discovery indexes. */
|
|
1036
1044
|
summary?: string;
|
|
1045
|
+
/**
|
|
1046
|
+
* On-demand documentation topics (`topic → markdown`), readable as
|
|
1047
|
+
* `xd://<tool>/<topic>`. Lets a tool keep large sub-surfaces out of its
|
|
1048
|
+
* description and advertise only a one-line pointer per topic.
|
|
1049
|
+
*/
|
|
1050
|
+
docTopics?(): Readonly<Record<string, string>>;
|
|
1037
1051
|
/**
|
|
1038
1052
|
* Concurrency mode for tool scheduling when multiple calls are in one turn.
|
|
1039
1053
|
* - "shared": can run alongside other shared tools (default)
|
|
@@ -1058,7 +1072,7 @@ export interface AgentTool<
|
|
|
1058
1072
|
* cleanly (e.g. `job` poll), so the abort surfaces the tool's current
|
|
1059
1073
|
* snapshot rather than corrupting a side effect. Every other call runs to
|
|
1060
1074
|
* completion even when steering is queued; the message lands at the next
|
|
1061
|
-
* batch boundary. Honored
|
|
1075
|
+
* batch boundary. Honored in both `interruptMode`s.
|
|
1062
1076
|
*/
|
|
1063
1077
|
interruptible?: boolean | ((args: Partial<Static<TParameters>>) => boolean);
|
|
1064
1078
|
/**
|