pi-tool-repair 0.3.6 → 0.3.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/README.md +3 -0
- package/package.json +1 -1
- package/src/grammar-repair.ts +25 -14
- package/src/index.ts +56 -2
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ Run `bun run test:host` for the offline real-host load, native codemode/nested-c
|
|
|
38
38
|
| Bare string as root input | `"/path/to/file"` | `{"path":"/path/to/file"}` |
|
|
39
39
|
| `fabric_exec` code arrays | `{"code":["const x=1;","return x;"]}` | newline-joined `code` |
|
|
40
40
|
| Schema anchor bleed (Kimi K2) | `"^pattern$"` in values | `"pattern"` |
|
|
41
|
+
| Index-suffixed tool names | `bash_1_234456789`, `functions.bash:0` | `bash` |
|
|
41
42
|
| Leaked tool grammar (opt-in) | `<|DSML|tool_calls>...` | pi `toolCall` block |
|
|
42
43
|
| Phantom tool use | `stopReason:"toolUse"` with no call | retryable error |
|
|
43
44
|
|
|
@@ -88,6 +89,7 @@ before_provider_request
|
|
|
88
89
|
model response → message_end
|
|
89
90
|
├─ strip leaked grammar tokens from native toolCall blocks
|
|
90
91
|
├─ recover complete leaked grammar calls when enabled
|
|
92
|
+
├─ recover index-suffixed tool names that match exactly one active tool
|
|
91
93
|
├─ validate raw arguments against each active tool's live schema
|
|
92
94
|
├─ apply only known aliases and schema-directed repairs
|
|
93
95
|
├─ commit a candidate only when it re-validates
|
|
@@ -111,6 +113,7 @@ Pi 0.84 validates tool arguments before emitting `tool_call`. Repair therefore r
|
|
|
111
113
|
| `coerceNumericString` | `"20"` → `20` when the live schema expects a number |
|
|
112
114
|
| `convertTimeoutMilliseconds` | `timeoutMs` → `timeout` seconds for `bash` |
|
|
113
115
|
| `joinStringArray` | all-string `fabric_exec.code` arrays → one newline-joined string |
|
|
116
|
+
| `recoverMangledToolName` | `bash_1_234456789` / `functions.bash:0` → `bash` when the name is not an active tool, the suffix is only digits and separators, exactly one active tool fits, and the arguments validate against it |
|
|
114
117
|
|
|
115
118
|
### Why validate-then-repair
|
|
116
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tool-repair",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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
|
@@ -170,24 +170,35 @@ export function resolveGrammarRepairForModel(
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
// Open models occasionally append index tokens to tool names (for example
|
|
173
|
-
// `bash_1_234456789` for `bash`)
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
// `bash_1_234456789` for `bash`), or leak Kimi's call-id shape
|
|
174
|
+
// `functions.{name}:{idx}` into the name. Recover the original name only when
|
|
175
|
+
// the trailing part is made up of separators and digits and exactly one known
|
|
176
|
+
// tool fits, so `read_file_1_2` never collapses to `read` and `s3_2_1` is not
|
|
177
|
+
// guessed when both `s3` and `s3_2` exist. Names that are already known, or
|
|
178
|
+
// whose suffix contains real characters (`read_multiple_files`), are returned
|
|
179
|
+
// untouched.
|
|
180
|
+
const MANGLED_TOOL_PREFIX = "functions.";
|
|
181
|
+
const MANGLED_TOOL_SUFFIX = /^[-_:\d]+$/;
|
|
182
|
+
|
|
183
|
+
export function recoverMangledToolName(name: string, knownTools: ReadonlySet<string>): string {
|
|
181
184
|
if (knownTools.has(name)) return name;
|
|
182
185
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
const bases = name.startsWith(MANGLED_TOOL_PREFIX)
|
|
187
|
+
? [name, name.slice(MANGLED_TOOL_PREFIX.length)]
|
|
188
|
+
: [name];
|
|
189
|
+
const matches = new Set<string>();
|
|
190
|
+
for (const base of bases) {
|
|
191
|
+
if (knownTools.has(base)) {
|
|
192
|
+
matches.add(base);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (const tool of knownTools) {
|
|
196
|
+
if (tool.length >= base.length || !base.startsWith(tool)) continue;
|
|
197
|
+
if (MANGLED_TOOL_SUFFIX.test(base.slice(tool.length))) matches.add(tool);
|
|
198
|
+
}
|
|
188
199
|
}
|
|
189
200
|
|
|
190
|
-
return
|
|
201
|
+
return matches.size === 1 ? [...matches][0]! : name;
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
export function repairAssistantMessageGrammarLeaks(
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
// ─── Configuration ────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { recoverMangledToolName, type MinimalAssistantMessage } from "./grammar-repair.js";
|
|
11
11
|
import { Value } from "typebox/value";
|
|
12
12
|
|
|
13
13
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -915,6 +915,52 @@ export function repairInputAgainstLiveSchema(
|
|
|
915
915
|
};
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
+
// A native call whose name is not an active tool fails with "tool not found".
|
|
919
|
+
// When the name is an active tool plus an index suffix (`bash_1_234456789`,
|
|
920
|
+
// `functions.bash:0`), rename it — but only if the arguments validate against
|
|
921
|
+
// that tool's schema, directly or after the usual argument repairs. Otherwise
|
|
922
|
+
// the call is left as-is so Pi reports the original error.
|
|
923
|
+
function recoverMangledToolCallName(
|
|
924
|
+
name: string,
|
|
925
|
+
rawInput: unknown,
|
|
926
|
+
schemas: ReadonlyMap<string, import("typebox").TSchema>,
|
|
927
|
+
knownTools: ReadonlySet<string>,
|
|
928
|
+
): LiveToolRepairOutcome | undefined {
|
|
929
|
+
const toolName = recoverMangledToolName(name, knownTools);
|
|
930
|
+
const schema = toolName === name ? undefined : schemas.get(toolName);
|
|
931
|
+
if (!schema) return undefined;
|
|
932
|
+
|
|
933
|
+
const hint = `Renamed tool "${name}" to "${toolName}"; the trailing index is not part of the tool name.`;
|
|
934
|
+
const outcome = repairInputAgainstLiveSchema(toolName, rawInput, schema);
|
|
935
|
+
const repaired = outcome.status === "recovered"
|
|
936
|
+
? outcome.repaired
|
|
937
|
+
: outcome.status === "unchanged" && schemaCheck(schema, rawInput) === true
|
|
938
|
+
? rawInput
|
|
939
|
+
: undefined;
|
|
940
|
+
|
|
941
|
+
if (repaired === undefined) {
|
|
942
|
+
return {
|
|
943
|
+
toolName: name,
|
|
944
|
+
status: "unrepairable",
|
|
945
|
+
input: rawInput,
|
|
946
|
+
rulesFired: ["recoverMangledToolName", ...outcome.rulesFired],
|
|
947
|
+
hints: [
|
|
948
|
+
`${hint} Not applied: the arguments do not validate against "${toolName}".`,
|
|
949
|
+
...outcome.hints,
|
|
950
|
+
],
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return {
|
|
955
|
+
toolName,
|
|
956
|
+
status: "recovered",
|
|
957
|
+
input: rawInput,
|
|
958
|
+
repaired,
|
|
959
|
+
rulesFired: ["recoverMangledToolName", ...outcome.rulesFired],
|
|
960
|
+
hints: [hint, ...outcome.hints],
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
918
964
|
export function repairAssistantToolCallInputs(
|
|
919
965
|
message: MinimalAssistantMessage,
|
|
920
966
|
tools: readonly LiveToolSchema[],
|
|
@@ -924,12 +970,20 @@ export function repairAssistantToolCallInputs(
|
|
|
924
970
|
}
|
|
925
971
|
|
|
926
972
|
const schemas = new Map(tools.map((tool) => [tool.name, tool.parameters]));
|
|
973
|
+
const knownTools = new Set(schemas.keys());
|
|
927
974
|
const repairs: LiveToolRepairOutcome[] = [];
|
|
928
975
|
let changed = false;
|
|
929
976
|
const content = message.content.map((part) => {
|
|
930
977
|
if (!isObject(part) || part.type !== "toolCall" || typeof part.name !== "string") return part;
|
|
931
978
|
const schema = schemas.get(part.name);
|
|
932
|
-
if (!schema)
|
|
979
|
+
if (!schema) {
|
|
980
|
+
const renamed = recoverMangledToolCallName(part.name, part.arguments, schemas, knownTools);
|
|
981
|
+
if (!renamed) return part;
|
|
982
|
+
repairs.push(renamed);
|
|
983
|
+
if (renamed.status !== "recovered") return part;
|
|
984
|
+
changed = true;
|
|
985
|
+
return { ...part, name: renamed.toolName, arguments: renamed.repaired };
|
|
986
|
+
}
|
|
933
987
|
const outcome = repairInputAgainstLiveSchema(part.name, part.arguments, schema);
|
|
934
988
|
if (outcome.status !== "unchanged") repairs.push(outcome);
|
|
935
989
|
if (outcome.status !== "recovered") return part;
|