fix(agent): subagent patuhi tool-calling contract + truncation char-safe

Hasil audit alur AI agent round 3 (fokus correctness & latent crash).

- fix(subagent): engine.rs sebelumnya mengeksekusi tool lalu push
  ChatMessage::tool hasil TANPA mendahuluinya dengan pesan assistant yang
  mendeklarasikan tool_calls → history malformed ([..., tool, tool,
  assistant(text)]). Kontrak OpenAI/Anthropic mensyaratkan pesan assistant
  (berisi tool_calls) sebelum hasil tool. Kini push response_msg
  (assistant + tool_calls + content) sebelum eksekusi, dan hapus push
  assistant content-only di akhir (agar tidak duplikat). Loop utama sudah
  benar; subagent kini selaras.
- fix(utils): &content[..1500] / &content[..1000] di build_rich_context
  dan &diff[..5000] di auto/engine.rs bisa panic saat indeks byte jatuh di
  tengah karakter multi-byte UTF-8 (emoji/CJK/panah). Tambah helper
  truncate_chars() yang memotong per karakter (char-safe) dan pakai di
  3 titik tersebut.
- test: +4 unit test truncate_chars (ASCII, potong, multibyte no-panic,
  emoji).

Catatan audit: subagent/auto (auto-review) & build_rich_context adalah dead
code (spawn_background_review & build_rich_context tidak pernah dipanggil).
Auto-review jangan diaktifkan asal (parser format teks rapuh + tanpa
verifikasi pasca-fix) — dilaporkan, bukan dicolokkan.
This commit is contained in:
asepharyana
2026-08-28 11:43:42 +07:00
parent 20ce81a6be
commit e982cbeb04
4 changed files with 71 additions and 20 deletions
+7 -6
View File
@@ -220,7 +220,7 @@ pub async fn run_agent(
.await?;
let content = response_msg.content.clone().unwrap_or_default();
let tool_calls = response_msg.tool_calls.unwrap_or_default();
let tool_calls = response_msg.tool_calls.clone().unwrap_or_default();
// If no tool calls, we're done — return content
if tool_calls.is_empty() {
@@ -229,6 +229,12 @@ pub async fn run_agent(
return Ok(content);
}
// Push the assistant message (with its tool_calls) BEFORE executing
// so the tool-calling contract is honoured: tool results reference
// the calls declared in the preceding assistant message. Without
// this, the history is malformed (`[...tool, tool, assistant]`).
messages.push(response_msg);
// Execute tool calls — read-only batches run concurrently (bounded,
// order preserved); any mutating tool forces the safe sequential path.
let results = execute_tool_batch(&tools, &tool_ctx, &tool_calls);
@@ -266,11 +272,6 @@ pub async fn run_agent(
messages.push(ChatMessage::tool(id, truncate_tool_output(result)));
}
// Add assistant response if there was text content
if !content.is_empty() {
messages.push(ChatMessage::assistant(Some(content)));
}
}
info!("Subagent reached iteration limit ({MAX_ITERATIONS})");