Summary
Since v0.43.0, the hoisted read / write / edit tools expose a non-compliant JSON Schema: a top-level anyOf with no root type: "object". OpenAI-compatible function-calling requires parameters to be { "type": "object", ... }, so:
- Strict providers reject the request entirely (DeepSeek → HTTP 400).
- Lenient models accept it but emit empty
{} arguments for these three tools (GLM-5.2).
bash, grep, and AFT's own tools (aft_search / aft_zoom / aft_outline / aft_inspect) are unaffected because they use flat type: "object" schemas.
Regression window
Introduced in v0.43.0 as the fix for #148 ("Read tool fails because of wrong argument name passed" — GLM passing filePath instead of `path"). Still present in v0.45.0 (latest as of filing). Because AFT auto-updates, this broke read/write/edit between sessions for anyone on v0.42.0 → v0.43.0+.
The v0.43.0 changelog says:
the Pi read, write, and edit tools now accept filePath as an alias for path (and the reverse)
That aliasing is implemented as a TypeBox.Union of two object shapes (one where path is required, one where filePath is required), which serializes to a bare anyOf.
Reproduction
Two models, two failure modes — same root cause.
1. Strict provider (DeepSeek, OpenAI-compatible) → 400
Switch the session model to deepseek-v4-flash and trigger any of read/write/edit. The provider rejects the whole request before the model runs:
Error: 400: {"message":"Invalid schema for function 'read': schema must be a JSON Schema of 'type: \"object\"', got 'type: null'.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}
"got 'type: null'" = the schema object has no type field at all (the anyOf sits at the root).
2. Lenient model (GLM-5.2) → empty {} arguments
On glm-5.2, every read/write/edit invocation comes back with empty args (the model cannot resolve which branch of the anyOf to populate):
Validation failed for tool "write":
- filePath: must have required properties filePath, content
- path: must have required properties path, content
- root: must match a schema in anyOf
Received arguments:
{}
Notably the #148 fix was filed specifically because of GLM, yet the resulting anyOf schema makes GLM unable to call these tools at all — strictly worse than the original symptom.
Control cases (same session, same model)
bash (flat type: object schema): ✅ works on both models
- AFT's own
aft_search/aft_zoom/aft_outline (own flat schemas): ✅ work on both models
- Disabling
@cortexkit/aft-pi (restores Pi's native read/write/edit): ✅ all three work again
Root cause
dist/index.js:
// line 32448
var ReadParams = Type2.Union([
Type2.Object({ path: Type2.String({...}), filePath: Type2.Optional(...), offset, limit }),
Type2.Object({ path: Type2.Optional(...), filePath: Type2.String({...}), offset, limit }),
]);
// line 32470
var WriteParams = Type2.Union([ /* path-required | filePath-required */ ]);
// line 32490
var EditParams = Type2.Union([ /* path-required | filePath-required */ ]);
// line 32522
var GrepParams = Type2.Object({ pattern, path, include, caseSensitive }); // ← flat, works
TypeBox.Union([...]) serializes to { "anyOf": [ {type:"object",...}, {type:"object",...} ] } with no root type: "object". Pi passes parameters straight through to the provider as tool.function.parameters, so the wire schema is non-compliant.
The runtime alias resolution that #148 actually needs already exists and does not require a schema union:
// line 9625
function coerceAliasedStringParam(value, aliasValue) { ... }
// line 32530 / 32533
function readPathArg(args) { return coerceAliasedStringParam(args.path, args.filePath); }
function mutationFilePathArg(args){ return coerceAliasedStringParam(args.filePath, args.path); }
So the Union is redundant: the coercion layer already accepts either name. GrepParams (flat Type.Object) proves the working pattern.
Suggested fix
Drop the Union and model both names as optional in a single flat Type.Object, then enforce "at least one present" at runtime (the coercion functions above already do the right thing). This restores a compliant { "type": "object", "properties": {...} } schema while keeping the path/filePath alias behavior from #148:
var ReadParams = Type2.Object({
path: Type2.Optional(Type2.String({ description: "Path to the file (absolute or relative to project root)" })),
filePath: Type2.Optional(Type2.String({ description: "Compatibility alias for `path`." })),
offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
limit: optionalInt(1, Number.MAX_SAFE_INTEGER),
});
// runtime: throw if readPathArg(args) is undefined (already the case today)
Same shape for WriteParams / EditParams. GrepParams is already correct and can serve as the template.
(Alternative if schema-level "exactly-one-of" is desired: keep it out of the top-level schema — embed the constraint inside properties — but the top-level object must always be type: "object".)
Workaround
- Pin
@cortexkit/aft-pi to 0.42.0, or
- Disable the package (loses
aft_* tools + the Rust reader until fixed).
Environment
@cortexkit/aft-pi: 0.45.0 (latest; regression started 0.43.0)
- Pi: 0.80.3
- Providers reproducing: DeepSeek (
deepseek-v4-flash, strict), ZAI (glm-5.2, lenient)
References
Summary
Since v0.43.0, the hoisted
read/write/edittools expose a non-compliant JSON Schema: a top-levelanyOfwith no roottype: "object". OpenAI-compatible function-calling requiresparametersto be{ "type": "object", ... }, so:{}arguments for these three tools (GLM-5.2).bash,grep, and AFT's own tools (aft_search/aft_zoom/aft_outline/aft_inspect) are unaffected because they use flattype: "object"schemas.Regression window
Introduced in v0.43.0 as the fix for #148 ("Read tool fails because of wrong argument name passed" — GLM passing
filePathinstead of `path"). Still present in v0.45.0 (latest as of filing). Because AFT auto-updates, this broke read/write/edit between sessions for anyone on v0.42.0 → v0.43.0+.The v0.43.0 changelog says:
That aliasing is implemented as a
TypeBox.Unionof two object shapes (one wherepathis required, one wherefilePathis required), which serializes to a bareanyOf.Reproduction
Two models, two failure modes — same root cause.
1. Strict provider (DeepSeek, OpenAI-compatible) → 400
Switch the session model to
deepseek-v4-flashand trigger any ofread/write/edit. The provider rejects the whole request before the model runs:"got 'type: null'"= the schema object has notypefield at all (theanyOfsits at the root).2. Lenient model (GLM-5.2) → empty
{}argumentsOn
glm-5.2, everyread/write/editinvocation comes back with empty args (the model cannot resolve which branch of theanyOfto populate):Notably the #148 fix was filed specifically because of GLM, yet the resulting
anyOfschema makes GLM unable to call these tools at all — strictly worse than the original symptom.Control cases (same session, same model)
bash(flattype: objectschema): ✅ works on both modelsaft_search/aft_zoom/aft_outline(own flat schemas): ✅ work on both models@cortexkit/aft-pi(restores Pi's nativeread/write/edit): ✅ all three work againRoot cause
dist/index.js:TypeBox.Union([...])serializes to{ "anyOf": [ {type:"object",...}, {type:"object",...} ] }with no roottype: "object". Pi passesparametersstraight through to the provider astool.function.parameters, so the wire schema is non-compliant.The runtime alias resolution that #148 actually needs already exists and does not require a schema union:
So the
Unionis redundant: the coercion layer already accepts either name.GrepParams(flatType.Object) proves the working pattern.Suggested fix
Drop the
Unionand model both names as optional in a single flatType.Object, then enforce "at least one present" at runtime (the coercion functions above already do the right thing). This restores a compliant{ "type": "object", "properties": {...} }schema while keeping thepath/filePathalias behavior from #148:Same shape for
WriteParams/EditParams.GrepParamsis already correct and can serve as the template.(Alternative if schema-level "exactly-one-of" is desired: keep it out of the top-level schema — embed the constraint inside
properties— but the top-level object must always betype: "object".)Workaround
@cortexkit/aft-pito0.42.0, oraft_*tools + the Rust reader until fixed).Environment
@cortexkit/aft-pi: 0.45.0 (latest; regression started 0.43.0)deepseek-v4-flash, strict), ZAI (glm-5.2, lenient)References