diff --git a/internal/lua/adapters/trae.lua b/internal/lua/adapters/trae.lua
index d05b45a..45949cc 100644
--- a/internal/lua/adapters/trae.lua
+++ b/internal/lua/adapters/trae.lua
@@ -46,9 +46,6 @@ end
-- Returns (tool_calls_array_or_nil, content_with_blocks_removed).
local function parse_text_tool_calls(content)
if type(content) ~= "string" or content == "" then return nil, content end
- if not (content:find("]*>%s*(.-)%s*")
collect("]*>%s*(.-)%s*")
+ -- Legacy / mimicked form: models that saw past assistant turns folded as
+ -- [Called tool: name({...})]
+ -- tend to emit the same shape instead of a real block. Recover it so
+ -- the agent loop keeps working; otherwise the client receives plain text
+ -- where a structured tool call should be. Capture the name and the JSON
+ -- object literal independently (the JSON is the only {...} run here).
+ if #tcs == 0 and content:find("%[Called tool") then
+ for name, argsraw in content:gmatch("%[Called tool%s*:%s*([%w_%-%.]+)%s*%((%b{})%s*%)%s*%]") do
+ local aok, decoded = pcall(json.decode, argsraw)
+ idx = idx + 1
+ table.insert(tcs, {
+ id = "call_legacy_" .. idx,
+ type = "function",
+ name = name,
+ arguments = aok and decoded or {}
+ })
+ end
+ end
+
if #tcs == 0 then return nil, content end
-- drop the blocks from user-visible content; keep any surrounding prose
local stripped = content:gsub("]*>%s*.-%s*", "")
stripped = stripped:gsub("]*>%s*.-%s*", "")
+ stripped = stripped:gsub("%[Called tool%s*:%s*[%w_%-%.]+%s*%(%b{}%s*%)%s*%]", "")
stripped = stripped:gsub("^%s+", ""):gsub("%s+$", "")
return tcs, stripped
end