Fix agentic tool loop: El scoping rules, json_get_raw for array/object fields, result truncation

This commit is contained in:
Will Anderson
2026-05-03 12:36:42 -05:00
parent af2ce3ddf3
commit d500415316
9 changed files with 179 additions and 48 deletions
+42 -37
View File
@@ -265,12 +265,9 @@ fn handle_chat_agentic(body: String) -> String {
let api_key: String = agentic_api_key()
let tools_json: String = agentic_tools_literal()
// Build initial messages array
let safe_msg: String = json_safe(message)
let safe_sys: String = json_safe(system)
let messages: String = "[{\"role\":\"user\",\"content\":\"" + safe_msg + "\"}]"
let api_url: String = "https://api.anthropic.com/v1/messages"
let h: Map = {}
map_set(h, "x-api-key", api_key)
@@ -299,47 +296,55 @@ fn handle_chat_agentic(body: String) -> String {
}
let stop_reason: String = json_get(raw_resp, "stop_reason")
let content_arr: String = json_get(raw_resp, "content")
// json_get_raw needed content is an array, json_get returns "" for non-strings
let content_arr: String = json_get_raw(raw_resp, "content")
let eff_content: String = if str_eq(content_arr, "") { "[]" } else { content_arr }
// Collect text and tool_use blocks from content
// Walk content blocks. El rule: mutations must be at top level of while body
// using if-expressions mutations inside if *blocks* don't escape scope.
let text_out: String = ""
let tool_results: String = ""
let has_tool_use: Bool = false
let has_tool: Bool = false
let tool_id: String = ""
let tool_name: String = ""
let tool_input: String = ""
let ci: Int = 0
let c_total: Int = json_array_len(content_arr)
let c_total: Int = json_array_len(eff_content)
while ci < c_total {
let block: String = json_array_get(content_arr, ci)
let block_type: String = json_get(block, "type")
if str_eq(block_type, "text") {
let block_text: String = json_get(block, "text")
let text_out = text_out + block_text
}
if str_eq(block_type, "tool_use") {
let has_tool_use = true
let tool_id: String = json_get(block, "id")
let tool_name: String = json_get(block, "name")
let tool_input: String = json_get(block, "input")
let tool_result: String = dispatch_tool(tool_name, tool_input)
let sep: String = if str_eq(tool_results, "") { "" } else { "," }
let tool_results = tool_results + sep
+ "{\"type\":\"tool_result\",\"tool_use_id\":\"" + tool_id + "\",\"content\":\"" + tool_result + "\"}"
}
let block: String = json_array_get(eff_content, ci)
let btype: String = json_get(block, "type")
// Accumulate text at top level using if-expression
let text_out = if str_eq(btype, "text") { text_out + json_get(block, "text") } else { text_out }
// Capture first tool_use block only
let is_new_tool: Bool = str_eq(btype, "tool_use") && !has_tool
let has_tool = if is_new_tool { true } else { has_tool }
let tool_id = if is_new_tool { json_get(block, "id") } else { tool_id }
let tool_name = if is_new_tool { json_get(block, "name") } else { tool_name }
// input is a JSON object must use json_get_raw, not json_get
let tool_input = if is_new_tool { json_get_raw(block, "input") } else { tool_input }
let ci = ci + 1
}
if str_eq(stop_reason, "tool_use") && has_tool_use {
// Append assistant turn with its content blocks, then tool results
let safe_content_arr: String = json_safe(content_arr)
let inner_msgs: String = str_slice(messages, 1, str_len(messages) - 1)
let messages = "[" + inner_msgs
+ ",{\"role\":\"assistant\",\"content\":" + content_arr + "}"
+ ",{\"role\":\"user\",\"content\":[" + tool_results + "]}"
+ "]"
let iteration = iteration + 1
} else {
let final_text = text_out
let keep_going = false
}
// Dispatch tool and build result message
let tool_result_raw: String = if has_tool { dispatch_tool(tool_name, tool_input) } else { "" }
// Truncate large tool results (web pages etc) to avoid oversized requests
let tool_result: String = if str_len(tool_result_raw) > 6000 {
str_slice(tool_result_raw, 0, 6000) + "...[truncated]"
} else { tool_result_raw }
let tool_msg: String = "{\"type\":\"tool_result\",\"tool_use_id\":\"" + tool_id + "\",\"content\":\"" + tool_result + "\"}"
// Update messages and loop state all at top level using if-expressions
let is_tool_turn: Bool = str_eq(stop_reason, "tool_use") && has_tool
let inner: String = str_slice(messages, 1, str_len(messages) - 1)
let messages = if is_tool_turn {
"[" + inner
+ ",{\"role\":\"assistant\",\"content\":" + eff_content + "}"
+ ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}"
+ "]"
} else { messages }
let final_text = if !is_tool_turn { text_out } else { final_text }
let keep_going = if !is_tool_turn { false } else { keep_going }
let iteration = iteration + 1
}
if str_eq(final_text, "") {