pi-tool-repair 0.1.4 → 0.1.6
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/package.json +9 -9
- package/src/index.ts +74 -0
- package/tool-repair.ts +37 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tool-repair",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
@@ -33,13 +33,6 @@
|
|
|
33
33
|
"docs/",
|
|
34
34
|
"README.md"
|
|
35
35
|
],
|
|
36
|
-
"scripts": {
|
|
37
|
-
"test": "vitest run",
|
|
38
|
-
"test:watch": "vitest",
|
|
39
|
-
"test:coverage": "vitest run --coverage",
|
|
40
|
-
"typecheck": "tsc --noEmit",
|
|
41
|
-
"lint:dead": "knip --no-gitignore"
|
|
42
|
-
},
|
|
43
36
|
"devDependencies": {
|
|
44
37
|
"@earendil-works/pi-coding-agent": "0.79.4",
|
|
45
38
|
"@types/node": "25.9.1",
|
|
@@ -52,5 +45,12 @@
|
|
|
52
45
|
"extensions": [
|
|
53
46
|
"./tool-repair.ts"
|
|
54
47
|
]
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"test:coverage": "vitest run --coverage",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"lint:dead": "knip --no-gitignore"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -131,6 +131,80 @@ export function stripAnchorBleedInPlace(obj: Record<string, unknown>): void {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Leaked grammar markers from GLM/ChatGLM style tool-call grammars.
|
|
135
|
+
// These can end up as literal prefixes/suffixes on parsed object keys
|
|
136
|
+
// or string values instead of being interpreted as XML tags.
|
|
137
|
+
const GRAMMAR_TOKEN_LEAKS = [
|
|
138
|
+
{ tag: "<arg_key>", at: "start" as const },
|
|
139
|
+
{ tag: "</arg_key>", at: "end" as const },
|
|
140
|
+
{ tag: "<arg_value>", at: "start" as const },
|
|
141
|
+
{ tag: "</arg_value>", at: "end" as const },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
export function stripGrammarTokenLeaksInPlace(obj: Record<string, unknown>): boolean {
|
|
145
|
+
let changed = false;
|
|
146
|
+
for (const key of Object.keys(obj)) {
|
|
147
|
+
const value = obj[key];
|
|
148
|
+
let newKey = key;
|
|
149
|
+
for (const { tag, at } of GRAMMAR_TOKEN_LEAKS) {
|
|
150
|
+
if (at === "start" && newKey.startsWith(tag)) {
|
|
151
|
+
newKey = newKey.slice(tag.length);
|
|
152
|
+
} else if (at === "end" && newKey.endsWith(tag)) {
|
|
153
|
+
newKey = newKey.slice(0, -tag.length);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
newKey = newKey.trim();
|
|
157
|
+
|
|
158
|
+
if (newKey !== key) {
|
|
159
|
+
obj[newKey] = value;
|
|
160
|
+
delete obj[key];
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof value === "string") {
|
|
165
|
+
let s = value;
|
|
166
|
+
for (const { tag, at } of GRAMMAR_TOKEN_LEAKS) {
|
|
167
|
+
if (at === "start" && s.startsWith(tag)) {
|
|
168
|
+
s = s.slice(tag.length);
|
|
169
|
+
} else if (at === "end" && s.endsWith(tag)) {
|
|
170
|
+
s = s.slice(0, -tag.length);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const trimmed = s.trim();
|
|
174
|
+
if (trimmed !== value) {
|
|
175
|
+
obj[newKey] = trimmed;
|
|
176
|
+
changed = true;
|
|
177
|
+
}
|
|
178
|
+
} else if (Array.isArray(value)) {
|
|
179
|
+
for (let i = 0; i < value.length; i++) {
|
|
180
|
+
const item = value[i];
|
|
181
|
+
if (typeof item === "string") {
|
|
182
|
+
let s = item;
|
|
183
|
+
for (const { tag, at } of GRAMMAR_TOKEN_LEAKS) {
|
|
184
|
+
if (at === "start" && s.startsWith(tag)) {
|
|
185
|
+
s = s.slice(tag.length);
|
|
186
|
+
} else if (at === "end" && s.endsWith(tag)) {
|
|
187
|
+
s = s.slice(0, -tag.length);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const trimmed = s.trim();
|
|
191
|
+
if (trimmed !== item) {
|
|
192
|
+
value[i] = trimmed;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
} else if (item && typeof item === "object") {
|
|
196
|
+
const nestedChanged = stripGrammarTokenLeaksInPlace(item as Record<string, unknown>);
|
|
197
|
+
if (nestedChanged) changed = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else if (value && typeof value === "object") {
|
|
201
|
+
const nestedChanged = stripGrammarTokenLeaksInPlace(value as Record<string, unknown>);
|
|
202
|
+
if (nestedChanged) changed = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return changed;
|
|
206
|
+
}
|
|
207
|
+
|
|
134
208
|
// ─── Phase 2: Repair Rules ────────────────────────────────────────────────────
|
|
135
209
|
|
|
136
210
|
export interface RepairResult {
|
package/tool-repair.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
hasAnchorBleedBug,
|
|
46
46
|
sanitizeSchemaAnchors,
|
|
47
47
|
stripAnchorBleedInPlace,
|
|
48
|
+
stripGrammarTokenLeaksInPlace,
|
|
48
49
|
wrapRootStringAsObject,
|
|
49
50
|
repairToolInput,
|
|
50
51
|
validateAgainstSchema,
|
|
@@ -133,8 +134,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
133
134
|
pi.on("message_end", (event) => {
|
|
134
135
|
if (event.message.role !== "assistant") return;
|
|
135
136
|
|
|
136
|
-
// Phase 1.5: Phantom toolUse normalization (always-on)
|
|
137
137
|
const currentMessage = event.message as unknown as MinimalAssistantMessage;
|
|
138
|
+
|
|
139
|
+
// Phase 1.4: Strip leaked grammar tokens from existing pi toolCall blocks.
|
|
140
|
+
// These can leak from GLM-style grammars straight into the parsed argument
|
|
141
|
+
// keys (e.g. "<arg_key>command" instead of "command"). We have to repair
|
|
142
|
+
// them here, before pi preflights and validates the tool calls.
|
|
143
|
+
let toolCallArgsChanged = false;
|
|
144
|
+
if (Array.isArray(currentMessage.content)) {
|
|
145
|
+
for (const part of currentMessage.content) {
|
|
146
|
+
if (
|
|
147
|
+
part && typeof part === "object" && !Array.isArray(part) &&
|
|
148
|
+
(part as Record<string, unknown>).type === "toolCall"
|
|
149
|
+
) {
|
|
150
|
+
const args = (part as Record<string, unknown>).arguments;
|
|
151
|
+
if (
|
|
152
|
+
args && typeof args === "object" && !Array.isArray(args) &&
|
|
153
|
+
stripGrammarTokenLeaksInPlace(args as Record<string, unknown>)
|
|
154
|
+
) {
|
|
155
|
+
toolCallArgsChanged = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Phase 1.5: Phantom toolUse normalization (always-on)
|
|
138
162
|
const phantomResult = normalizePhantomToolUse(currentMessage);
|
|
139
163
|
|
|
140
164
|
if (phantomResult.changed) {
|
|
@@ -173,7 +197,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
// Phase 1: Grammar leak repair (opt-in)
|
|
176
|
-
if (!grammarRepairConfig.enabled)
|
|
200
|
+
if (!grammarRepairConfig.enabled) {
|
|
201
|
+
if (toolCallArgsChanged) return { message: currentMessage as any };
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
177
204
|
|
|
178
205
|
const knownTools = new Set(
|
|
179
206
|
safeGetActiveTools(pi)
|
|
@@ -186,9 +213,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
186
213
|
knownTools,
|
|
187
214
|
);
|
|
188
215
|
|
|
189
|
-
if (!result.changed) return;
|
|
216
|
+
if (!result.changed && !toolCallArgsChanged) return;
|
|
190
217
|
|
|
191
|
-
if (grammarRepairConfig.debug) {
|
|
218
|
+
if (grammarRepairConfig.debug && result.changed) {
|
|
192
219
|
const calls = result.recoveredCalls.map((call) => `${call.grammar}:${call.name}`).join(",") || "none";
|
|
193
220
|
process.stderr.write(
|
|
194
221
|
`[pi-tool-repair] grammar-repair mode=${grammarRepairConfig.mode} ` +
|
|
@@ -213,6 +240,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
240
|
}
|
|
214
241
|
}
|
|
215
242
|
|
|
243
|
+
// Defense-in-depth: strip leaked grammar tokens (e.g. GLM <arg_key>) from
|
|
244
|
+
// parsed tool-call keys/values before the schema sees them.
|
|
245
|
+
if (input && typeof input === "object") {
|
|
246
|
+
stripGrammarTokenLeaksInPlace(input as Record<string, unknown>);
|
|
247
|
+
}
|
|
248
|
+
|
|
216
249
|
// Only repair built-in tools we have schemas for
|
|
217
250
|
const schema = BUILTIN_SCHEMAS[toolName];
|
|
218
251
|
if (!schema) return;
|