414 bool executed_tool =
false;
415 std::vector<std::string> executed_tools;
418 for (
const auto& tool_call : agent_response.
tool_calls) {
420 std::vector<std::string> arg_parts;
421 for (
const auto& [key, value] : tool_call.args) {
422 arg_parts.push_back(absl::StrCat(key,
"=", value));
424 std::string args_str = absl::StrJoin(arg_parts,
", ");
429 std::string tool_output;
430 if (!tool_result_or.ok()) {
432 absl::StrCat(
"Error: ", tool_result_or.status().message());
435 tool_output = tool_result_or.value();
439 if (!tool_output.empty()) {
442 std::string marked_output = absl::StrCat(
443 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
444 "The tool returned the following data:\n", tool_output,
"\n\n",
445 "Please provide a text_response field in your JSON to summarize "
446 "this information for the user.");
447 auto tool_result_msg =
449 tool_result_msg.is_internal =
true;
450 history_.push_back(tool_result_msg);
452 executed_tool =
true;
453 executed_tools.push_back(tool_call.tool_name);
464 std::optional<ProposalCreationResult> proposal_result;
465 absl::Status proposal_status = absl::OkStatus();
466 bool attempted_proposal =
false;
468 if (!agent_response.
commands.empty()) {
469 attempted_proposal =
true;
476 request.
prompt = it->message;
487 if (!creation_or.ok()) {
488 proposal_status = creation_or.status();
490 proposal_status.message()));
492 proposal_result = std::move(creation_or.value());
500 if (!response_text.empty())
501 response_text.append(
"\n\n");
502 response_text.append(
"Reasoning: ").append(agent_response.
reasoning);
505 if (!agent_response.
commands.empty()) {
506 if (!response_text.empty())
507 response_text.append(
"\n\n");
508 response_text.append(
"Commands:\n")
509 .append(absl::StrJoin(agent_response.
commands,
"\n"));
512 CountExecutableCommands(agent_response.
commands);
514 if (proposal_result.has_value()) {
515 const auto& metadata = proposal_result->metadata;
516 if (!response_text.empty())
517 response_text.append(
"\n\n");
518 response_text.append(
519 absl::StrFormat(
"✅ Proposal %s ready with %d change%s (%d command%s).",
520 metadata.id, proposal_result->change_count,
521 proposal_result->change_count == 1 ?
"" :
"s",
522 proposal_result->executed_commands,
523 proposal_result->executed_commands == 1 ?
"" :
"s"));
525 }
else if (attempted_proposal && !proposal_status.ok()) {
526 if (!response_text.empty())
527 response_text.append(
"\n\n");
528 response_text.append(absl::StrCat(
"⚠️ Failed to prepare proposal: ",
529 proposal_status.message()));
535 history_.back().message ==
"Thinking...") {
542 if (proposal_result.has_value()) {
544 summary.
id = proposal_result->metadata.id;
553 meta.model =
"gemini";
554 meta.tool_names = executed_tools;
565 const std::string& message) {
566 if (message.empty() &&
history_.empty()) {
567 return absl::InvalidArgumentError(
568 "Conversation must start with a non-empty message.");
571 if (!message.empty()) {
573 const std::string auto_hook =
575 if (!auto_hook.empty()) {
577 hook_message.is_internal =
true;
578 history_.push_back(std::move(hook_message));
596 bool waiting_for_text_response =
false;
597 absl::Time turn_start = absl::Now();
598 std::vector<std::string> executed_tools;
601 util::PrintInfo(absl::StrCat(
"Starting agent loop (max ", max_iterations,
604 absl::StrCat(
"History size: ",
history_.size(),
" messages"));
607 for (
int iteration = 0; iteration < max_iterations; ++iteration) {
616 waiting_for_text_response ?
"Generating final response..."
624 if (!response_or.ok()) {
626 response_or.status().message()));
627 return absl::InternalError(absl::StrCat(
"Failed to get AI response: ",
628 response_or.status().message()));
631 const auto& agent_response = response_or.value();
636 <<
" - Tool calls: " << agent_response.tool_calls.size()
639 <<
" - Commands: " << agent_response.commands.size()
642 << (agent_response.text_response.empty() ?
"empty" :
"present")
652 if (!agent_response.tool_calls.empty()) {
655 if (waiting_for_text_response) {
657 absl::StrCat(
"LLM called tools again instead of providing final "
658 "response (Iteration: ",
659 iteration + 1,
"/", max_iterations,
")"));
662 bool executed_tool =
false;
663 for (
const auto& tool_call : agent_response.tool_calls) {
665 std::vector<std::string> arg_parts;
666 for (
const auto& [key, value] : tool_call.args) {
667 arg_parts.push_back(absl::StrCat(key,
"=", value));
669 std::string args_str = absl::StrJoin(arg_parts,
", ");
674 if (!tool_result_or.ok()) {
676 tool_result_or.status().message()));
677 return absl::InternalError(absl::StrCat(
678 "Tool execution failed: ", tool_result_or.status().message()));
681 const std::string& tool_output = tool_result_or.value();
682 if (!tool_output.empty()) {
690 std::string preview = tool_output.substr(
691 0, std::min(
size_t(200), tool_output.size()));
692 if (tool_output.size() > 200)
700 std::string marked_output = absl::StrCat(
701 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
702 "The tool returned the following data:\n", tool_output,
"\n\n",
703 "Please provide a text_response field in your JSON to summarize "
704 "this information for the user.");
705 auto tool_result_msg =
707 tool_result_msg.is_internal =
709 history_.push_back(tool_result_msg);
711 executed_tool =
true;
712 executed_tools.push_back(tool_call.tool_name);
717 waiting_for_text_response =
true;
724 if (waiting_for_text_response && agent_response.text_response.empty() &&
725 agent_response.commands.empty()) {
727 absl::StrCat(
"LLM did not provide text_response after receiving tool "
728 "results (Iteration: ",
729 iteration + 1,
"/", max_iterations,
")"));
734 std::optional<ProposalCreationResult> proposal_result;
735 absl::Status proposal_status = absl::OkStatus();
736 bool attempted_proposal =
false;
738 if (!agent_response.commands.empty()) {
739 attempted_proposal =
true;
742 proposal_status = absl::FailedPreconditionError(
743 "No ROM context available for proposal creation");
745 "Cannot create proposal because no ROM context is active.");
748 absl::FailedPreconditionError(
"ROM context is not loaded");
750 "Cannot create proposal because the ROM context is not loaded.");
757 request.
ai_provider = absl::GetFlag(FLAGS_ai_provider);
760 if (!creation_or.ok()) {
761 proposal_status = creation_or.status();
763 proposal_status.message()));
765 proposal_result = std::move(creation_or.value());
768 "Created proposal ", proposal_result->metadata.id,
" with ",
769 proposal_result->change_count,
" change(s)."));
775 std::string response_text = agent_response.text_response;
776 if (!agent_response.reasoning.empty()) {
777 if (!response_text.empty()) {
778 response_text.append(
"\n\n");
780 response_text.append(
"Reasoning: ");
781 response_text.append(agent_response.reasoning);
783 const int executable_commands =
784 CountExecutableCommands(agent_response.commands);
785 if (!agent_response.commands.empty()) {
786 if (!response_text.empty()) {
787 response_text.append(
"\n\n");
789 response_text.append(
"Commands:\n");
790 response_text.append(absl::StrJoin(agent_response.commands,
"\n"));
794 if (proposal_result.has_value()) {
795 const auto& metadata = proposal_result->metadata;
796 if (!response_text.empty()) {
797 response_text.append(
"\n\n");
799 response_text.append(absl::StrFormat(
800 "✅ Proposal %s ready with %d change%s (%d command%s).\n"
801 "Review it in the Proposal drawer or run `z3ed agent diff "
802 "--proposal-id %s`.\n"
803 "Sandbox ROM: %s\nProposal JSON: %s",
804 metadata.id, proposal_result->change_count,
805 proposal_result->change_count == 1 ?
"" :
"s",
806 proposal_result->executed_commands,
807 proposal_result->executed_commands == 1 ?
"" :
"s", metadata.id,
808 metadata.sandbox_rom_path.string(),
809 proposal_result->proposal_json_path.string()));
811 }
else if (attempted_proposal && !proposal_status.ok()) {
812 if (!response_text.empty()) {
813 response_text.append(
"\n\n");
815 response_text.append(
816 absl::StrCat(
"⚠️ Failed to prepare a proposal automatically: ",
817 proposal_status.message()));
821 if (proposal_result.has_value()) {
823 summary.
id = proposal_result->metadata.id;
834 if (!agent_response.warnings.empty()) {
835 chat_response.
warnings = agent_response.warnings;
839 ? agent_response.provider
841 meta.model = !agent_response.model.empty() ? agent_response.model
843 meta.latency_seconds =
844 agent_response.latency_seconds > 0.0
845 ? agent_response.latency_seconds
846 : absl::ToDoubleSeconds(absl::Now() - turn_start);
848 meta.tool_names = executed_tools;
849 meta.parameters = agent_response.parameters;
853 return chat_response;
856 return absl::InternalError(
857 "Agent did not produce a response after executing tools.");