@linxiraos/pi-agent-core 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [1.4.0] - 2026-10-21
3
+ ## [1.4.2] - 2026-10-24
4
4
 
5
- - Version line moves to 1.4.0 with the rest of the suite; no user-visible change in this package.
5
+ - Version alignment with the 1.4.2 release line (no behavioral changes).
6
6
 
7
7
  ## [9.2.2] - 2026-01-31
8
8
 
@@ -134,6 +134,14 @@
134
134
 
135
135
  Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mono](https://github.com/badlogic/pi-mono).
136
136
 
137
+ ## [1.4.1] - 2026-10-23
138
+
139
+ - Version alignment with the 1.4.1 release line (no behavioral changes).
140
+
141
+ ## [1.4.0] - 2026-10-21
142
+
143
+ - Version line moves to 1.4.0 with the rest of the suite; no user-visible change in this package.
144
+
137
145
  ## [1.3.2] - 2026-10-19
138
146
 
139
147
  - Version line moves to 1.3.2 with the rest of the suite; no user-visible change in this package.
@@ -14,6 +14,8 @@ export declare const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
14
14
  export interface AnthropicCompactionPreserveData {
15
15
  provider: string;
16
16
  content: string;
17
+ /** Opaque signed tool-change records; replayed without normalization. */
18
+ toolChanges?: Record<string, unknown>[];
17
19
  /** Signature attached to an on-demand block; replayed verbatim. */
18
20
  signature?: string;
19
21
  /** Legacy threshold block state; replay-only. */
@@ -35,9 +37,11 @@ export interface AnthropicCompactionPreserveData {
35
37
  * lineage the beta documents); endpoint eligibility is resolved the way the
36
38
  * provider routes requests, so a Foundry or `ANTHROPIC_BASE_URL` reroute of a
37
39
  * first-party model is excluded unless the route opted in with
38
- * `remoteCompaction.enabled`.
40
+ * `remoteCompaction.enabled`. Bedrock Converse models compact through a
41
+ * Bedrock Runtime side request; their ordinary turns stay on Converse.
39
42
  */
40
- export declare function shouldUseAnthropicNativeCompaction(model: Model): model is Model<"anthropic-messages">;
43
+ export declare function shouldUseAnthropicNativeCompaction(model: Model): model is Model<"anthropic-messages" | "bedrock-converse-stream">;
44
+ type AnthropicNativeCompactionModel = Model<"anthropic-messages" | "bedrock-converse-stream">;
41
45
  export declare function getPreservedAnthropicCompactionData(preserveData: Record<string, unknown> | undefined): AnthropicCompactionPreserveData | undefined;
42
46
  /** Set or strip the Anthropic compaction slot; a new compaction never inherits a stale summary. */
43
47
  export declare function withAnthropicCompactionPreserveData(preserveData: Record<string, unknown> | undefined, compaction: AnthropicCompactionPreserveData | undefined): Record<string, unknown> | undefined;
@@ -67,6 +71,7 @@ export interface AnthropicNativeCompactionRequest {
67
71
  export interface AnthropicNativeCompactionResponse {
68
72
  content: string;
69
73
  signature: string;
74
+ toolChanges?: Record<string, unknown>[];
70
75
  usage: Usage;
71
76
  model: string;
72
77
  }
@@ -81,4 +86,5 @@ export interface AnthropicNativeCompactionOptions extends Pick<SimpleStreamOptio
81
86
  * without a summary is a native failure, including tool use, refusals and
82
87
  * output limits; the configured method order can then choose a fallback.
83
88
  */
84
- export declare function requestAnthropicNativeCompaction(model: Model<"anthropic-messages">, apiKey: ApiKey, request: AnthropicNativeCompactionRequest, signal: AbortSignal | undefined, options: AnthropicNativeCompactionOptions): Promise<AnthropicNativeCompactionResponse>;
89
+ export declare function requestAnthropicNativeCompaction(model: AnthropicNativeCompactionModel, apiKey: ApiKey, request: AnthropicNativeCompactionRequest, signal: AbortSignal | undefined, options: AnthropicNativeCompactionOptions): Promise<AnthropicNativeCompactionResponse>;
90
+ export {};
@@ -10,9 +10,10 @@ import type { AgentTool } from "./types.js";
10
10
  export type ToolArgumentSource = "parsed" | "payload";
11
11
  /**
12
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.
13
+ * {@link AgentTool.lenientArgValidation}: a lenient tool keeps unknown root
14
+ * keys its closed schema would heal away, and on a schema mismatch receives its
15
+ * raw arguments minus the internal `__parseError`/`__rawJson` sentinels.
16
+ * Malformed JSON from a provider stream is never lenient.
16
17
  *
17
18
  * @throws the validation error when the tool is strict or the arguments failed to parse.
18
19
  */
@@ -1034,7 +1034,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
1034
1034
  * before ordinary dispatch commits its result.
1035
1035
  */
1036
1036
  speculation?: ToolSpeculationPolicy;
1037
- /** If true, schema validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. Malformed argument JSON still returns the parse error. */
1037
+ /** If true, schema validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM, and unknown root keys a closed schema would drop stay on the args. Malformed argument JSON still returns the parse error. */
1038
1038
  lenientArgValidation?: boolean;
1039
1039
  /**
1040
1040
  * Whether the agent loop may abort this tool mid-execution — or skip it
@@ -1090,6 +1090,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
1090
1090
  approval?: ToolApproval;
1091
1091
  /** Lines appended after the standard approval prompt header. */
1092
1092
  formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
1093
+ /**
1094
+ * Remembered-approval keys for `tools.approvalMode: always-ask-cached`: the files (or URL
1095
+ * targets) this call touches. When every key is already granted the call skips its prompt;
1096
+ * approving the call can record them. Omit it (or return `undefined`) and the call prompts by tier.
1097
+ */
1098
+ approvalCacheTargets?: (args: unknown) => readonly string[] | undefined;
1093
1099
  /** The main execution callback for this tool. */
1094
1100
  execute: AgentToolExecFn<TParameters, TDetails, TTheme>;
1095
1101
  /** Optional custom rendering for tool call display (returns UI component) */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-agent-core",
4
- "version": "1.4.0",
4
+ "version": "1.4.2",
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.4.0",
39
- "@linxiraos/pi-catalog": "1.4.0",
40
- "@linxiraos/pi-natives": "1.4.0",
41
- "@linxiraos/pi-utils": "1.4.0",
42
- "@linxiraos/pi-wire": "1.4.0",
43
- "@linxiraos/pi-snapcompact": "1.4.0",
38
+ "@linxiraos/pi-ai": "1.4.2",
39
+ "@linxiraos/pi-catalog": "1.4.2",
40
+ "@linxiraos/pi-natives": "1.4.2",
41
+ "@linxiraos/pi-utils": "1.4.2",
42
+ "@linxiraos/pi-wire": "1.4.2",
43
+ "@linxiraos/pi-snapcompact": "1.4.2",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@linxiraos/pi-omptype": "1.4.0",
47
+ "@linxiraos/pi-omptype": "1.4.2",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
50
  "@types/bun": "1.4.3"
@@ -19,7 +19,10 @@ import type {
19
19
  } from "@linxiraos/pi-ai";
20
20
  import * as AIError from "@linxiraos/pi-ai/error";
21
21
  import { resolveAnthropicCompactionEffort } from "@linxiraos/pi-ai/providers/anthropic";
22
- import { supportsAnthropicCompaction } from "@linxiraos/pi-ai/providers/anthropic-compaction";
22
+ import {
23
+ supportsAnthropicCompaction,
24
+ supportsBedrockConverseCompaction,
25
+ } from "@linxiraos/pi-ai/providers/anthropic-compaction";
23
26
  import { isRecord, prompt } from "@linxiraos/pi-utils";
24
27
  import { type InstrumentedChatSpanOptions, instrumentedCompleteSimple } from "../telemetry";
25
28
  import type { AgentMessage } from "../types";
@@ -31,6 +34,8 @@ export const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
31
34
  export interface AnthropicCompactionPreserveData {
32
35
  provider: string;
33
36
  content: string;
37
+ /** Opaque signed tool-change records; replayed without normalization. */
38
+ toolChanges?: Record<string, unknown>[];
34
39
  /** Signature attached to an on-demand block; replayed verbatim. */
35
40
  signature?: string;
36
41
  /** Legacy threshold block state; replay-only. */
@@ -51,18 +56,28 @@ function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messa
51
56
  return model.api === "anthropic-messages";
52
57
  }
53
58
 
59
+ function isBedrockConverseModel(model: Model): model is Model<"bedrock-converse-stream"> {
60
+ return model.api === "bedrock-converse-stream";
61
+ }
62
+
54
63
  /**
55
64
  * Whether a model compacts through the Anthropic compaction beta. Model
56
65
  * eligibility is catalog policy (`compat.supportsServerCompaction`, the
57
66
  * lineage the beta documents); endpoint eligibility is resolved the way the
58
67
  * provider routes requests, so a Foundry or `ANTHROPIC_BASE_URL` reroute of a
59
68
  * first-party model is excluded unless the route opted in with
60
- * `remoteCompaction.enabled`.
69
+ * `remoteCompaction.enabled`. Bedrock Converse models compact through a
70
+ * Bedrock Runtime side request; their ordinary turns stay on Converse.
61
71
  */
62
- export function shouldUseAnthropicNativeCompaction(model: Model): model is Model<"anthropic-messages"> {
63
- return isAnthropicMessagesModel(model) && supportsAnthropicCompaction(model);
72
+ export function shouldUseAnthropicNativeCompaction(
73
+ model: Model,
74
+ ): model is Model<"anthropic-messages" | "bedrock-converse-stream"> {
75
+ if (isAnthropicMessagesModel(model)) return supportsAnthropicCompaction(model);
76
+ return isBedrockConverseModel(model) && supportsBedrockConverseCompaction(model);
64
77
  }
65
78
 
79
+ type AnthropicNativeCompactionModel = Model<"anthropic-messages" | "bedrock-converse-stream">;
80
+
66
81
  export function getPreservedAnthropicCompactionData(
67
82
  preserveData: Record<string, unknown> | undefined,
68
83
  ): AnthropicCompactionPreserveData | undefined {
@@ -70,6 +85,9 @@ export function getPreservedAnthropicCompactionData(
70
85
  if (!isRecord(candidate)) return undefined;
71
86
  if (typeof candidate.provider !== "string" || candidate.provider.length === 0) return undefined;
72
87
  if (typeof candidate.content !== "string" || candidate.content.length === 0) return undefined;
88
+ const toolChanges = candidate.toolChanges;
89
+ // The signature covers `tool_changes`: a block missing or mangling them cannot replay.
90
+ if (toolChanges !== undefined && (!Array.isArray(toolChanges) || !toolChanges.every(isRecord))) return undefined;
73
91
  return {
74
92
  provider: candidate.provider,
75
93
  content: candidate.content,
@@ -90,6 +108,7 @@ export function getPreservedAnthropicCompactionData(
90
108
  ),
91
109
  }
92
110
  : {}),
111
+ ...(toolChanges !== undefined ? { toolChanges } : {}),
93
112
  ...(candidate.exactTail === true ? { exactTail: true as const } : {}),
94
113
  ...(typeof candidate.model === "string" ? { model: candidate.model } : {}),
95
114
  ...(typeof candidate.usedTokens === "number" ? { usedTokens: candidate.usedTokens } : {}),
@@ -124,6 +143,7 @@ export function getAnthropicCompactionPayload(
124
143
  ...(preserved.signature ? { signature: preserved.signature } : {}),
125
144
  ...(preserved.encryptedContent ? { encryptedContent: preserved.encryptedContent } : {}),
126
145
  ...(preserved.filesText ? { filesText: preserved.filesText } : {}),
146
+ ...(preserved.toolChanges ? { toolChanges: preserved.toolChanges } : {}),
127
147
  ...(preserved.retainedFiles?.length ? { retainedFiles: preserved.retainedFiles } : {}),
128
148
  ...(preserved.exactTail ? { exactTail: true as const } : {}),
129
149
  };
@@ -196,6 +216,7 @@ export interface AnthropicNativeCompactionRequest {
196
216
  export interface AnthropicNativeCompactionResponse {
197
217
  content: string;
198
218
  signature: string;
219
+ toolChanges?: Record<string, unknown>[];
199
220
  usage: Usage;
200
221
  model: string;
201
222
  }
@@ -224,7 +245,7 @@ export interface AnthropicNativeCompactionOptions
224
245
  * output limits; the configured method order can then choose a fallback.
225
246
  */
226
247
  export async function requestAnthropicNativeCompaction(
227
- model: Model<"anthropic-messages">,
248
+ model: AnthropicNativeCompactionModel,
228
249
  apiKey: ApiKey,
229
250
  request: AnthropicNativeCompactionRequest,
230
251
  signal: AbortSignal | undefined,
@@ -237,7 +258,9 @@ export async function requestAnthropicNativeCompaction(
237
258
  apiKey,
238
259
  signal,
239
260
  maxTokens: request.maxTokens,
240
- reasoning: resolveAnthropicCompactionEffort(model, request.context.messages, request.reasoning),
261
+ reasoning: isAnthropicMessagesModel(model)
262
+ ? resolveAnthropicCompactionEffort(model, request.context.messages, request.reasoning)
263
+ : request.reasoning,
241
264
  initiatorOverride: options.initiatorOverride,
242
265
  metadata: options.metadata,
243
266
  fetch: options.fetch,
@@ -279,6 +302,7 @@ export async function requestAnthropicNativeCompaction(
279
302
  return {
280
303
  content: payload.content,
281
304
  signature: payload.signature,
305
+ ...(payload.toolChanges ? { toolChanges: payload.toolChanges } : {}),
282
306
  usage: response.usage,
283
307
  model: response.model,
284
308
  };
@@ -2024,6 +2024,7 @@ export async function compact(
2024
2024
  let nativeFirstKeptEntryId = firstKeptEntryId;
2025
2025
  let nativeUsedTokens: number | undefined;
2026
2026
  let nativeRetainedFiles: AnthropicCompactionFiles[] | undefined;
2027
+ let nativeToolChanges: Record<string, unknown>[] | undefined;
2027
2028
  if (!usedRemoteCompaction && settings.remoteEnabled !== false && shouldUseAnthropicNativeCompaction(model)) {
2028
2029
  const previousNative = getPreservedAnthropicCompactionData(previousPreserveData);
2029
2030
  // Lead with the previous summary as the live context renders it:
@@ -2052,6 +2053,7 @@ export async function compact(
2052
2053
  : {}),
2053
2054
  ...(previousNative.filesText ? { filesText: previousNative.filesText } : {}),
2054
2055
  ...(previousNative.retainedFiles ? { retainedFiles: previousNative.retainedFiles } : {}),
2056
+ ...(previousNative.toolChanges ? { toolChanges: previousNative.toolChanges } : {}),
2055
2057
  ...(previousNative.exactTail ? { exactTail: true as const } : {}),
2056
2058
  }
2057
2059
  : undefined,
@@ -2148,6 +2150,7 @@ export async function compact(
2148
2150
  );
2149
2151
  nativeSummary = remote.content;
2150
2152
  nativeSignature = remote.signature;
2153
+ nativeToolChanges = remote.toolChanges;
2151
2154
  nativeUsedTokens = calculatePromptTokens(remote.usage);
2152
2155
  usedRemoteCompaction = true;
2153
2156
  } catch (err) {
@@ -2235,16 +2238,18 @@ export async function compact(
2235
2238
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
2236
2239
  summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
2237
2240
  if (nativeSummary !== undefined) {
2238
- // The replayed block stays byte-identical to the API's summary so it
2239
- // matches its signature. The harness file lists above travel
2240
- // separately: the converter replaces the summary message with the
2241
- // block and skips its text, so they would otherwise be invisible to
2242
- // this provider. Every other provider keeps reading the entry text.
2241
+ // The replayed block (summary and any `tool_changes`) stays
2242
+ // byte-identical to the API's output so it matches its signature. The
2243
+ // harness file lists above travel separately: the converter replaces the
2244
+ // summary message with the block and skips its text, so they would
2245
+ // otherwise be invisible to this provider. Every other provider keeps
2246
+ // reading the entry text.
2243
2247
  const filesText = upsertFileOperations("", readFiles, modifiedFiles, fileOps.read) || undefined;
2244
2248
  preserveData = withAnthropicCompactionPreserveData(preserveData, {
2245
2249
  provider: model.provider,
2246
2250
  content: nativeSummary,
2247
2251
  ...(nativeSignature ? { signature: nativeSignature } : {}),
2252
+ ...(nativeToolChanges ? { toolChanges: nativeToolChanges } : {}),
2248
2253
  ...(filesText ? { filesText } : {}),
2249
2254
  ...(nativeRetainedFiles ? { retainedFiles: nativeRetainedFiles } : {}),
2250
2255
  exactTail: true,
@@ -12,9 +12,10 @@ export type ToolArgumentSource = "parsed" | "payload";
12
12
 
13
13
  /**
14
14
  * Validate a call's arguments against `tool`'s schema, honoring
15
- * {@link AgentTool.lenientArgValidation}: on a schema mismatch a lenient tool
16
- * receives its raw arguments minus the internal `__parseError`/`__rawJson`
17
- * sentinels. Malformed JSON from a provider stream is never lenient.
15
+ * {@link AgentTool.lenientArgValidation}: a lenient tool keeps unknown root
16
+ * keys its closed schema would heal away, and on a schema mismatch receives its
17
+ * raw arguments minus the internal `__parseError`/`__rawJson` sentinels.
18
+ * Malformed JSON from a provider stream is never lenient.
18
19
  *
19
20
  * @throws the validation error when the tool is strict or the arguments failed to parse.
20
21
  */
@@ -24,7 +25,9 @@ export function validateAgentToolArguments(
24
25
  source: ToolArgumentSource = "parsed",
25
26
  ): ToolCall["arguments"] {
26
27
  try {
27
- return validateToolArguments(tool, toolCall);
28
+ const validated = validateToolArguments(tool, toolCall, { keepUnknownRootKeys: tool.lenientArgValidation });
29
+ // Kept unknown root keys must not smuggle the internal sentinels past validation.
30
+ return tool.lenientArgValidation ? withoutSentinels(validated) : validated;
28
31
  } catch (error) {
29
32
  const args: unknown = toolCall.arguments;
30
33
  if (!tool.lenientArgValidation) throw error;
@@ -36,3 +39,12 @@ export function validateAgentToolArguments(
36
39
  return fallback;
37
40
  }
38
41
  }
42
+
43
+ function withoutSentinels(args: ToolCall["arguments"]): ToolCall["arguments"] {
44
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
45
+ if (!Object.hasOwn(args, "__parseError") && !Object.hasOwn(args, "__rawJson")) return args;
46
+ const stripped = { ...(args as Record<string, unknown>) };
47
+ delete stripped.__parseError;
48
+ delete stripped.__rawJson;
49
+ return stripped;
50
+ }
package/src/types.ts CHANGED
@@ -1170,7 +1170,7 @@ export interface AgentTool<
1170
1170
  * before ordinary dispatch commits its result.
1171
1171
  */
1172
1172
  speculation?: ToolSpeculationPolicy;
1173
- /** If true, schema validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM. Malformed argument JSON still returns the parse error. */
1173
+ /** If true, schema validation errors are non-fatal: raw args are passed to execute() instead of returning an error to the LLM, and unknown root keys a closed schema would drop stay on the args. Malformed argument JSON still returns the parse error. */
1174
1174
  lenientArgValidation?: boolean;
1175
1175
  /**
1176
1176
  * Whether the agent loop may abort this tool mid-execution — or skip it
@@ -1229,6 +1229,13 @@ export interface AgentTool<
1229
1229
  /** Lines appended after the standard approval prompt header. */
1230
1230
  formatApprovalDetails?: (args: unknown) => string | string[] | undefined;
1231
1231
 
1232
+ /**
1233
+ * Remembered-approval keys for `tools.approvalMode: always-ask-cached`: the files (or URL
1234
+ * targets) this call touches. When every key is already granted the call skips its prompt;
1235
+ * approving the call can record them. Omit it (or return `undefined`) and the call prompts by tier.
1236
+ */
1237
+ approvalCacheTargets?: (args: unknown) => readonly string[] | undefined;
1238
+
1232
1239
  /** The main execution callback for this tool. */
1233
1240
  execute: AgentToolExecFn<TParameters, TDetails, TTheme>;
1234
1241