300 bool executed_tool =
false;
301 std::vector<std::string> executed_tools;
304 for (
const auto& tool_call : agent_response.
tool_calls) {
306 std::vector<std::string> arg_parts;
307 for (
const auto& [key, value] : tool_call.args) {
308 arg_parts.push_back(absl::StrCat(key,
"=", value));
310 std::string args_str = absl::StrJoin(arg_parts,
", ");
315 std::string tool_output;
316 if (!tool_result_or.ok()) {
318 absl::StrCat(
"Error: ", tool_result_or.status().message());
321 tool_output = tool_result_or.value();
325 if (!tool_output.empty()) {
328 std::string marked_output = absl::StrCat(
329 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
330 "The tool returned the following data:\n", tool_output,
"\n\n",
331 "Please provide a text_response field in your JSON to summarize "
332 "this information for the user.");
333 auto tool_result_msg =
335 tool_result_msg.is_internal =
true;
336 history_.push_back(tool_result_msg);
338 executed_tool =
true;
339 executed_tools.push_back(tool_call.tool_name);
350 std::optional<ProposalCreationResult> proposal_result;
351 absl::Status proposal_status = absl::OkStatus();
352 bool attempted_proposal =
false;
354 if (!agent_response.
commands.empty()) {
355 attempted_proposal =
true;
362 request.
prompt = it->message;
373 if (!creation_or.ok()) {
374 proposal_status = creation_or.status();
376 proposal_status.message()));
378 proposal_result = std::move(creation_or.value());
386 if (!response_text.empty())
387 response_text.append(
"\n\n");
388 response_text.append(
"Reasoning: ").append(agent_response.
reasoning);
391 if (!agent_response.
commands.empty()) {
392 if (!response_text.empty())
393 response_text.append(
"\n\n");
394 response_text.append(
"Commands:\n")
395 .append(absl::StrJoin(agent_response.
commands,
"\n"));
398 CountExecutableCommands(agent_response.
commands);
400 if (proposal_result.has_value()) {
401 const auto& metadata = proposal_result->metadata;
402 if (!response_text.empty())
403 response_text.append(
"\n\n");
404 response_text.append(
405 absl::StrFormat(
"✅ Proposal %s ready with %d change%s (%d command%s).",
406 metadata.id, proposal_result->change_count,
407 proposal_result->change_count == 1 ?
"" :
"s",
408 proposal_result->executed_commands,
409 proposal_result->executed_commands == 1 ?
"" :
"s"));
411 }
else if (attempted_proposal && !proposal_status.ok()) {
412 if (!response_text.empty())
413 response_text.append(
"\n\n");
414 response_text.append(absl::StrCat(
"⚠️ Failed to prepare proposal: ",
415 proposal_status.message()));
421 history_.back().message ==
"Thinking...") {
428 if (proposal_result.has_value()) {
430 summary.
id = proposal_result->metadata.id;
439 meta.
model =
"gemini";
451 const std::string& message) {
452 if (message.empty() &&
history_.empty()) {
453 return absl::InvalidArgumentError(
454 "Conversation must start with a non-empty message.");
457 if (!message.empty()) {
472 bool waiting_for_text_response =
false;
473 absl::Time turn_start = absl::Now();
474 std::vector<std::string> executed_tools;
477 util::PrintInfo(absl::StrCat(
"Starting agent loop (max ", max_iterations,
480 absl::StrCat(
"History size: ",
history_.size(),
" messages"));
483 for (
int iteration = 0; iteration < max_iterations; ++iteration) {
492 waiting_for_text_response ?
"Generating final response..."
500 if (!response_or.ok()) {
502 response_or.status().message()));
503 return absl::InternalError(absl::StrCat(
"Failed to get AI response: ",
504 response_or.status().message()));
507 const auto& agent_response = response_or.value();
512 <<
" - Tool calls: " << agent_response.tool_calls.size()
515 <<
" - Commands: " << agent_response.commands.size()
518 << (agent_response.text_response.empty() ?
"empty" :
"present")
528 if (!agent_response.tool_calls.empty()) {
531 if (waiting_for_text_response) {
533 absl::StrCat(
"LLM called tools again instead of providing final "
534 "response (Iteration: ",
535 iteration + 1,
"/", max_iterations,
")"));
538 bool executed_tool =
false;
539 for (
const auto& tool_call : agent_response.tool_calls) {
541 std::vector<std::string> arg_parts;
542 for (
const auto& [key, value] : tool_call.args) {
543 arg_parts.push_back(absl::StrCat(key,
"=", value));
545 std::string args_str = absl::StrJoin(arg_parts,
", ");
550 if (!tool_result_or.ok()) {
552 tool_result_or.status().message()));
553 return absl::InternalError(absl::StrCat(
554 "Tool execution failed: ", tool_result_or.status().message()));
557 const std::string& tool_output = tool_result_or.value();
558 if (!tool_output.empty()) {
566 std::string preview = tool_output.substr(
567 0, std::min(
size_t(200), tool_output.size()));
568 if (tool_output.size() > 200)
576 std::string marked_output = absl::StrCat(
577 "[TOOL RESULT for ", tool_call.tool_name,
"]\n",
578 "The tool returned the following data:\n", tool_output,
"\n\n",
579 "Please provide a text_response field in your JSON to summarize "
580 "this information for the user.");
581 auto tool_result_msg =
583 tool_result_msg.is_internal =
585 history_.push_back(tool_result_msg);
587 executed_tool =
true;
588 executed_tools.push_back(tool_call.tool_name);
593 waiting_for_text_response =
true;
600 if (waiting_for_text_response && agent_response.text_response.empty() &&
601 agent_response.commands.empty()) {
603 absl::StrCat(
"LLM did not provide text_response after receiving tool "
604 "results (Iteration: ",
605 iteration + 1,
"/", max_iterations,
")"));
610 std::optional<ProposalCreationResult> proposal_result;
611 absl::Status proposal_status = absl::OkStatus();
612 bool attempted_proposal =
false;
614 if (!agent_response.commands.empty()) {
615 attempted_proposal =
true;
618 proposal_status = absl::FailedPreconditionError(
619 "No ROM context available for proposal creation");
621 "Cannot create proposal because no ROM context is active.");
624 absl::FailedPreconditionError(
"ROM context is not loaded");
626 "Cannot create proposal because the ROM context is not loaded.");
633 request.
ai_provider = absl::GetFlag(FLAGS_ai_provider);
636 if (!creation_or.ok()) {
637 proposal_status = creation_or.status();
639 proposal_status.message()));
641 proposal_result = std::move(creation_or.value());
644 "Created proposal ", proposal_result->metadata.id,
" with ",
645 proposal_result->change_count,
" change(s)."));
651 std::string response_text = agent_response.text_response;
652 if (!agent_response.reasoning.empty()) {
653 if (!response_text.empty()) {
654 response_text.append(
"\n\n");
656 response_text.append(
"Reasoning: ");
657 response_text.append(agent_response.reasoning);
659 const int executable_commands =
660 CountExecutableCommands(agent_response.commands);
661 if (!agent_response.commands.empty()) {
662 if (!response_text.empty()) {
663 response_text.append(
"\n\n");
665 response_text.append(
"Commands:\n");
666 response_text.append(absl::StrJoin(agent_response.commands,
"\n"));
670 if (proposal_result.has_value()) {
671 const auto& metadata = proposal_result->metadata;
672 if (!response_text.empty()) {
673 response_text.append(
"\n\n");
675 response_text.append(absl::StrFormat(
676 "✅ Proposal %s ready with %d change%s (%d command%s).\n"
677 "Review it in the Proposal drawer or run `z3ed agent diff "
678 "--proposal-id %s`.\n"
679 "Sandbox ROM: %s\nProposal JSON: %s",
680 metadata.id, proposal_result->change_count,
681 proposal_result->change_count == 1 ?
"" :
"s",
682 proposal_result->executed_commands,
683 proposal_result->executed_commands == 1 ?
"" :
"s", metadata.id,
684 metadata.sandbox_rom_path.string(),
685 proposal_result->proposal_json_path.string()));
687 }
else if (attempted_proposal && !proposal_status.ok()) {
688 if (!response_text.empty()) {
689 response_text.append(
"\n\n");
691 response_text.append(
692 absl::StrCat(
"⚠️ Failed to prepare a proposal automatically: ",
693 proposal_status.message()));
697 if (proposal_result.has_value()) {
699 summary.
id = proposal_result->metadata.id;
710 if (!agent_response.warnings.empty()) {
711 chat_response.
warnings = agent_response.warnings;
714 meta.
provider = !agent_response.provider.empty()
715 ? agent_response.provider
717 meta.
model = !agent_response.model.empty() ? agent_response.model
720 agent_response.latency_seconds > 0.0
721 ? agent_response.latency_seconds
722 : absl::ToDoubleSeconds(absl::Now() - turn_start);
729 return chat_response;
732 return absl::InternalError(
733 "Agent did not produce a response after executing tools.");