pi-tool-repair 0.1.5 → 0.1.7
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 +1 -1
- package/src/grammar-repair.ts +15 -0
- package/src/index.ts +18 -5
- package/tool-repair.ts +30 -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.7",
|
|
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",
|
package/src/grammar-repair.ts
CHANGED
|
@@ -156,6 +156,21 @@ export function repairAssistantMessageGrammarLeaks(
|
|
|
156
156
|
changed = true;
|
|
157
157
|
for (const candidate of candidates) {
|
|
158
158
|
if (candidate.stripOnly) continue;
|
|
159
|
+
|
|
160
|
+
// Safety: don't recover tool calls whose grammar parsed to an empty
|
|
161
|
+
// argument object. These are almost always malformed GLM/XML fragments
|
|
162
|
+
// (e.g. `<tool_call>write</tool_call>`) that would otherwise crash as a
|
|
163
|
+
// real tool invocation with missing required properties.
|
|
164
|
+
if (Object.keys(candidate.arguments).length === 0) {
|
|
165
|
+
if (config.debug) {
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
`[pi-tool-repair] grammar-repair skipping ${candidate.grammar}:${candidate.name} ` +
|
|
168
|
+
"(empty arguments, likely malformed grammar)\n",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
159
174
|
recoveredCalls.push({
|
|
160
175
|
name: candidate.name,
|
|
161
176
|
arguments: candidate.arguments,
|
package/src/index.ts
CHANGED
|
@@ -141,7 +141,8 @@ const GRAMMAR_TOKEN_LEAKS = [
|
|
|
141
141
|
{ tag: "</arg_value>", at: "end" as const },
|
|
142
142
|
];
|
|
143
143
|
|
|
144
|
-
export function stripGrammarTokenLeaksInPlace(obj: Record<string, unknown>):
|
|
144
|
+
export function stripGrammarTokenLeaksInPlace(obj: Record<string, unknown>): boolean {
|
|
145
|
+
let changed = false;
|
|
145
146
|
for (const key of Object.keys(obj)) {
|
|
146
147
|
const value = obj[key];
|
|
147
148
|
let newKey = key;
|
|
@@ -157,6 +158,7 @@ export function stripGrammarTokenLeaksInPlace(obj: Record<string, unknown>): voi
|
|
|
157
158
|
if (newKey !== key) {
|
|
158
159
|
obj[newKey] = value;
|
|
159
160
|
delete obj[key];
|
|
161
|
+
changed = true;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
if (typeof value === "string") {
|
|
@@ -168,7 +170,11 @@ export function stripGrammarTokenLeaksInPlace(obj: Record<string, unknown>): voi
|
|
|
168
170
|
s = s.slice(0, -tag.length);
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
|
-
|
|
173
|
+
const trimmed = s.trim();
|
|
174
|
+
if (trimmed !== value) {
|
|
175
|
+
obj[newKey] = trimmed;
|
|
176
|
+
changed = true;
|
|
177
|
+
}
|
|
172
178
|
} else if (Array.isArray(value)) {
|
|
173
179
|
for (let i = 0; i < value.length; i++) {
|
|
174
180
|
const item = value[i];
|
|
@@ -181,15 +187,22 @@ export function stripGrammarTokenLeaksInPlace(obj: Record<string, unknown>): voi
|
|
|
181
187
|
s = s.slice(0, -tag.length);
|
|
182
188
|
}
|
|
183
189
|
}
|
|
184
|
-
|
|
190
|
+
const trimmed = s.trim();
|
|
191
|
+
if (trimmed !== item) {
|
|
192
|
+
value[i] = trimmed;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
185
195
|
} else if (item && typeof item === "object") {
|
|
186
|
-
stripGrammarTokenLeaksInPlace(item as Record<string, unknown>);
|
|
196
|
+
const nestedChanged = stripGrammarTokenLeaksInPlace(item as Record<string, unknown>);
|
|
197
|
+
if (nestedChanged) changed = true;
|
|
187
198
|
}
|
|
188
199
|
}
|
|
189
200
|
} else if (value && typeof value === "object") {
|
|
190
|
-
stripGrammarTokenLeaksInPlace(value as Record<string, unknown>);
|
|
201
|
+
const nestedChanged = stripGrammarTokenLeaksInPlace(value as Record<string, unknown>);
|
|
202
|
+
if (nestedChanged) changed = true;
|
|
191
203
|
}
|
|
192
204
|
}
|
|
205
|
+
return changed;
|
|
193
206
|
}
|
|
194
207
|
|
|
195
208
|
// ─── Phase 2: Repair Rules ────────────────────────────────────────────────────
|
package/tool-repair.ts
CHANGED
|
@@ -134,8 +134,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
134
134
|
pi.on("message_end", (event) => {
|
|
135
135
|
if (event.message.role !== "assistant") return;
|
|
136
136
|
|
|
137
|
-
// Phase 1.5: Phantom toolUse normalization (always-on)
|
|
138
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)
|
|
139
162
|
const phantomResult = normalizePhantomToolUse(currentMessage);
|
|
140
163
|
|
|
141
164
|
if (phantomResult.changed) {
|
|
@@ -174,7 +197,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
174
197
|
}
|
|
175
198
|
|
|
176
199
|
// Phase 1: Grammar leak repair (opt-in)
|
|
177
|
-
if (!grammarRepairConfig.enabled)
|
|
200
|
+
if (!grammarRepairConfig.enabled) {
|
|
201
|
+
if (toolCallArgsChanged) return { message: currentMessage as any };
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
178
204
|
|
|
179
205
|
const knownTools = new Set(
|
|
180
206
|
safeGetActiveTools(pi)
|
|
@@ -187,9 +213,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
187
213
|
knownTools,
|
|
188
214
|
);
|
|
189
215
|
|
|
190
|
-
if (!result.changed) return;
|
|
216
|
+
if (!result.changed && !toolCallArgsChanged) return;
|
|
191
217
|
|
|
192
|
-
if (grammarRepairConfig.debug) {
|
|
218
|
+
if (grammarRepairConfig.debug && result.changed) {
|
|
193
219
|
const calls = result.recoveredCalls.map((call) => `${call.grammar}:${call.name}`).join(",") || "none";
|
|
194
220
|
process.stderr.write(
|
|
195
221
|
`[pi-tool-repair] grammar-repair mode=${grammarRepairConfig.mode} ` +
|