pi-tool-repair 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -36,6 +36,12 @@ Reverse-engineered from [Command Code](https://commandcode.ai/)'s tool parsing p
36
36
 
37
37
  **With `pi install`** (recommended):
38
38
 
39
+ ```bash
40
+ pi install npm:pi-tool-repair
41
+ ```
42
+
43
+ Or install from GitHub:
44
+
39
45
  ```bash
40
46
  pi install https://github.com/monotykamary/pi-tool-repair
41
47
  ```
@@ -43,7 +49,7 @@ pi install https://github.com/monotykamary/pi-tool-repair
43
49
  **With npm**:
44
50
 
45
51
  ```bash
46
- npm install pi-tool-repair
52
+ npm install npm:pi-tool-repair
47
53
  ```
48
54
 
49
55
  **Manual** — add to `~/.pi/agent/settings.json`:
@@ -162,7 +168,7 @@ Safety gates:
162
168
 
163
169
  - `requireKnownTool: true` only recovers calls whose name is in pi's active tool registry.
164
170
  - Markup inside fenced code blocks is ignored so syntax discussions and examples are preserved.
165
- - Incomplete or unparseable blocks are left alone.
171
+ - Incomplete or unparseable blocks are not recovered as tool calls. For DSML, dangling or truncated marker tokens (e.g. a stream that died at `<|DSML|tool_calls` with no closing `>`) are still stripped from visible text so the raw marker doesn't persist in the transcript.
166
172
  - If the provider already emitted native `toolCall` blocks, leaked shadow text is stripped but duplicate calls are not added.
167
173
 
168
174
  Covered grammar families: DeepSeek DSML, MiniMax/Anthropic `<invoke>`, Qwen/Hermes `<tool_call>`, Kimi sentinels, Mistral `[TOOL_CALLS]`, Llama `<|python_tag|>`, GLM `arg_key`/`arg_value`, Granite JSON `<tool_call>`, MiniMax-Text-01 TypeScript calls, and OLMo3 `<function_calls>` pythonic calls. See [`docs/tool-call-grammar-leakage-survey.md`](./docs/tool-call-grammar-leakage-survey.md) for the survey.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tool-repair",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Validate-then-repair extension for pi — fixes common LLM tool-call mistakes (null fields, stringified arrays, wrong field names, anchor bleed) before tools execute",
5
5
  "type": "module",
6
6
  "author": "Tom X Nguyen",
@@ -41,7 +41,7 @@
41
41
  "lint:dead": "knip --no-gitignore"
42
42
  },
43
43
  "devDependencies": {
44
- "@earendil-works/pi-coding-agent": "0.75.4",
44
+ "@earendil-works/pi-coding-agent": "0.79.4",
45
45
  "@types/node": "25.9.1",
46
46
  "@vitest/coverage-v8": "4.1.7",
47
47
  "knip": "6.14.1",
@@ -38,6 +38,7 @@ export interface RecoveredToolCall {
38
38
 
39
39
  interface Candidate extends RecoveredToolCall {
40
40
  range: Range;
41
+ stripOnly?: boolean;
41
42
  }
42
43
 
43
44
  interface Range {
@@ -147,13 +148,14 @@ export function repairAssistantMessageGrammarLeaks(
147
148
  if (text === undefined) return part;
148
149
 
149
150
  const candidates = selectCandidates(parseToolGrammarCandidates(text, enabled))
150
- .filter((candidate) => isAllowedTool(candidate.name, config, knownTools));
151
+ .filter((candidate) => candidate.stripOnly || isAllowedTool(candidate.name, config, knownTools));
151
152
 
152
153
  if (candidates.length === 0) return part;
153
154
 
154
155
  strippedRanges += candidates.length;
155
156
  changed = true;
156
157
  for (const candidate of candidates) {
158
+ if (candidate.stripOnly) continue;
157
159
  recoveredCalls.push({
158
160
  name: candidate.name,
159
161
  arguments: candidate.arguments,
@@ -201,16 +203,21 @@ export function repairAssistantMessageGrammarLeaks(
201
203
 
202
204
  export function parseToolGrammarLeaks(text: string, grammars: Iterable<GrammarName> = ALL_GRAMMARS): RecoveredToolCall[] {
203
205
  const enabled = new Set(grammars);
204
- return selectCandidates(parseToolGrammarCandidates(text, enabled)).map((candidate) => ({
205
- name: candidate.name,
206
- arguments: candidate.arguments,
207
- grammar: candidate.grammar,
208
- }));
206
+ return selectCandidates(parseToolGrammarCandidates(text, enabled))
207
+ .filter((candidate) => !candidate.stripOnly)
208
+ .map((candidate) => ({
209
+ name: candidate.name,
210
+ arguments: candidate.arguments,
211
+ grammar: candidate.grammar,
212
+ }));
209
213
  }
210
214
 
211
215
  function parseToolGrammarCandidates(text: string, enabled: Set<GrammarName>): Candidate[] {
212
216
  const candidates: Candidate[] = [];
213
- if (enabled.has("dsml")) candidates.push(...parseDsml(text));
217
+ if (enabled.has("dsml")) {
218
+ candidates.push(...parseDsml(text));
219
+ candidates.push(...parseDsmlDanglingMarkers(text));
220
+ }
214
221
  if (enabled.has("kimi")) candidates.push(...parseKimi(text));
215
222
  if (enabled.has("mistral")) {
216
223
  candidates.push(...parseMistral(text));
@@ -252,6 +259,28 @@ function parseDsml(text: string): Candidate[] {
252
259
  return candidates;
253
260
  }
254
261
 
262
+ function parseDsmlDanglingMarkers(text: string): Candidate[] {
263
+ if (!text.includes("DSML")) return [];
264
+ const prefix = "(?:|{1,2}DSML|{1,2}|DSML||\\s*\\|\\s*DSML\\s*\\|\\s*)";
265
+ const markerRe = new RegExp(
266
+ `</?${prefix}(?:tool_calls|function_calls|invoke|parameter)(?:\\s+[^>\\n]*)?>?`,
267
+ "giu",
268
+ );
269
+ const candidates: Candidate[] = [];
270
+ for (const match of text.matchAll(markerRe)) {
271
+ if (match.index === undefined) continue;
272
+ if (isInsideCodeFence(text, match.index)) continue;
273
+ candidates.push({
274
+ name: "",
275
+ arguments: {},
276
+ grammar: "dsml",
277
+ range: { start: match.index, end: match.index + match[0].length },
278
+ stripOnly: true,
279
+ });
280
+ }
281
+ return candidates;
282
+ }
283
+
255
284
  function findDsmlClose(text: string, from: number, outerName: string): Range | undefined {
256
285
  const prefix = "(?:|{1,2}DSML|{1,2}|DSML||\\s*\\|\\s*DSML\\s*\\|\\s*)";
257
286
  const closeRe = new RegExp(`</${prefix}${outerName}>`, "giu");
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  // ─── Configuration ────────────────────────────────────────────────────────────
9
9
 
10
+ import type { MinimalAssistantMessage } from "./grammar-repair.js";
11
+
10
12
  export interface RepairConfig {
11
13
  debug: boolean;
12
14
  anchorBleedModels: RegExp[];