@oh-my-pi/pi-agent-core 18.12.1 → 18.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.12.3] - 2026-10-19
6
+
7
+ ### Added
8
+
9
+ - `AgentTool` gained the optional `approvalCacheTargets` hook, which the `always-ask-cached` approval mode uses to check and record per-file grants ([#16049](https://github.com/can1357/oh-my-pi/pull/16049) by [@Peribunt](https://github.com/Peribunt))
10
+
11
+ ### Changed
12
+
13
+ - Tools with `lenientArgValidation` now receive unknown root arguments that their closed schema would otherwise drop, so they can reject them ([#16317](https://github.com/can1357/oh-my-pi/issues/16317))
14
+
15
+ ## [18.12.2] - 2026-10-18
16
+
17
+ ### Changed
18
+
19
+ - The failed-tool-call correction sent to DeepSeek models now also covers chat-template tool-call tokens written out as text, not only malformed DSML ([#16216](https://github.com/can1357/oh-my-pi/pull/16216) by [@H4vC](https://github.com/H4vC))
20
+
21
+ ### Fixed
22
+
23
+ - Fixed Ollama chat history estimates counting stripped native thinking and missing foreign thinking replayed as text, which skewed compaction after model switches ([#16164](https://github.com/can1357/oh-my-pi/issues/16164))
24
+
5
25
  ## [18.12.0] - 2026-10-17
6
26
 
7
27
  ### Fixed
@@ -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": "@oh-my-pi/pi-agent-core",
4
- "version": "18.12.1",
4
+ "version": "18.12.3",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -38,16 +38,16 @@
38
38
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
39
39
  },
40
40
  "dependencies": {
41
- "@oh-my-pi/pi-ai": "18.12.1",
42
- "@oh-my-pi/pi-catalog": "18.12.1",
43
- "@oh-my-pi/pi-natives": "18.12.1",
44
- "@oh-my-pi/pi-utils": "18.12.1",
45
- "@oh-my-pi/pi-wire": "18.12.1",
46
- "@oh-my-pi/snapcompact": "18.12.1",
41
+ "@oh-my-pi/pi-ai": "18.12.3",
42
+ "@oh-my-pi/pi-catalog": "18.12.3",
43
+ "@oh-my-pi/pi-natives": "18.12.3",
44
+ "@oh-my-pi/pi-utils": "18.12.3",
45
+ "@oh-my-pi/pi-wire": "18.12.3",
46
+ "@oh-my-pi/snapcompact": "18.12.3",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.12.1",
50
+ "@oh-my-pi/omptype": "18.12.3",
51
51
  "@opentelemetry/context-async-hooks": "^2.9.0",
52
52
  "@opentelemetry/sdk-trace-base": "^2.9.0",
53
53
  "@types/bun": "^1.3.14"
@@ -1,4 +1,4 @@
1
- Tool call failed: your previous response tried to invoke a tool, but the DSML markup was malformed, so it arrived as plain text, no tool was executed, and the broken markup was removed from your response. Usually the opening `<|DSML|tool_calls>` and/or `<|DSML|invoke name="…">` tags are missing, or the tool name was written as a plain line.
1
+ Tool call failed: your previous response tried to invoke a tool, but the tool-call markup was malformed, so it arrived as plain text, no tool was executed, and the broken markup was removed from your response. Usually the opening `<|DSML|tool_calls>` and/or `<|DSML|invoke name="…">` tags are missing, the tool name was written as a plain line, or the chat template's tool-call special tokens were typed out as text.
2
2
 
3
3
  A tool call must be one complete envelope:
4
4
 
package/src/tokenizer.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
+ import { renderDemotedThinking } from "@oh-my-pi/pi-ai/dialect/demotion";
3
+ import { ollamaChatDropsOwnThinking } from "@oh-my-pi/pi-ai/providers/ollama";
2
4
  import { isColdReplayableResponsesTurn } from "@oh-my-pi/pi-ai/providers/openai-shared";
3
- import { shouldDropTruncatedThinkingOnlyAssistant } from "@oh-my-pi/pi-ai/providers/transform-messages";
5
+ import {
6
+ isSameModelTurn,
7
+ shouldDropTruncatedThinkingOnlyAssistant,
8
+ } from "@oh-my-pi/pi-ai/providers/transform-messages";
4
9
  import {
5
10
  getOpenAIResponsesHistoryItems,
6
11
  sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
@@ -207,6 +212,7 @@ export class Tokenizer {
207
212
  readonly #frameBilling: snapcompact.FrameBilling;
208
213
  readonly frameBillingKey: string;
209
214
  readonly #reasoningReader: ReasoningReplayReader | undefined;
215
+ readonly #dropsOllamaThinking: boolean;
210
216
  /** Claude's own API and prefix-bound hosts keep another model's signature from their deployment. */
211
217
  readonly #readsDeploymentSignatures: boolean;
212
218
  readonly reasoningReplayKey: string;
@@ -244,6 +250,7 @@ export class Tokenizer {
244
250
  this.#frameBilling = snapcompact.frameBilling(model ?? undefined);
245
251
  this.frameBillingKey = snapcompact.frameBillingKey(this.#frameBilling);
246
252
  this.#reasoningReader = reasoningReplayReader(model);
253
+ this.#dropsOllamaThinking = ollamaChatDropsOwnThinking(model);
247
254
  const compat = model?.compat;
248
255
  this.#readsDeploymentSignatures =
249
256
  model?.api === "anthropic-messages" &&
@@ -415,6 +422,8 @@ export class Tokenizer {
415
422
  // reaches the wire then (the payload items replay instead); a turn whose
416
423
  // backend reported no reasoning tokens keeps the signature as its price.
417
424
  const replayedReasoning = this.#replayedReasoningTokens(message);
425
+ const ollamaReader = this.#reasoningReader?.api === "ollama-chat" ? this.#reasoningReader : undefined;
426
+ const ownOllamaTurn = ollamaReader !== undefined && isSameModelTurn(message, ollamaReader);
418
427
  if (typeof replayedReasoning === "number") extra += replayedReasoning;
419
428
  const signaturesPriced = this.thinkingReplayTokens === "signature";
420
429
  // The replay's signature rule (`transformMessages`): an aborted or
@@ -441,6 +450,13 @@ export class Tokenizer {
441
450
  if (block.type === "text") {
442
451
  fragments.push(block.text);
443
452
  } else if (block.type === "thinking") {
453
+ // `transformMessages` drops another model's empty thinking and demotes the
454
+ // rest to visible text; only the reader's own thinking stays native.
455
+ if (ollamaReader && !ownOllamaTurn) {
456
+ if (block.thinking.trim()) fragments.push(renderDemotedThinking(ollamaReader.id, block.thinking));
457
+ continue;
458
+ }
459
+ if (this.#dropsOllamaThinking && ownOllamaTurn) continue;
444
460
  fragments.push(block.thinking);
445
461
  // Providers charge for the opaque signature/reasoning payload that
446
462
  // rides alongside the thinking text (OpenAI Responses encrypted
@@ -561,6 +577,7 @@ export class Tokenizer {
561
577
  snapcompact.frameBillingKey(snapcompact.frameBilling(model ?? undefined)) === this.frameBillingKey &&
562
578
  reasoningReplayKey(model) === this.reasoningReplayKey &&
563
579
  resolveThinkingReplayTokens(model ?? {}) === this.thinkingReplayTokens &&
580
+ ollamaChatDropsOwnThinking(model) === this.#dropsOllamaThinking &&
564
581
  (options?.nativeHistoryReplay ?? true) === this.nativeHistoryReplay
565
582
  );
566
583
  }
@@ -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