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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tool-repair",
3
- "version": "0.1.5",
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",
@@ -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>): void {
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
- obj[newKey] = s.trim();
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
- value[i] = s.trim();
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) return;
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} ` +