420 bool executed_tool =
false;
421 std::vector<std::string> executed_tools;
424 for (
const auto& tool_call : agent_response.
tool_calls) {
426 std::vector<std::string> arg_parts;
427 for (
const auto& [key, value] : tool_call.args) {
428 arg_parts.push_back(absl::StrCat(key,
"=", value));
430 std::string args_str = absl::StrJoin(arg_parts,
", ");
435 std::string tool_output;
436 if (!tool_result_or.ok()) {
438 absl::StrCat(
"Error: ", tool_result_or.status().message());
441 tool_output = tool_result_or.value();
445 if (!tool_output.empty()) {
448 std::string marked_output = absl::StrCat(
449 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
450 "The tool returned the following data:\n", tool_output,
"\n\n",
451 "Please provide a text_response field in your JSON to summarize "
452 "this information for the user.");
453 auto tool_result_msg =
455 tool_result_msg.is_internal =
true;
456 history_.push_back(tool_result_msg);
458 executed_tool =
true;
459 executed_tools.push_back(tool_call.tool_name);
470 std::optional<ProposalCreationResult> proposal_result;
471 absl::Status proposal_status = absl::OkStatus();
472 bool attempted_proposal =
false;
474 if (!agent_response.
commands.empty()) {
475 attempted_proposal =
true;
482 request.
prompt = it->message;
493 if (!creation_or.ok()) {
494 proposal_status = creation_or.status();
496 proposal_status.message()));
498 proposal_result = std::move(creation_or.value());
506 if (!response_text.empty())
507 response_text.append(
"\n\n");
508 response_text.append(
"Reasoning: ").append(agent_response.
reasoning);
511 if (!agent_response.
commands.empty()) {
512 if (!response_text.empty())
513 response_text.append(
"\n\n");
514 response_text.append(
"Commands:\n")
515 .append(absl::StrJoin(agent_response.
commands,
"\n"));
518 CountExecutableCommands(agent_response.
commands);
520 if (proposal_result.has_value()) {
521 const auto& metadata = proposal_result->metadata;
522 if (!response_text.empty())
523 response_text.append(
"\n\n");
524 response_text.append(
525 absl::StrFormat(
"✅ Proposal %s ready with %d change%s (%d command%s).",
526 metadata.id, proposal_result->change_count,
527 proposal_result->change_count == 1 ?
"" :
"s",
528 proposal_result->executed_commands,
529 proposal_result->executed_commands == 1 ?
"" :
"s"));
531 }
else if (attempted_proposal && !proposal_status.ok()) {
532 if (!response_text.empty())
533 response_text.append(
"\n\n");
534 response_text.append(absl::StrCat(
"⚠️ Failed to prepare proposal: ",
535 proposal_status.message()));
541 history_.back().message ==
"Thinking...") {
548 if (proposal_result.has_value()) {
550 summary.
id = proposal_result->metadata.id;
559 meta.
model =
"gemini";
571 const std::string& message) {
572 if (message.empty() &&
history_.empty()) {
573 return absl::InvalidArgumentError(
574 "Conversation must start with a non-empty message.");
577 if (!message.empty()) {
579 const std::string auto_hook =
581 if (!auto_hook.empty()) {
583 hook_message.is_internal =
true;
584 history_.push_back(std::move(hook_message));
602 bool waiting_for_text_response =
false;
603 absl::Time turn_start = absl::Now();
604 std::vector<std::string> executed_tools;
607 util::PrintInfo(absl::StrCat(
"Starting agent loop (max ", max_iterations,
610 absl::StrCat(
"History size: ",
history_.size(),
" messages"));
613 for (
int iteration = 0; iteration < max_iterations; ++iteration) {
622 waiting_for_text_response ?
"Generating final response..."
630 if (!response_or.ok()) {
632 response_or.status().message()));
633 return absl::InternalError(absl::StrCat(
"Failed to get AI response: ",
634 response_or.status().message()));
637 const auto& agent_response = response_or.value();
642 <<
" - Tool calls: " << agent_response.tool_calls.size()
645 <<
" - Commands: " << agent_response.commands.size()
648 << (agent_response.text_response.empty() ?
"empty" :
"present")
658 if (!agent_response.tool_calls.empty()) {
661 if (waiting_for_text_response) {
663 absl::StrCat(
"LLM called tools again instead of providing final "
664 "response (Iteration: ",
665 iteration + 1,
"/", max_iterations,
")"));
668 bool executed_tool =
false;
669 for (
const auto& tool_call : agent_response.tool_calls) {
671 std::vector<std::string> arg_parts;
672 for (
const auto& [key, value] : tool_call.args) {
673 arg_parts.push_back(absl::StrCat(key,
"=", value));
675 std::string args_str = absl::StrJoin(arg_parts,
", ");
680 if (!tool_result_or.ok()) {
682 tool_result_or.status().message()));
683 return absl::InternalError(absl::StrCat(
684 "Tool execution failed: ", tool_result_or.status().message()));
687 const std::string& tool_output = tool_result_or.value();
688 if (!tool_output.empty()) {
696 std::string preview = tool_output.substr(
697 0, std::min(
size_t(200), tool_output.size()));
698 if (tool_output.size() > 200)
706 std::string marked_output = absl::StrCat(
707 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
708 "The tool returned the following data:\n", tool_output,
"\n\n",
709 "Please provide a text_response field in your JSON to summarize "
710 "this information for the user.");
711 auto tool_result_msg =
713 tool_result_msg.is_internal =
715 history_.push_back(tool_result_msg);
717 executed_tool =
true;
718 executed_tools.push_back(tool_call.tool_name);
723 waiting_for_text_response =
true;
730 if (waiting_for_text_response && agent_response.text_response.empty() &&
731 agent_response.commands.empty()) {
733 absl::StrCat(
"LLM did not provide text_response after receiving tool "
734 "results (Iteration: ",
735 iteration + 1,
"/", max_iterations,
")"));
740 std::optional<ProposalCreationResult> proposal_result;
741 absl::Status proposal_status = absl::OkStatus();
742 bool attempted_proposal =
false;
744 if (!agent_response.commands.empty()) {
745 attempted_proposal =
true;
748 proposal_status = absl::FailedPreconditionError(
749 "No ROM context available for proposal creation");
751 "Cannot create proposal because no ROM context is active.");
754 absl::FailedPreconditionError(
"ROM context is not loaded");
756 "Cannot create proposal because the ROM context is not loaded.");
763 request.
ai_provider = absl::GetFlag(FLAGS_ai_provider);
766 if (!creation_or.ok()) {
767 proposal_status = creation_or.status();
769 proposal_status.message()));
771 proposal_result = std::move(creation_or.value());
774 "Created proposal ", proposal_result->metadata.id,
" with ",
775 proposal_result->change_count,
" change(s)."));
781 std::string response_text = agent_response.text_response;
782 if (!agent_response.reasoning.empty()) {
783 if (!response_text.empty()) {
784 response_text.append(
"\n\n");
786 response_text.append(
"Reasoning: ");
787 response_text.append(agent_response.reasoning);
789 const int executable_commands =
790 CountExecutableCommands(agent_response.commands);
791 if (!agent_response.commands.empty()) {
792 if (!response_text.empty()) {
793 response_text.append(
"\n\n");
795 response_text.append(
"Commands:\n");
796 response_text.append(absl::StrJoin(agent_response.commands,
"\n"));
800 if (proposal_result.has_value()) {
801 const auto& metadata = proposal_result->metadata;
802 if (!response_text.empty()) {
803 response_text.append(
"\n\n");
805 response_text.append(absl::StrFormat(
806 "✅ Proposal %s ready with %d change%s (%d command%s).\n"
807 "Review it in the Proposal drawer or run `z3ed agent diff "
808 "--proposal-id %s`.\n"
809 "Sandbox ROM: %s\nProposal JSON: %s",
810 metadata.id, proposal_result->change_count,
811 proposal_result->change_count == 1 ?
"" :
"s",
812 proposal_result->executed_commands,
813 proposal_result->executed_commands == 1 ?
"" :
"s", metadata.id,
814 metadata.sandbox_rom_path.string(),
815 proposal_result->proposal_json_path.string()));
817 }
else if (attempted_proposal && !proposal_status.ok()) {
818 if (!response_text.empty()) {
819 response_text.append(
"\n\n");
821 response_text.append(
822 absl::StrCat(
"⚠️ Failed to prepare a proposal automatically: ",
823 proposal_status.message()));
827 if (proposal_result.has_value()) {
829 summary.
id = proposal_result->metadata.id;
840 if (!agent_response.warnings.empty()) {
841 chat_response.
warnings = agent_response.warnings;
844 meta.
provider = !agent_response.provider.empty()
845 ? agent_response.provider
847 meta.
model = !agent_response.model.empty() ? agent_response.model
850 agent_response.latency_seconds > 0.0
851 ? agent_response.latency_seconds
852 : absl::ToDoubleSeconds(absl::Now() - turn_start);
859 return chat_response;
862 return absl::InternalError(
863 "Agent did not produce a response after executing tools.");