@linxiraos/pi-agent-core 1.1.26 → 1.2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +6 -243
  2. package/README.md +1 -1
  3. package/dist/types/agent.d.ts +27 -7
  4. package/dist/types/compaction/anthropic.d.ts +14 -8
  5. package/dist/types/compaction/branch-summarization.d.ts +1 -1
  6. package/dist/types/compaction/compaction-v2-streaming.d.ts +5 -1
  7. package/dist/types/compaction/compaction.d.ts +24 -2
  8. package/dist/types/compaction/entries.d.ts +2 -2
  9. package/dist/types/compaction/messages.d.ts +6 -0
  10. package/dist/types/compaction/openai.d.ts +14 -2
  11. package/dist/types/compaction/pruning.d.ts +64 -16
  12. package/dist/types/compaction/tool-protection.d.ts +2 -1
  13. package/dist/types/compaction/utils.d.ts +5 -0
  14. package/dist/types/image-tokens.d.ts +29 -0
  15. package/dist/types/index.d.ts +1 -0
  16. package/dist/types/speculative-execution.d.ts +1 -1
  17. package/dist/types/telemetry.d.ts +70 -60
  18. package/dist/types/tokenizer.d.ts +9 -6
  19. package/dist/types/tool-arguments.d.ts +19 -0
  20. package/dist/types/types.d.ts +12 -1
  21. package/package.json +8 -8
  22. package/src/agent-loop.ts +191 -34
  23. package/src/agent.ts +126 -21
  24. package/src/append-only-context.ts +38 -29
  25. package/src/compaction/anthropic.ts +30 -12
  26. package/src/compaction/branch-summarization.ts +1 -1
  27. package/src/compaction/compaction-v2-streaming.ts +23 -16
  28. package/src/compaction/compaction.ts +163 -73
  29. package/src/compaction/entries.ts +2 -2
  30. package/src/compaction/message-cache.ts +1 -1
  31. package/src/compaction/messages.ts +25 -0
  32. package/src/compaction/openai.ts +65 -28
  33. package/src/compaction/pruning.ts +328 -96
  34. package/src/compaction/tool-protection.ts +8 -2
  35. package/src/compaction/utils.ts +9 -0
  36. package/src/image-tokens.ts +77 -0
  37. package/src/index.ts +2 -0
  38. package/src/prompts/dsml-tool-call-leak.md +19 -0
  39. package/src/speculative-execution.ts +5 -9
  40. package/src/telemetry.ts +100 -81
  41. package/src/tokenizer.ts +24 -17
  42. package/src/tool-arguments.ts +38 -0
  43. package/src/types.ts +12 -1
@@ -1,10 +1,29 @@
1
1
  /**
2
2
  * Tool output pruning utilities for compaction.
3
3
  */
4
+ import type { ToolResultMessage } from "@linxiraos/pi-ai";
4
5
  import type { Tokenizer } from "../tokenizer.js";
5
6
  import type { SessionEntry } from "./entries.js";
7
+ import { type ConvertToLlm } from "./messages.js";
6
8
  import { type ProtectedToolMatcher } from "./tool-protection.js";
