pi-tool-repair 0.1.3 → 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
@@ -168,7 +168,7 @@ Safety gates:
168
168
 
169
169
  - `requireKnownTool: true` only recovers calls whose name is in pi's active tool registry.
170
170
  - Markup inside fenced code blocks is ignored so syntax discussions and examples are preserved.
171
- - 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.
172
172
  - If the provider already emitted native `toolCall` blocks, leaked shadow text is stripped but duplicate calls are not added.
173
173
 
174
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,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-tool-repair",
3
- "version": "0.1.3",
4
- "description": "Validate-then-repair extension for pi \u2014 fixes common LLM tool-call mistakes (null fields, stringified arrays, wrong field names, anchor bleed) before tools execute",
3
+ "version": "0.1.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",
7
7
  "license": "MIT",
@@ -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");