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 +8 -2
- package/package.json +2 -2
- package/src/grammar-repair.ts +36 -7
- package/src/index.ts +2 -0
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
|
|
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.
|
|
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.
|
|
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",
|
package/src/grammar-repair.ts
CHANGED
|
@@ -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))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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"))
|
|
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