7
- export interface PruneConfig {
9
+ /** Bound warm-cache pruning by the provider's prompt-cache lookback window. */
10
+ export interface CacheLookbackConfig {
11
+ /**
12
+ * Prompt-cache lookback of the model the next request goes to, in block
13
+ * positions (catalog `prompt-cache-lookback`; Anthropic: 20). When set,
14
+ * warm-cache pruning also leaves a result whose rewrite would put the next
15
+ * request's cache lookup out of reach, whatever its token suffix (see
16
+ * `cacheLookbackFloor`). Undefined = no lookback bound.
17
+ */
18
+ cacheLookbackPositions?: number;
19
+ /**
20
+ * Projects context messages to what the provider receives, so lookback
21
+ * positions count app roles (file mentions, command output, custom
22
+ * messages) as sent. Default {@link defaultConvertToLlm}.
23
+ */
24
+ convertToLlm?: ConvertToLlm;
25
+ }
26
+ export interface PruneConfig extends CacheLookbackConfig {
8
27
  /** Keep the most recent tool output tokens intact. */
9
28
  protectTokens: number;
10
29
  /** Only prune if total savings meets this threshold. */
@@ -18,6 +37,8 @@ export interface PruneConfig {
18
37
  * unchanged.
19
38
  */
20
39
  supersedeKey?: SupersedeKeyFn;
40
+ /** Whether a keyed result shows its whole target (see {@link SupersedePruneConfig.supersedeComplete}). */
41
+ supersedeComplete?: SupersedeCompleteFn;
21
42
  /** Useless-flagged results bypass the protect window (see {@link USELESS_NOTICE}). Default true. */
22
43
  pruneUseless?: boolean;
23
44
  /**
@@ -34,7 +55,8 @@ export interface PruneConfig {
34
55
  * already-sent cache prefix: mutating it forces the provider to re-write the
35
56
  * whole suffix (cacheWrite premium). Such results — including superseded and
36
57
  * useless ones, which otherwise bypass {@link protectTokens} — are left for
37
- * compaction/shake (which rebuild the cache anyway) to reclaim. Undefined =
58
+ * compaction/shake (which rebuild the cache anyway) to reclaim, as are
59
+ * results beyond {@link CacheLookbackConfig.cacheLookbackPositions}. Undefined =
38
60
  * no cache guard (legacy: superseded/useless prune at any depth).
39
61
  */
40
62
  cacheWarmSuffixTokens?: number;
@@ -56,18 +78,42 @@ export declare const SUPERSEDED_NOTICE = "[Superseded by a newer read of this fi
56
78
  export declare const USELESS_NOTICE = "[Uneventful result elided]";
57
79
  /**
58
80
  * Maps a tool call to a supersede key. Results sharing a key form a group in
59
- * which every result except the newest is a supersede candidate. A key `K`
60
- * additionally supersedes keys with prefix `K + "\u0000"` (selector-free read
61
- * supersedes selector-carrying reads of the same base path). Return
62
- * `undefined` to exempt a call from supersede grouping.
81
+ * which a newer successful result supersedes every older one. A key `K` also
82
+ * covers keys with prefix `K + "\u0000"`: they are superseded only when the
83
+ * newest successful result for `K` is complete (see
84
+ * {@link SupersedeCompleteFn}). Return `undefined` to exempt a call from
85
+ * supersede grouping.
63
86
  */
64
87
  export type SupersedeKeyFn = (toolName: string, args: Record<string, unknown>) => string | undefined;
65
- export interface SupersedePruneConfig {
66
- /** Supersede key function; results sharing a key supersede older ones. */
88
+ /**
89
+ * Whether a successful keyed result shows its whole target, so a result for a
90
+ * parent key (e.g. a selector-free read) may supersede child-key results (e.g.
91
+ * range reads). A summary, truncated page, or notice is not complete.
92
+ */
93
+ export type SupersedeCompleteFn = (message: ToolResultMessage) => boolean;
94
+ export interface SupersedePruneConfig extends CacheLookbackConfig {
95
+ /** Supersede key function; a newer successful result with the same key supersedes older ones (see {@link SupersedeKeyFn}). */
67
96
  supersedeKey?: SupersedeKeyFn;
97
+ /**
98
+ * Whether a successful keyed result is complete. Absent, every successful
99
+ * result is complete. Child-key results are superseded only when the parent
100
+ * key's newest successful result is complete; same-key results follow
101
+ * newest-wins, since a same-key re-read that shows a different view means
102
+ * the target changed. A failed result supersedes only older failed results.
103
+ *
104
+ * Trade-off, by design: after `read foo.ts:50-200` → edit → a bare
105
+ * `read foo.ts` that returns a summary, the pre-edit range stays in context
106
+ * until a complete bare read or a same-range re-read. Keeping possibly stale
107
+ * detail is preferred over losing detail the summary does not show.
108
+ */
109
+ supersedeComplete?: SupersedeCompleteFn;
68
110
  /** Also prune results flagged useless by their tool. Default false. */
69
111
  pruneUseless?: boolean;
70
- /** Prune a candidate now when all messages after it total at most this many estimated tokens. Default 8 000. */
112
+ /**
113
+ * Prune a candidate now when all messages after it total at most this many
114
+ * estimated tokens and it sits within
115
+ * {@link CacheLookbackConfig.cacheLookbackPositions}. Default 8 000.
116
+ */
71
117
  suffixTokenLimit?: number;
72
118
  /**
73
119
  * Prune all candidates when the last message is at least this old: the
@@ -104,18 +150,20 @@ export declare const MIN_PRUNE_TOKENS = 50;
104
150
  * candidate is pruned now only when the suffix after it is small (tail case —
105
151
  * the read→edit→read loop) or when the context has been idle long enough that
106
152
  * the provider cache is cold anyway (then all still-sent candidates flush).
107
- * Never mutates entries before `keepBoundaryId` (summarized away — not sent).
153
+ * Never mutates entries before `keepBoundaryId` (summarized away — not sent),
154
+ * and never walks them unless a sent result's call lives there.
108
155
  */
109
- export declare function pruneSupersededToolResults(entries: SessionEntry[], tokenizer: Tokenizer, config: SupersedePruneConfig): PruneResult;
110
- export declare function pruneToolOutputs(entries: SessionEntry[], tokenizer: Tokenizer, config?: PruneConfig): PruneResult;
156
+ export declare function pruneSupersededToolResults(entries: readonly SessionEntry[], tokenizer: Tokenizer, config: SupersedePruneConfig): PruneResult;
157
+ export declare function pruneToolOutputs(entries: readonly SessionEntry[], tokenizer: Tokenizer, config?: PruneConfig): PruneResult;
111
158
  /**
112
159
  * Supersede key for the `read` tool: the file path with the trailing line/raw
113
160
  * selector stripped (the read tool's own splitter grammar via
114
161
  * {@link splitReadSelector}, e.g. `src/foo.ts:50-200`, `:2-4:raw`).
115
162
  * Internal/URL-scheme paths (`skill://…`, `https://…`) are exempt.
116
- * Selector-free reads key on the bare path; selector-carrying reads key on
117
- * `path + "\u0000" + selector`, so two reads collide only when the newer is
118
- * selector-free or the selectors are identical (the pass's prefix rule lets a
119
- * bare-path read supersede selector-carrying reads of the same file).
163
+ * Selector-free reads key on the bare path; line-range reads key on
164
+ * `path + "\u0000" + selector`, so a complete bare-path read can supersede them.
165
+ * `raw` and `conflicts` reads show a different form than a plain read (e.g. raw
166
+ * notebook JSON vs converted notebook text), so they key on
167
+ * `path + "\u0001" + selector` and are superseded only by the same selector.
120
168
  */
121
169
  export declare function readToolSupersedeKey(toolName: string, args: Record<string, unknown>): string | undefined;
@@ -6,7 +6,8 @@ export interface ProtectedToolContext {
6
6
  readonly toolCall: AgentToolCall | undefined;
7
7
  }
8
8
  export type ProtectedToolMatcher = string | ((context: ProtectedToolContext) => boolean);
9
- export declare function collectToolCallsById(entries: readonly SessionEntry[]): Map<string, AgentToolCall>;
9
+ /** Map tool-call ids to their calls across `entries[start, end)` (later calls win). */
10
+ export declare function collectToolCallsById(entries: readonly SessionEntry[], start?: number, end?: number): Map<string, AgentToolCall>;
10
11
  /** Return the tool-result message carried by a session entry, if any. */
11
12
  export declare function getToolResultMessage(entry: SessionEntry): ToolResultMessage | undefined;
12
13
  /**
@@ -26,6 +26,11 @@ export declare function splitReadSelector(path: string): {
26
26
  * and matches its write/edit path when computing Read/Write/RW markers.
27
27
  */
28
28
  export declare function stripReadSelector(path: string): string;
29
+ /**
30
+ * Whether a selector from {@link splitReadSelector} shows the file in a form other
31
+ * than its plain lines: verbatim `raw` bytes or the merge `conflicts` view.
32
+ */
33
+ export declare function isAlternateFormReadSelector(sel: string): boolean;
29
34
  /**
30
35
  * Whether `path` references a `scheme://` URL (internal URI or web URL) rather
31
36
  * than a filesystem path that belongs in the compaction `<files>` summary.
@@ -0,0 +1,29 @@
1
+ import type { ImageContent } from "@linxiraos/pi-ai";
2
+ import { type ImageSize } from "@linxiraos/pi-catalog/compat/image-tokenization";
3
+ /**
4
+ * Dimension-based image token estimates shared by the local context counter
5
+ * ({@link Tokenizer.countMessage}) and the native remote-compaction fit probe.
6
+ * Both sides must price the same image the same way: when they disagreed
7
+ * (1.2k flat locally vs a 12k flat worst case remotely) an image-heavy session
8
+ * stayed under the local compaction trigger yet was refused as over-window by
9
+ * the remote probe.
10
+ *
11
+ * Follows the catalog's image rule for the OpenAI Responses wire (GPT-5.5's
12
+ * 32px patches, a per-detail pixel limit and patch budget, x1.2 multiplier),
13
+ * which is also a close upper estimate for other providers once omp has
14
+ * downscaled the image (≤1568px by default).
15
+ */
16
+ /** OpenAI image `detail` values; `undefined` means the provider default (`auto`). */
17
+ export type ImageDetail = ImageContent["detail"];
18
+ export type { ImageSize };
19
+ /**
20
+ * Estimated input tokens for one image. Unknown dimensions (undecodable data,
21
+ * remote URLs, provider file ids) charge the detail level's full patch budget.
22
+ */
23
+ export declare function estimateImageTokens(size: ImageSize | null | undefined, detail?: ImageDetail): number;
24
+ /** Pixel size read from the header of base64-encoded image data, or null. */
25
+ export declare function base64ImageSize(base64: string): ImageSize | null;
26
+ /** Pixel size of a `data:<mime>;base64,` URL, or null for any other URL. */
27
+ export declare function dataUrlImageSize(url: string): ImageSize | null;
28
+ /** {@link estimateImageTokens} for an in-memory image block. */
29
+ export declare function estimateImageContentTokens(image: Pick<ImageContent, "data" | "detail">): number;
@@ -13,5 +13,6 @@ export * from "./telemetry.js";
13
13
  export * from "./thinking.js";
14
14
  export * from "./tool-context.js";
15
15
  export * from "./tokenizer.js";
16
+ export * from "./tool-arguments.js";
16
17
  export * from "./types.js";
17
18
  export * from "./utils/yield.js";
@@ -1,4 +1,4 @@
1
- import { type AssistantMessage } from "@linxiraos/pi-ai";
1
+ import type { AssistantMessage } from "@linxiraos/pi-ai";
2
2
  import type { AgentContext, AgentLoopConfig, AgentTool, AgentToolCall, AgentToolResult, SpeculativeAuthorization, SpeculativeChildDefinition, SpeculativeChildHandle, SpeculativeLaunchContext, SpeculativeToolExecutionConfig, ToolSpeculationEffect, ToolSpeculationStreamSession } from "./types.js";
3
3
  export type SpeculativeRawOutcome = {
4
4
  result: AgentToolResult<unknown>;
@@ -2,7 +2,7 @@
2
2
  * OpenTelemetry instrumentation for the agent loop.
3
3
  *
4
4
  * Implements the OpenTelemetry GenAI semantic conventions
5
- * (https://opentelemetry.io/docs/specs/semconv/gen-ai/) plus `omp.gen_ai.*`
5
+ * (https://opentelemetry.io/docs/specs/semconv/gen-ai/) plus `zeta.gen_ai.*`
6
6
  * extension attributes for run summaries, dashboard summaries, and cost hints
7
7
  * that are useful to downstream observability UIs.
8
8
  *
@@ -79,40 +79,40 @@ export declare const enum OpenAIAttr {
79
79
  }
80
80
  /** Project extension attributes. Kept out of the reserved `gen_ai.*` namespace. */
81
81
  export declare const enum OmpGenAIAttr {
82
- AgentStepNumber = "omp.gen_ai.agent.step.number",
83
- AgentStepCount = "omp.gen_ai.agent.step.count",
84
- RequestReasoningEffort = "omp.gen_ai.request.reasoning.effort",
85
- RequestToolChoice = "omp.gen_ai.request.tool.choice",
86
- RequestAvailableTools = "omp.gen_ai.request.available_tools",
87
- RequestMessages = "omp.gen_ai.request.messages",
88
- ResponseText = "omp.gen_ai.response.text",
89
- ResponseToolCalls = "omp.gen_ai.response.tool_calls",
90
- ResponseUpstreamProvider = "omp.gen_ai.response.upstream_provider",
91
- UsageTotalTokens = "omp.gen_ai.usage.total_tokens",
92
- UsageServerSideTools = "omp.gen_ai.usage.server_tool_requests",
93
- CostEstimatedUsd = "omp.gen_ai.cost.estimated_usd",
94
- CostInputUsd = "omp.gen_ai.cost.input_usd",
95
- CostOutputUsd = "omp.gen_ai.cost.output_usd",
96
- CostUnavailableReason = "omp.gen_ai.cost.unavailable_reason",
97
- ToolStatus = "omp.gen_ai.tool.status",
98
- ToolCallIntent = "omp.gen_ai.tool.call.intent",
99
- HandoffFromAgentName = "omp.gen_ai.handoff.from_agent.name",
100
- HandoffFromAgentId = "omp.gen_ai.handoff.from_agent.id",
101
- HandoffToAgentName = "omp.gen_ai.handoff.to_agent.name",
102
- HandoffToAgentId = "omp.gen_ai.handoff.to_agent.id",
103
- OneshotKind = "omp.gen_ai.oneshot.kind",
104
- GatewayName = "omp.gen_ai.gateway.name",
105
- GatewayEndpoint = "omp.gen_ai.gateway.endpoint",
106
- GatewayCallId = "omp.gen_ai.gateway.call_id",
107
- GatewayRoutedTo = "omp.gen_ai.gateway.routed_to",
82
+ AgentStepNumber = "zeta.gen_ai.agent.step.number",
83
+ AgentStepCount = "zeta.gen_ai.agent.step.count",
84
+ RequestReasoningEffort = "zeta.gen_ai.request.reasoning.effort",
85
+ RequestToolChoice = "zeta.gen_ai.request.tool.choice",
86
+ RequestAvailableTools = "zeta.gen_ai.request.available_tools",
87
+ RequestMessages = "zeta.gen_ai.request.messages",
88
+ ResponseText = "zeta.gen_ai.response.text",
89
+ ResponseToolCalls = "zeta.gen_ai.response.tool_calls",
90
+ ResponseUpstreamProvider = "zeta.gen_ai.response.upstream_provider",
91
+ UsageTotalTokens = "zeta.gen_ai.usage.total_tokens",
92
+ UsageServerSideTools = "zeta.gen_ai.usage.server_tool_requests",
93
+ CostEstimatedUsd = "zeta.gen_ai.cost.estimated_usd",
94
+ CostInputUsd = "zeta.gen_ai.cost.input_usd",
95
+ CostOutputUsd = "zeta.gen_ai.cost.output_usd",
96
+ CostUnavailableReason = "zeta.gen_ai.cost.unavailable_reason",
97
+ ToolStatus = "zeta.gen_ai.tool.status",
98
+ ToolCallIntent = "zeta.gen_ai.tool.call.intent",
99
+ HandoffFromAgentName = "zeta.gen_ai.handoff.from_agent.name",
100
+ HandoffFromAgentId = "zeta.gen_ai.handoff.from_agent.id",
101
+ HandoffToAgentName = "zeta.gen_ai.handoff.to_agent.name",
102
+ HandoffToAgentId = "zeta.gen_ai.handoff.to_agent.id",
103
+ OneshotKind = "zeta.gen_ai.oneshot.kind",
104
+ GatewayName = "zeta.gen_ai.gateway.name",
105
+ GatewayEndpoint = "zeta.gen_ai.gateway.endpoint",
106
+ GatewayCallId = "zeta.gen_ai.gateway.call_id",
107
+ GatewayRoutedTo = "zeta.gen_ai.gateway.routed_to",
108
108
  /** Cloudflare AI Gateway response-cache status (`cf-aig-cache-status`), never prompt-cache. */
109
- GatewayResponseCacheStatus = "omp.gen_ai.gateway.response_cache.status",
109
+ GatewayResponseCacheStatus = "zeta.gen_ai.gateway.response_cache.status",
110
110
  /** Caller-level reason a judgment ran (`find`, `ttsr`, `judge_batch`, …). */
111
- JudgmentPurpose = "omp.gen_ai.judgment.purpose",
111
+ JudgmentPurpose = "zeta.gen_ai.judgment.purpose",
112
112
  /** Questions the caller asked in one judgment request. */
113
- JudgmentQuestions = "omp.gen_ai.judgment.questions",
113
+ JudgmentQuestions = "zeta.gen_ai.judgment.questions",
114
114
  /** Questions answered from the local judgment cache instead of the provider. */
115
- JudgmentCachedQuestions = "omp.gen_ai.judgment.cached_questions"
115
+ JudgmentCachedQuestions = "zeta.gen_ai.judgment.cached_questions"
116
116
  }
117
117
  /** GenAI operation names — values for {@link GenAIAttr.OperationName}. */
118
118
  export declare const GenAIOperation: {
@@ -145,16 +145,24 @@ export interface ChatUsageSnapshot {
145
145
  }
146
146
  /** Context passed to the cost estimator. */
147
147
  export interface CostEstimatorContext {
148
+ /** OTel provider label (`gen_ai.provider.name`). */
148
149
  readonly provider: string;
150
+ /** Provider id before OTel normalization (e.g. `openai-codex` for the `openai` label). */
151
+ readonly providerId: string | undefined;
152
+ /** Model id reported on the response; a provider may report a served model instead of the requested one. */
149
153
  readonly model: string;
154
+ /** Requested model id, when the caller knows it. */
155
+ readonly modelId: string | undefined;
150
156
  readonly serviceTier: ServiceTier | undefined;
151
157
  readonly usage: ChatUsageSnapshot;
158
+ /** Cost the provider already computed for this request; the value persisted with the message. */
159
+ readonly usageCost: Usage["cost"];
152
160
  }
153
161
  /**
154
162
  * Cost estimator result.
155
- * { usd: number } — cost is known; emitted as omp.gen_ai.cost.estimated_usd
163
+ * { usd: number } — cost is known; emitted as zeta.gen_ai.cost.estimated_usd
156
164
  * { unavailable: string } — cost is intentionally unknown; emitted as
157
- * omp.gen_ai.cost.unavailable_reason
165
+ * zeta.gen_ai.cost.unavailable_reason
158
166
  * undefined — no opinion; nothing emitted
159
167
  */
160
168
  export type CostEstimate = {
@@ -205,7 +213,7 @@ export interface ChatUsageEvent {
205
213
  *
206
214
  * Use this to reconcile gateway-issued ids (e.g. `x-litellm-call-id`) with
207
215
  * downstream billing / spend dashboards. Known gateway patterns are also
208
- * auto-stamped on the chat span as `omp.gen_ai.gateway.*` attributes.
216
+ * auto-stamped on the chat span as `zeta.gen_ai.gateway.*` attributes.
209
217
  */
210
218
  readonly headers: Readonly<Record<string, string>> | undefined;
211
219
  }
@@ -390,6 +398,8 @@ export interface ChatRequestSnapshot {
390
398
  */
391
399
  export declare function finishChatSpan(telemetry: AgentTelemetry | undefined, span: Span | undefined, message: AssistantMessage, options: {
392
400
  readonly stepNumber: number;
401
+ /** Requested model id; `message.model` may name a served fallback model instead. */
402
+ readonly modelId?: string;
393
403
  readonly serviceTier?: ServiceTier;
394
404
  readonly responseHeaders?: Readonly<Record<string, string>>;
395
405
  readonly baseUrl?: string;
@@ -508,10 +518,10 @@ export interface InstrumentedChatSpanOptions {
508
518
  /** Step index recorded on the span; defaults to `-1` for non-loop calls. */
509
519
  readonly stepNumber?: number;
510
520
  /**
511
- * Tag stamped onto `omp.gen_ai.oneshot.kind`. Values used by the agent:
521
+ * Tag stamped onto `zeta.gen_ai.oneshot.kind`. Values used by the agent:
512
522
  * `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`,
513
- * `handoff`, `branch_summary`, `image_question`. Free-form to allow callers
514
- * outside this package to add new kinds without bumping the helper.
523
+ * `handoff`, `branch_summary`, `image_question`, `skill_description`. Free-form
524
+ * to allow callers outside this package to add new kinds without bumping the helper.
515
525
  */
516
526
  readonly oneshotKind?: string;
517
527
  /** Extra span attributes applied verbatim. */
@@ -613,30 +623,30 @@ export declare function finishInvokeAgentSpan(telemetry: AgentTelemetry | undefi
613
623
  * calling this helper.
614
624
  */
615
625
  export declare function fireOnRunEnd(telemetry: AgentTelemetry, summary: AgentRunSummary, coverage: AgentRunCoverage): void;
616
- /** Aggregate `omp.gen_ai.agent.*` attributes stamped on the `invoke_agent` span. */
626
+ /** Aggregate `zeta.gen_ai.agent.*` attributes stamped on the `invoke_agent` span. */
617
627
  export declare const enum OmpGenAIAggregateAttr {
618
- ChatsCount = "omp.gen_ai.agent.chats.count",
619
- ChatsTotalLatencyMs = "omp.gen_ai.agent.chats.total_latency_ms",
620
- ChatsStopReasonPrefix = "omp.gen_ai.agent.chats.stop_reason.",
621
- ToolsCount = "omp.gen_ai.agent.tools.count",
622
- ToolsOkCount = "omp.gen_ai.agent.tools.ok.count",
623
- ToolsErrorCount = "omp.gen_ai.agent.tools.error.count",
624
- ToolsSkippedCount = "omp.gen_ai.agent.tools.skipped.count",
625
- ToolsBlockedCount = "omp.gen_ai.agent.tools.blocked.count",
626
- ToolsTimeoutCount = "omp.gen_ai.agent.tools.timeout.count",
627
- ToolsAbortedCount = "omp.gen_ai.agent.tools.aborted.count",
628
- ToolsTotalLatencyMs = "omp.gen_ai.agent.tools.total_latency_ms",
629
- ToolsInvoked = "omp.gen_ai.agent.tools.invoked",
630
- ToolsAvailable = "omp.gen_ai.agent.tools.available",
631
- ToolsUnused = "omp.gen_ai.agent.tools.unused",
632
- UsageInputTokensTotal = "omp.gen_ai.agent.usage.input_tokens.total",
633
- UsageOutputTokensTotal = "omp.gen_ai.agent.usage.output_tokens.total",
634
- UsageCacheReadInputTokensTotal = "omp.gen_ai.agent.usage.cache_read.input_tokens.total",
635
- UsageCacheCreationInputTokensTotal = "omp.gen_ai.agent.usage.cache_creation.input_tokens.total",
636
- UsageReasoningOutputTokensTotal = "omp.gen_ai.agent.usage.reasoning.output_tokens.total",
637
- UsageTotalTokensTotal = "omp.gen_ai.agent.usage.total_tokens.total",
638
- CostEstimatedUsdTotal = "omp.gen_ai.agent.cost.estimated_usd.total",
639
- ErrorsCount = "omp.gen_ai.agent.errors.count"
628
+ ChatsCount = "zeta.gen_ai.agent.chats.count",
629
+ ChatsTotalLatencyMs = "zeta.gen_ai.agent.chats.total_latency_ms",
630
+ ChatsStopReasonPrefix = "zeta.gen_ai.agent.chats.stop_reason.",
631
+ ToolsCount = "zeta.gen_ai.agent.tools.count",
632
+ ToolsOkCount = "zeta.gen_ai.agent.tools.ok.count",
633
+ ToolsErrorCount = "zeta.gen_ai.agent.tools.error.count",
634
+ ToolsSkippedCount = "zeta.gen_ai.agent.tools.skipped.count",
635
+ ToolsBlockedCount = "zeta.gen_ai.agent.tools.blocked.count",
636
+ ToolsTimeoutCount = "zeta.gen_ai.agent.tools.timeout.count",
637
+ ToolsAbortedCount = "zeta.gen_ai.agent.tools.aborted.count",
638
+ ToolsTotalLatencyMs = "zeta.gen_ai.agent.tools.total_latency_ms",
639
+ ToolsInvoked = "zeta.gen_ai.agent.tools.invoked",
640
+ ToolsAvailable = "zeta.gen_ai.agent.tools.available",
641
+ ToolsUnused = "zeta.gen_ai.agent.tools.unused",
642
+ UsageInputTokensTotal = "zeta.gen_ai.agent.usage.input_tokens.total",
643
+ UsageOutputTokensTotal = "zeta.gen_ai.agent.usage.output_tokens.total",
644
+ UsageCacheReadInputTokensTotal = "zeta.gen_ai.agent.usage.cache_read.input_tokens.total",
645
+ UsageCacheCreationInputTokensTotal = "zeta.gen_ai.agent.usage.cache_creation.input_tokens.total",
646
+ UsageReasoningOutputTokensTotal = "zeta.gen_ai.agent.usage.reasoning.output_tokens.total",
647
+ UsageTotalTokensTotal = "zeta.gen_ai.agent.usage.total_tokens.total",
648
+ CostEstimatedUsdTotal = "zeta.gen_ai.agent.cost.estimated_usd.total",
649
+ ErrorsCount = "zeta.gen_ai.agent.errors.count"
640
650
  }
641
651
  /**
642
652
  * Run `fn` with `span` activated on the OTEL context. Spans created
@@ -1,5 +1,6 @@
1
1
  import type { Model } from "@linxiraos/pi-ai";
2
2
  import * as natives from "@linxiraos/pi-natives";
3
+ import * as snapcompact from "@linxiraos/pi-snapcompact";
3
4
  import type { AgentMessage } from "./types.js";
4
5
  /** Maps the catalog-resolved tokenizer family to its native implementation. */
5
6
  export declare function tokenizerEncodingForModel(model: Pick<Model, "tokenizer"> | null | undefined): natives.Encoding | null;
@@ -42,14 +43,15 @@ export interface TokenBudgetCheck {
42
43
  * Model-aware local token counter. Immutable: the catalog-resolved encoding
43
44
  * is fixed at construction, so a cached count can never straddle two
44
45
  * encodings. An `Agent` owns one for its active model (swapping the instance
45
- * when the model's encoding changes); one-shot flows construct their own for
46
- * the model that will be billed. Known tokenizer families use exact native
47
- * counts; unknown models keep the fast byte estimate (or o200k when
48
- * `PI_TOKENIZER_ACCURATE=1`).
46
+ * when the model's encoding or snapcompact frame pricing changes); one-shot
47
+ * flows construct their own for the model that will be billed. Known
48
+ * tokenizer families use exact native counts; unknown models keep the fast
49
+ * byte estimate (or o200k when `PI_TOKENIZER_ACCURATE=1`).
49
50
  */
50
51
  export declare class Tokenizer {
51
52
  #private;
52
- constructor(model?: Pick<Model, "tokenizer"> | null);
53
+ readonly frameBillingKey: string;
54
+ constructor(model?: (Pick<Model, "tokenizer"> & snapcompact.ShapeTarget) | null);
53
55
  get encoding(): natives.Encoding | null;
54
56
  countTokens(text: string | string[], mode?: TokenCountMode): number;
55
57
  /**
@@ -70,7 +72,8 @@ export declare class Tokenizer {
70
72
  * Settled historical messages are counted once and reused until an owner
71
73
  * (prune/shake/strip-images) calls `invalidateMessageCache`; streaming
72
74
  * assistants bypass the memo entirely (see the message-cache settle-gate
73
- * invariant). Image blocks charge a fixed per-image estimate.
75
+ * invariant). Image blocks charge {@link estimateImageContentTokens}, the
76
+ * same dimension-based estimate the native remote-compaction probe applies.
74
77
  */
75
78
  countMessage(message: AgentMessage, options?: MessageCountOptions): number;
76
79
  /** Sum of {@link countMessage} over `messages`. */
@@ -0,0 +1,19 @@
1
+ import { type ToolCall } from "@linxiraos/pi-ai";
2
+ import type { AgentTool } from "./types.js";
3
+ /**
4
+ * Where a call's `__parseError`/`__rawJson` keys came from.
5
+ * - `"parsed"`: a provider stream parsed the arguments, so `__parseError` is a
6
+ * genuine parse-failure sentinel and is never handed to a tool.
7
+ * - `"payload"`: the arguments are a payload the model wrote (eval tool bridge,
8
+ * `xd://` device content), so those keys are forged and only stripped.
9
+ */
10
+ export type ToolArgumentSource = "parsed" | "payload";
11
+ /**
12
+ * Validate a call's arguments against `tool`'s schema, honoring
13
+ * {@link AgentTool.lenientArgValidation}: on a schema mismatch a lenient tool
14
+ * receives its raw arguments minus the internal `__parseError`/`__rawJson`
15
+ * sentinels. Malformed JSON from a provider stream is never lenient.
16
+ *
17
+ * @throws the validation error when the tool is strict or the arguments failed to parse.
18
+ */
19
+ export declare function validateAgentToolArguments(tool: AgentTool, toolCall: ToolCall, source?: ToolArgumentSource): ToolCall["arguments"];
@@ -269,6 +269,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
269
269
  * background.
270
270
  */
271
271
  hasBackgroundCompletions?: () => boolean | Promise<boolean>;
272
+ /**
273
+ * Peeks whether a passive aside (an extension or user message queued with
274
+ * `deliverAs: "aside"`) is waiting for injection at the next boundary.
275
+ *
276
+ * Same rules as {@link hasBackgroundCompletions}: non-consuming, ends only
277
+ * *interruptible* waits, never raises {@link ToolCallContext.steeringSignal}.
278
+ * The host still injects the aside once at the boundary; ordinary running
279
+ * tools finish first.
280
+ */
281
+ hasQueuedAsides?: () => boolean | Promise<boolean>;
272
282
  /**
273
283
  * Returns follow-up messages to process after the agent would otherwise stop.
274
284
  *
@@ -281,7 +291,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
281
291
  * Returns non-interrupting "aside" messages to inject at a step boundary.
282
292
  *
283
293
  * Polled after each tool batch (before the next LLM call) AND at the yield
284
- * check. Unlike steering, these NEVER abort in-flight tools — they are passive
294
+ * check. Unlike steering, these never abort foreground tools (only an
295
+ * interruptible `wait` ends early, via the peek hooks) — they are passive
285
296
  * notifications (e.g. background-job completions, late LSP diagnostics) that
286
297
  * should reach the model between requests without waiting for the agent to
287
298
  * fully stop. Returned messages are appended to the context with normal
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-agent-core",
4
- "version": "1.1.26",
4
+ "version": "1.2.0",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Linxira-OS",
@@ -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
- "@linxiraos/pi-ai": "1.1.26",
39
- "@linxiraos/pi-catalog": "1.1.26",
40
- "@linxiraos/pi-natives": "1.1.26",
41
- "@linxiraos/pi-utils": "1.1.26",
42
- "@linxiraos/pi-wire": "1.1.26",
43
- "@linxiraos/pi-snapcompact": "1.1.26",
38
+ "@linxiraos/pi-ai": "1.2.0",
39
+ "@linxiraos/pi-catalog": "1.2.0",
40
+ "@linxiraos/pi-natives": "1.2.0",
41
+ "@linxiraos/pi-utils": "1.2.0",
42
+ "@linxiraos/pi-wire": "1.2.0",
43
+ "@linxiraos/pi-snapcompact": "1.2.0",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@linxiraos/pi-omptype": "1.1.26",
47
+ "@linxiraos/pi-omptype": "1.2.0",
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"