230 const std::string& message) {
231 if (message.empty() &&
history_.empty()) {
232 return absl::InvalidArgumentError(
233 "Conversation must start with a non-empty message.");
236 if (!message.empty()) {
243 bool waiting_for_text_response =
false;
244 absl::Time turn_start = absl::Now();
247 util::PrintInfo(absl::StrCat(
"Starting agent loop (max ", max_iterations,
" iterations)"));
251 for (
int iteration = 0; iteration < max_iterations; ++iteration) {
260 waiting_for_text_response
261 ?
"Generating final response..."
269 if (!response_or.ok()) {
271 "Failed to get AI response: ", response_or.status().message()));
272 return absl::InternalError(absl::StrCat(
273 "Failed to get AI response: ", response_or.status().message()));
276 const auto& agent_response = response_or.value();
285 << (agent_response.text_response.empty() ?
"empty" :
"present")
294 if (!agent_response.tool_calls.empty()) {
296 if (waiting_for_text_response) {
298 absl::StrCat(
"LLM called tools again instead of providing final response (Iteration: ",
299 iteration + 1,
"/", max_iterations,
")"));
302 bool executed_tool =
false;
303 for (
const auto& tool_call : agent_response.tool_calls) {
305 std::vector<std::string> arg_parts;
306 for (
const auto& [key, value] : tool_call.args) {
307 arg_parts.push_back(absl::StrCat(key,
"=", value));
309 std::string args_str = absl::StrJoin(arg_parts,
", ");
314 if (!tool_result_or.ok()) {
316 "Tool execution failed: ", tool_result_or.status().message()));
317 return absl::InternalError(absl::StrCat(
318 "Tool execution failed: ", tool_result_or.status().message()));
321 const std::string& tool_output = tool_result_or.value();
322 if (!tool_output.empty()) {
329 std::string preview = tool_output.substr(0, std::min(
size_t(200), tool_output.size()));
330 if (tool_output.size() > 200) preview +=
"...";
336 std::string marked_output = absl::StrCat(
337 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
338 "The tool returned the following data:\n",
340 "Please provide a text_response field in your JSON to summarize this information for the user.");
342 tool_result_msg.is_internal =
true;
343 history_.push_back(tool_result_msg);
345 executed_tool =
true;
350 waiting_for_text_response =
true;
357 if (waiting_for_text_response && agent_response.text_response.empty() &&
358 agent_response.commands.empty()) {
360 absl::StrCat(
"LLM did not provide text_response after receiving tool results (Iteration: ",
361 iteration + 1,
"/", max_iterations,
")"));
366 std::optional<ProposalCreationResult> proposal_result;
367 absl::Status proposal_status = absl::OkStatus();
368 bool attempted_proposal =
false;
370 if (!agent_response.commands.empty()) {
371 attempted_proposal =
true;
374 proposal_status = absl::FailedPreconditionError(
375 "No ROM context available for proposal creation");
377 "Cannot create proposal because no ROM context is active.");
379 proposal_status = absl::FailedPreconditionError(
380 "ROM context is not loaded");
382 "Cannot create proposal because the ROM context is not loaded.");
389 request.
ai_provider = absl::GetFlag(FLAGS_ai_provider);
392 if (!creation_or.ok()) {
393 proposal_status = creation_or.status();
395 "Failed to create proposal: ", proposal_status.message()));
397 proposal_result = std::move(creation_or.value());
400 "Created proposal ", proposal_result->metadata.id,
401 " with ", proposal_result->change_count,
" change(s)."));
407 std::string response_text = agent_response.text_response;
408 if (!agent_response.reasoning.empty()) {
409 if (!response_text.empty()) {
410 response_text.append(
"\n\n");
412 response_text.append(
"Reasoning: ");
413 response_text.append(agent_response.reasoning);
415 const int executable_commands =
416 CountExecutableCommands(agent_response.commands);
417 if (!agent_response.commands.empty()) {
418 if (!response_text.empty()) {
419 response_text.append(
"\n\n");
421 response_text.append(
"Commands:\n");
422 response_text.append(absl::StrJoin(agent_response.commands,
"\n"));
426 if (proposal_result.has_value()) {
427 const auto& metadata = proposal_result->metadata;
428 if (!response_text.empty()) {
429 response_text.append(
"\n\n");
431 response_text.append(absl::StrFormat(
432 "✅ Proposal %s ready with %d change%s (%d command%s).\n"
433 "Review it in the Proposal drawer or run `z3ed agent diff --proposal-id %s`.\n"
434 "Sandbox ROM: %s\nProposal JSON: %s",
435 metadata.id, proposal_result->change_count,
436 proposal_result->change_count == 1 ?
"" :
"s",
437 proposal_result->executed_commands,
438 proposal_result->executed_commands == 1 ?
"" :
"s",
439 metadata.id, metadata.sandbox_rom_path.string(),
440 proposal_result->proposal_json_path.string()));
442 }
else if (attempted_proposal && !proposal_status.ok()) {
443 if (!response_text.empty()) {
444 response_text.append(
"\n\n");
446 response_text.append(absl::StrCat(
447 "⚠️ Failed to prepare a proposal automatically: ",
448 proposal_status.message()));
452 if (proposal_result.has_value()) {
454 summary.
id = proposal_result->metadata.id;
467 return chat_response;
470 return absl::InternalError(
471 "Agent did not produce a response after executing tools.");