diff --git a/enterprise/app/services/captain/assistant/agent_runner_service.rb b/enterprise/app/services/captain/assistant/agent_runner_service.rb index 9daf769e0..95cca90cc 100644 --- a/enterprise/app/services/captain/assistant/agent_runner_service.rb +++ b/enterprise/app/services/captain/assistant/agent_runner_service.rb @@ -47,7 +47,11 @@ class Captain::Assistant::AgentRunnerService runner = add_usage_metadata_callback(runner) runner = add_callbacks_to_runner(runner) if @callbacks.any? install_instrumentation(runner) - result = runner.run(message_to_process, context: context, max_turns: 100) + # max_turns is the hard safety cap: each "turn" = one LLM call + optional tool calls. + # 100 allowed runaway loops (LLM calling faq_lookup indefinitely when confused). + # 15 is plenty for normal flows (greeting -> handoff -> coleta -> tool calls -> resposta) + # while keeping a burn-budget ceiling per message. + result = runner.run(message_to_process, context: context, max_turns: 15) process_agent_result(result, original_query: message_to_process) rescue StandardError => e @@ -373,14 +377,17 @@ class Captain::Assistant::AgentRunnerService assistant_agent = build_orchestrator_agent_with_memory scenario_agents = @assistant.scenarios.enabled.map(&:agent) - # Orchestrator can hand off INTO any scenario. Scenarios do NOT hand off - # back to the orchestrator — that creates a ping-pong where the scenario - # calls handoff_to_jasmine mid-flow, the orchestrator resumes the turn, - # and responses get duplicated or routed through the FAQ guardrail. When - # a customer changes topic mid-scenario, pick_starting_agent on the next - # turn already routes back to the orchestrator based on conversation - # state — no manual handoff needed from the scenario side. + # Bidirectional handoff: orchestrator -> scenarios AND scenarios -> orchestrator. + # Historical note: removing the back-edge looks attractive (prevents ping-pong) + # but in practice the scenario LLM uses handoff_to_orchestrator as a "fallback" + # when it gets confused. Without that fallback, the LLM keeps calling other + # available tools (faq_lookup, etc.) in a loop — observed real-world incident + # where Daniela called faq_lookup dozens of times in a runaway. Keep the edge. + # Ping-pong is instead contained by max_turns in generate_response AND by + # explicit prompt rules in the scenario instruction forbidding gratuitous + # handoffs. assistant_agent.register_handoffs(*scenario_agents) if scenario_agents.any? + scenario_agents.each { |scenario_agent| scenario_agent.register_handoffs(assistant_agent) } [assistant_agent] + scenario_agents end