Short description
edit accepts an argument object carrying only filePath and symbol as valid per the advertised schema, but incomplete per the runtime contract — a model that stops there gets rejected. This validation error accounts for 86 of kimi-k3's 1389 edit calls; in the session I captured, 9 consecutive calls omitted content entirely.
What happened?
What I expected: a model calling edit would either supply the mode payload (content for symbol mode) or be steered to do so.
What I observed: kimi-k3 (K3 via Moonshot, api.kimi.com/coding/v1/chat/completions, served through an OpenAI-compatible proxy) repeatedly emits a tool call whose arguments are exactly {"filePath": "<...>", "symbol": "<...>"} — the JSON object closes right after symbol, with no content key at all. AFT then rejects it with:
edit: symbol mode requires both 'symbol' and 'content' string properties
The model's own reasoning trace says it intends to use edits[] ("My two edit calls failed because I mistakenly used symbol mode without content. Let me do the actual edits with batch edits mode"), yet the emitted arguments are symbol-only again. It loops until an unrelated /system-reminder about repeated tool calls breaks the cycle.
The captured call never carried content. The proxy keeps raw upstream SSE (one file per request, === API RESPONSE 1 === section). Deltas for one failing call, with the token-boundary fragments of filePath consolidated into one delta for readability:
"arguments":"{"
"arguments":"\"filePath\":\"/<project>/src/main/java/.../api/MinecraftVersion.java\""
"arguments":",\"symbol\":\"MINECRAFT_26_3\""
"arguments":"}"
Concatenating them yields {"filePath":"…/MinecraftVersion.java","symbol":"MINECRAFT_26_3"} — a complete, valid object with finish_reason: tool_calls. That one request carried two identical edit calls, and the host-side record for the same call holds those two keys and no content (Log output below). So the field was not removed by the proxy, the host or the plugin: it was not emitted.
Frequency (OpenCode state DB, tool-call records). Counting edit calls whose error is this message. Figures cover 2026-07-20 through 2026-09-18, joined per model from each record's own providerID/modelID:
| model |
edit calls |
edit errors |
this message |
| kimi-k3 |
1389 |
282 |
86 |
| glm-5.3-flash |
117 |
14 |
2 |
| glm-5.2 |
648 |
80 |
1 |
Every other model in the same records produced no occurrence of this message.
Two caveats on that count: the error text cannot separate a missing content from a null-valued one, since both produce the same string; and 86 of 1389 is a correlation between the schema shape and the failure, not a controlled causal result.
Where the contract comes from. The installed v0.56.2 build defines the tool's arguments as a flat object with a single required key — read from createEditTool in the installed plugin (dist/entry/server.js, v0.56.2):
args: {
filePath: z.string(),
symbol: z.string().optional(),
content: z.string().optional(),
appendContent: z.string().optional(),
edits: z.array(z.object({ /* oldString/newString/replaceAll/occurrence …, or startLine/endLine/content */ })).min(1).optional()
}
No oneOf/anyOf, and additionalProperties is not set. The request body carries the same contract serialized to JSON Schema; abridged to the relevant keys, as captured from the proxy for the same session:
{ "type": "object",
"required": ["filePath"],
"properties": {
"filePath": { "type": "string" },
"symbol": { "type": "string" },
"content": { "type": "string" },
"appendContent": { "type": "string" },
"edits": { "type": "array", "minItems": 1, "items": { "type": "object" } } } }
So an object with only filePath and symbol passes the advertised schema, and is rejected by the runtime. The same request advertises write as required: ["filePath","content"]; this model's write record is 148 calls / 2 errors.
The declarations match crates/aft/src/subc_tool_schemas.json and createEditTool at v0.56.2.
That shape lines up with two statements in Moonshot's K3 documentation:
Setting additionalProperties to false and listing mandatory fields in required reduces invalid arguments. … When your inventory grows to dozens or hundreds of tools, do not send every schema in every request.
— Build an Agent with Kimi K3
… the more candidate tools there are, the more likely the model picks the wrong tool or constructs invalid call arguments.
— Dynamically Loaded Tools
The captured requests carried 83 tool definitions (tool_surface: "all"). That is an observation about the request, not a tested cause: I have not measured whether a smaller inventory changes the outcome.
Requests
- Make the advertised schema express the parameter branches the runtime accepts, with each branch's required fields, and state what happens when more than one payload is present. Today
symbol+content, appendContent, and both item shapes of edits[] are optional siblings with no combination rule, so the valid-object-but-invalid-call gap is structural rather than accidental. Whatever combination keywords are used must match the runtime's existing behaviour, so that a branch payload can no longer be omitted while still passing validation. additionalProperties: false is a separate, optional item: it does not address a missing field, and it needs checking against the path alias and the target endpoints.
- On the OpenCode surface, sync
path → filePath in the edit description. createEditTool already states the intent in-line (// filePath, not path: host UI header contract), and 9a9158d63 (2026-07-24) applied it to the schema — but getEditDescription still says to pass path + symbol + content and shows { "path": "src/app.ts", ... } (line 703), while docs/tools.md (v0.56.2, ### edit) uses path throughout without ever naming filePath. path is accepted as an alias at runtime, so nothing is functionally broken — the advertised schema and the prose it reads simply disagree on the field name. This is a separate, documentation-only item.
- Minimal reproduction and acceptance criteria, in case the above is not self-evident: in OpenCode 1.18.31 with
@cortexkit/aft-opencode v0.56.2, running the default edit surface (with edit_mode unset, so the edit tool is not the hashline variant), create a scratch file holding one known symbol, call edit with an argument object containing only filePath and symbol, and observe the message quoted above. A fix satisfies this when (a) an object with no mode payload no longer validates against the advertised schema, and (b) valid calls of every supported branch still pass.
The failure is generated upstream of AFT — the model emitted an incomplete argument object and AFT's rejection is correct. Request 1 is about the advertised schema; Request 2 is the separate naming inconsistency; Request 3 checks Request 1.
Diagnostics
Manual report (the CLI reported aft binary: not installed when run via npx, so values below come from the installed plugin and its bundled binary):
@cortexkit/aft (CLI) v0.56.2
@cortexkit/aft-opencode v0.56.2
bundled aft binary v0.56.2
OpenCode host 1.18.31
platform macOS arm64 (darwin)
Relevant config (~/.config/cortexkit/aft.jsonc):
Model: kimi-k3 via an OpenAI-compatible proxy (api.kimi.com/coding/v1/chat/completions, reasoning_effort: "max"). The captured follow-up requests do include assistant reasoning_content and tool_calls; I have not isolated preserved-thinking behavior as a contributing factor.
Plugin version
0.56.2
AFT binary version
0.56.2
Platform
macOS arm64 (darwin)
Log output (optional)
Raw upstream SSE deltas for one failing call (path redacted; the token-boundary fragments of filePath are consolidated into a single delta), 2026-09-18, from the proxy's request log:
"tool_calls":[{"index":0,"id":"functions.MINECRAFT_26_3:0","type":"function","function":{"name":"edit","arguments":""}}]
"tool_calls":[{"index":0,"function":{"arguments":"{"}}]
"tool_calls":[{"index":0,"function":{"arguments":"\"filePath\":\"/<project>/src/main/java/.../api/MinecraftVersion.java\""}}]
"tool_calls":[{"index":0,"function":{"arguments":",\"symbol\":\"MINECRAFT_26_3\""}}]
"tool_calls":[{"index":0,"function":{"arguments":"}"}}]
{"choices":[{"finish_reason":"tool_calls", ...}]}
Host-side record of the same call (OpenCode storage, part.data):
{ "type": "tool", "tool": "edit",
"state": { "status": "error",
"input": { "filePath": "/<project>/src/main/java/.../api/MinecraftVersion.java",
"symbol": "MINECRAFT_26_3" },
"error": "edit: symbol mode requires both 'symbol' and 'content' string properties" } }
Short description
editaccepts an argument object carrying onlyfilePathandsymbolas valid per the advertised schema, but incomplete per the runtime contract — a model that stops there gets rejected. This validation error accounts for 86 of kimi-k3's 1389editcalls; in the session I captured, 9 consecutive calls omittedcontententirely.What happened?
What I expected: a model calling
editwould either supply the mode payload (contentfor symbol mode) or be steered to do so.What I observed: kimi-k3 (K3 via Moonshot,
api.kimi.com/coding/v1/chat/completions, served through an OpenAI-compatible proxy) repeatedly emits a tool call whose arguments are exactly{"filePath": "<...>", "symbol": "<...>"}— the JSON object closes right aftersymbol, with nocontentkey at all. AFT then rejects it with:The model's own reasoning trace says it intends to use
edits[]("My two edit calls failed because I mistakenly used symbol mode without content. Let me do the actual edits with batch edits mode"), yet the emitted arguments are symbol-only again. It loops until an unrelated/system-reminderabout repeated tool calls breaks the cycle.The captured call never carried
content. The proxy keeps raw upstream SSE (one file per request,=== API RESPONSE 1 ===section). Deltas for one failing call, with the token-boundary fragments offilePathconsolidated into one delta for readability:Concatenating them yields
{"filePath":"…/MinecraftVersion.java","symbol":"MINECRAFT_26_3"}— a complete, valid object withfinish_reason: tool_calls. That one request carried two identicaleditcalls, and the host-side record for the same call holds those two keys and nocontent(Log output below). So the field was not removed by the proxy, the host or the plugin: it was not emitted.Frequency (OpenCode state DB, tool-call records). Counting
editcalls whose error is this message. Figures cover 2026-07-20 through 2026-09-18, joined per model from each record's ownproviderID/modelID:Every other model in the same records produced no occurrence of this message.
Two caveats on that count: the error text cannot separate a missing
contentfrom a null-valued one, since both produce the same string; and 86 of 1389 is a correlation between the schema shape and the failure, not a controlled causal result.Where the contract comes from. The installed v0.56.2 build defines the tool's arguments as a flat object with a single required key — read from
createEditToolin the installed plugin (dist/entry/server.js, v0.56.2):No
oneOf/anyOf, andadditionalPropertiesis not set. The request body carries the same contract serialized to JSON Schema; abridged to the relevant keys, as captured from the proxy for the same session:{ "type": "object", "required": ["filePath"], "properties": { "filePath": { "type": "string" }, "symbol": { "type": "string" }, "content": { "type": "string" }, "appendContent": { "type": "string" }, "edits": { "type": "array", "minItems": 1, "items": { "type": "object" } } } }So an object with only
filePathandsymbolpasses the advertised schema, and is rejected by the runtime. The same request advertiseswriteasrequired: ["filePath","content"]; this model'swriterecord is 148 calls / 2 errors.The declarations match
crates/aft/src/subc_tool_schemas.jsonandcreateEditToolat v0.56.2.That shape lines up with two statements in Moonshot's K3 documentation:
The captured requests carried 83 tool definitions (
tool_surface: "all"). That is an observation about the request, not a tested cause: I have not measured whether a smaller inventory changes the outcome.Requests
symbol+content,appendContent, and both item shapes ofedits[]are optional siblings with no combination rule, so the valid-object-but-invalid-call gap is structural rather than accidental. Whatever combination keywords are used must match the runtime's existing behaviour, so that a branch payload can no longer be omitted while still passing validation.additionalProperties: falseis a separate, optional item: it does not address a missing field, and it needs checking against thepathalias and the target endpoints.path→filePathin theeditdescription.createEditToolalready states the intent in-line (// filePath, not path: host UI header contract), and9a9158d63(2026-07-24) applied it to the schema — butgetEditDescriptionstill says to passpath+symbol+contentand shows{ "path": "src/app.ts", ... }(line 703), whiledocs/tools.md(v0.56.2,### edit) usespaththroughout without ever namingfilePath.pathis accepted as an alias at runtime, so nothing is functionally broken — the advertised schema and the prose it reads simply disagree on the field name. This is a separate, documentation-only item.@cortexkit/aft-opencodev0.56.2, running the default edit surface (withedit_modeunset, so theedittool is not the hashline variant), create a scratch file holding one known symbol, calleditwith an argument object containing onlyfilePathandsymbol, and observe the message quoted above. A fix satisfies this when (a) an object with no mode payload no longer validates against the advertised schema, and (b) valid calls of every supported branch still pass.The failure is generated upstream of AFT — the model emitted an incomplete argument object and AFT's rejection is correct. Request 1 is about the advertised schema; Request 2 is the separate naming inconsistency; Request 3 checks Request 1.
Diagnostics
Manual report (the CLI reported
aft binary: not installedwhen run vianpx, so values below come from the installed plugin and its bundled binary):Relevant config (
~/.config/cortexkit/aft.jsonc):{ "tool_surface": "all", // edit_mode was unset while the calls in this report were made -> "default" (find/replace/symbol/batch surface). }Model:
kimi-k3via an OpenAI-compatible proxy (api.kimi.com/coding/v1/chat/completions,reasoning_effort: "max"). The captured follow-up requests do include assistantreasoning_contentandtool_calls; I have not isolated preserved-thinking behavior as a contributing factor.Plugin version
0.56.2
AFT binary version
0.56.2
Platform
macOS arm64 (darwin)
Log output (optional)
Raw upstream SSE deltas for one failing call (path redacted; the token-boundary fragments of
filePathare consolidated into a single delta), 2026-09-18, from the proxy's request log:Host-side record of the same call (OpenCode storage,
part.data):{ "type": "tool", "tool": "edit", "state": { "status": "error", "input": { "filePath": "/<project>/src/main/java/.../api/MinecraftVersion.java", "symbol": "MINECRAFT_26_3" }, "error": "edit: symbol mode requires both 'symbol' and 'content' string properties" } }