268 const std::string& message) {
269 if (message.empty() &&
history_.empty()) {
270 return absl::InvalidArgumentError(
271 "Conversation must start with a non-empty message.");
274 if (!message.empty()) {
281 bool waiting_for_text_response =
false;
282 absl::Time turn_start = absl::Now();
285 util::PrintInfo(absl::StrCat(
"Starting agent loop (max ", max_iterations,
" iterations)"));
289 for (
int iteration = 0; iteration < max_iterations; ++iteration) {
298 waiting_for_text_response
299 ?
"Generating final response..."
307 if (!response_or.ok()) {
309 "Failed to get AI response: ", response_or.status().message()));
310 return absl::InternalError(absl::StrCat(
311 "Failed to get AI response: ", response_or.status().message()));
314 const auto& agent_response = response_or.value();
323 << (agent_response.text_response.empty() ?
"empty" :
"present")
332 if (!agent_response.tool_calls.empty()) {
334 if (waiting_for_text_response) {
336 absl::StrCat(
"LLM called tools again instead of providing final response (Iteration: ",
337 iteration + 1,
"/", max_iterations,
")"));
340 bool executed_tool =
false;
341 for (
const auto& tool_call : agent_response.tool_calls) {
343 std::vector<std::string> arg_parts;
344 for (
const auto& [key, value] : tool_call.args) {
345 arg_parts.push_back(absl::StrCat(key,
"=", value));
347 std::string args_str = absl::StrJoin(arg_parts,
", ");
352 if (!tool_result_or.ok()) {
354 "Tool execution failed: ", tool_result_or.status().message()));
355 return absl::InternalError(absl::StrCat(
356 "Tool execution failed: ", tool_result_or.status().message()));
359 const std::string& tool_output = tool_result_or.value();
360 if (!tool_output.empty()) {
367 std::string preview = tool_output.substr(0, std::min(
size_t(200), tool_output.size()));
368 if (tool_output.size() > 200) preview +=
"...";
374 std::string marked_output = absl::StrCat(
375 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
376 "The tool returned the following data:\n",
378 "Please provide a text_response field in your JSON to summarize this information for the user.");
380 tool_result_msg.is_internal =
true;
381 history_.push_back(tool_result_msg);
383 executed_tool =
true;
388 waiting_for_text_response =
true;
395 if (waiting_for_text_response && agent_response.text_response.empty() &&
396 agent_response.commands.empty()) {
398 absl::StrCat(
"LLM did not provide text_response after receiving tool results (Iteration: ",
399 iteration + 1,
"/", max_iterations,
")"));
404 std::optional<ProposalCreationResult> proposal_result;
405 absl::Status proposal_status = absl::OkStatus();
406 bool attempted_proposal =
false;
408 if (!agent_response.commands.empty()) {
409 attempted_proposal =
true;
412 proposal_status = absl::FailedPreconditionError(
413 "No ROM context available for proposal creation");
415 "Cannot create proposal because no ROM context is active.");
417 proposal_status = absl::FailedPreconditionError(
418 "ROM context is not loaded");
420 "Cannot create proposal because the ROM context is not loaded.");
427 request.
ai_provider = absl::GetFlag(FLAGS_ai_provider);
430 if (!creation_or.ok()) {
431 proposal_status = creation_or.status();
433 "Failed to create proposal: ", proposal_status.message()));
435 proposal_result = std::move(creation_or.value());
438 "Created proposal ", proposal_result->metadata.id,
439 " with ", proposal_result->change_count,
" change(s)."));
445 std::string response_text = agent_response.text_response;
446 if (!agent_response.reasoning.empty()) {
447 if (!response_text.empty()) {
448 response_text.append(
"\n\n");
450 response_text.append(
"Reasoning: ");
451 response_text.append(agent_response.reasoning);
453 const int executable_commands =
454 CountExecutableCommands(agent_response.commands);
455 if (!agent_response.commands.empty()) {
456 if (!response_text.empty()) {
457 response_text.append(
"\n\n");
459 response_text.append(
"Commands:\n");
460 response_text.append(absl::StrJoin(agent_response.commands,
"\n"));
464 if (proposal_result.has_value()) {
465 const auto& metadata = proposal_result->metadata;
466 if (!response_text.empty()) {
467 response_text.append(
"\n\n");
469 response_text.append(absl::StrFormat(
470 "✅ Proposal %s ready with %d change%s (%d command%s).\n"
471 "Review it in the Proposal drawer or run `z3ed agent diff --proposal-id %s`.\n"
472 "Sandbox ROM: %s\nProposal JSON: %s",
473 metadata.id, proposal_result->change_count,
474 proposal_result->change_count == 1 ?
"" :
"s",
475 proposal_result->executed_commands,
476 proposal_result->executed_commands == 1 ?
"" :
"s",
477 metadata.id, metadata.sandbox_rom_path.string(),
478 proposal_result->proposal_json_path.string()));
480 }
else if (attempted_proposal && !proposal_status.ok()) {
481 if (!response_text.empty()) {
482 response_text.append(
"\n\n");
484 response_text.append(absl::StrCat(
485 "⚠️ Failed to prepare a proposal automatically: ",
486 proposal_status.message()));
490 if (proposal_result.has_value()) {
492 summary.
id = proposal_result->metadata.id;
505 return chat_response;
508 return absl::InternalError(
509 "Agent did not produce a response after executing tools.");