diff --git a/agent_framework_oci/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflows/agent_graph.py index 0dcd6b49..ed100f5c 100644 --- a/agent_framework_oci/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflows/agent_graph.py @@ -248,6 +248,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -266,6 +288,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -412,7 +438,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -576,6 +603,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -667,6 +695,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py index 0dcd6b49..ed100f5c 100644 --- a/agent_framework_oci/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Deterministic_Transactional_Workflow/agent_template_backend/app/workflows/agent_graph.py @@ -248,6 +248,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -266,6 +288,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -412,7 +438,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -576,6 +603,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -667,6 +695,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/External_Guardrails_Judges/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/External_Guardrails_Judges/agent_template_backend/app/workflows/agent_graph.py index 8be1347c..343ddf4c 100644 --- a/agent_framework_oci/Tuning-Performance/External_Guardrails_Judges/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/External_Guardrails_Judges/agent_template_backend/app/workflows/agent_graph.py @@ -246,6 +246,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -264,6 +286,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -410,7 +436,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -574,6 +601,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -665,6 +693,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/agent_graph.py index 0dcd6b49..ed100f5c 100644 --- a/agent_framework_oci/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Long_Term_Memory/templates/agent_template_backend/app/workflows/agent_graph.py @@ -248,6 +248,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -266,6 +288,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -412,7 +438,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -576,6 +603,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -667,6 +695,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend/app/workflows/agent_graph.py index 8be1347c..343ddf4c 100644 --- a/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend/app/workflows/agent_graph.py @@ -246,6 +246,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -264,6 +286,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -410,7 +436,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -574,6 +601,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -665,6 +693,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py index 8be1347c..343ddf4c 100644 --- a/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Normal/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py @@ -246,6 +246,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -264,6 +286,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -410,7 +436,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -574,6 +601,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -665,6 +693,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/workflows/agent_graph.py index 8be1347c..343ddf4c 100644 --- a/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend/app/workflows/agent_graph.py @@ -246,6 +246,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -264,6 +286,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -410,7 +436,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -574,6 +601,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -665,6 +693,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py index 8be1347c..343ddf4c 100644 --- a/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Route_Stickness/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py @@ -246,6 +246,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -264,6 +286,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -410,7 +436,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -574,6 +601,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -665,6 +693,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/workflows/agent_graph.py index cd16da66..4df9b201 100644 --- a/agent_framework_oci/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/workflows/agent_graph.py @@ -248,6 +248,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -266,6 +288,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -412,7 +438,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -576,6 +603,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -667,6 +695,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py index cd16da66..4df9b201 100644 --- a/agent_framework_oci/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py @@ -248,6 +248,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -266,6 +288,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -412,7 +438,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -576,6 +603,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -667,6 +695,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/libs/agent_framework/build/lib/agent_framework/guardrails/rails.py b/agent_framework_oci/libs/agent_framework/build/lib/agent_framework/guardrails/rails.py index 187e5570..18b3fe9c 100644 --- a/agent_framework_oci/libs/agent_framework/build/lib/agent_framework/guardrails/rails.py +++ b/agent_framework_oci/libs/agent_framework/build/lib/agent_framework/guardrails/rails.py @@ -338,9 +338,43 @@ class LoopRail(Guardrail): code = "VLOOP" stage = "input" + @staticmethod + def _transaction_status(ctx: dict[str, Any]) -> str: + nonterminal = { + "COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING", + "PAUSED", "WAITING_INPUT", + } + for result in reversed(list(ctx.get("mcp_results") or [])): + if isinstance(result, dict): + result_status = str(result.get("transaction_status") or "").strip().upper() + if result_status in nonterminal: + return result_status + active_transaction = ctx.get("active_transaction") + if isinstance(active_transaction, dict): + active_status = str(active_transaction.get("status") or "").strip().upper() + if active_status in nonterminal: + return active_status + return str(ctx.get("transaction_status") or "").strip().upper() + async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision: + ctx = _ctx(context) + transaction_status = self._transaction_status(ctx) + if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + return RailDecision( + code=self.code, + allowed=True, + reason=f"continuidade_transacional:{transaction_status}", + sanitized_text=text, + metadata={ + "history_window": len(list(ctx.get("history_texts") or [])[-6:]), + "repeated": False, + "mechanism": "deterministic_transaction_bypass", + "transaction_status": transaction_status, + "calibrated": True, + }, + ) normalized = _lower(text).strip() - history = [_lower(h).strip() for h in _ctx(context).get("history_texts", [])[-6:]] + history = [_lower(h).strip() for h in ctx.get("history_texts", [])[-6:]] repeated = history.count(normalized) >= 2 if normalized else False return RailDecision( code=self.code, diff --git a/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py b/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py index 9a32e5bb..676683b5 100644 --- a/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py +++ b/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py @@ -358,30 +358,38 @@ class LoopRail(Guardrail): @staticmethod def _transaction_status(ctx: dict[str, Any]) -> str: - status = str(ctx.get("transaction_status") or "").strip().upper() - if status: - return status + nonterminal = { + "COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING", + "PAUSED", "WAITING_INPUT", + } + # A queued multi-intent transaction may be promoted immediately after + # the previous one completed. Prefer the newest live MCP evidence over + # a stale top-level COMPLETED status left by checkpoint merge order. + for result in reversed(list(ctx.get("mcp_results") or [])): + if isinstance(result, dict): + result_status = str(result.get("transaction_status") or "").strip().upper() + if result_status in nonterminal: + return result_status active_transaction = ctx.get("active_transaction") if isinstance(active_transaction, dict): - return str(active_transaction.get("status") or "").strip().upper() - return "" + active_status = str(active_transaction.get("status") or "").strip().upper() + if active_status in nonterminal: + return active_status + return str(ctx.get("transaction_status") or "").strip().upper() async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision: ctx = _ctx(context) transaction_status = self._transaction_status(ctx) - # A short confirmation (for example ``sim``/``não``) may legitimately - # appear several times in the same session because each transaction has - # its own confirmation boundary. VLOOP protects against conversational - # repetition; it must not consume the input that belongs to an active - # transaction awaiting confirmation. The transaction runtime/classifier - # remains responsible for deciding whether the utterance is actually a - # valid confirm/reject response. - if transaction_status == "AWAITING_CONFIRMATION": + # Parameter values and short confirmations may legitimately repeat in + # the same session. VLOOP must not consume input owned by the active + # transaction contract; extraction/confirmation remains responsible + # for deciding whether the value is valid. + if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: return RailDecision( code=self.code, allowed=True, - reason="continuidade_transacional:AWAITING_CONFIRMATION", + reason=f"continuidade_transacional:{transaction_status}", sanitized_text=text, metadata={ "history_window": len(list(ctx.get("history_texts") or [])[-6:]), diff --git a/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/multi_intent.py b/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/multi_intent.py index ed64cf8a..c35ec95b 100644 --- a/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/multi_intent.py +++ b/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/multi_intent.py @@ -69,17 +69,30 @@ class MultiIntentPlanner: def _intent_for_clause(self, clause: str) -> IntentDefinition | None: normalized = self._normalize(clause) - clause_tokens = set(re.findall(r"[\w]+", normalized, flags=re.UNICODE)) + ordered_clause_tokens = re.findall(r"[\w]+", normalized, flags=re.UNICODE) + clause_tokens = set(ordered_clause_tokens) matches: list[tuple[int, int, IntentDefinition]] = [] for intent in self.intents: for keyword in intent.keywords: token = self._normalize(keyword) keyword_tokens = re.findall(r"[\w]+", token, flags=re.UNICODE) + # Expressões configuradas representam conceitos, não somente + # substrings literais. Preserve a ordem dos termos e aceite + # qualificadores intermediários: "cancelar pedido" também + # reconhece "cancelar meu pedido", sem enumerar variações no YAML. + cursor = 0 + ordered_match = bool(keyword_tokens) + for keyword_token in keyword_tokens: + try: + cursor = ordered_clause_tokens.index(keyword_token, cursor) + 1 + except ValueError: + ordered_match = False + break matched = bool( keyword_tokens and ( (len(keyword_tokens) == 1 and keyword_tokens[0] in clause_tokens) - or (len(keyword_tokens) > 1 and token in normalized) + or (len(keyword_tokens) > 1 and ordered_match) ) ) if matched: @@ -146,7 +159,7 @@ class MultiIntentPlanner: if len(operations) < 2: return None transactional = [item for item in operations if self._is_transactional(item)] - if len(transactional) == 1: + if transactional: primary_operation = transactional[0] operations.sort(key=lambda item: item is not primary_operation) for operation in operations[1:]: diff --git a/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/pending_topics.py b/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/pending_topics.py index 37693c48..628b6647 100644 --- a/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/pending_topics.py +++ b/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/pending_topics.py @@ -16,6 +16,29 @@ class PendingTopicDrain: handled_topics: list[dict[str, Any]] mcp_results: list[dict[str, Any]] rag_results: list[dict[str, Any]] + state_patch: dict[str, Any] + + +_LIVE_TRANSACTION_STATUSES = { + "COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "WAITING_CONFIRMATION", + "PENDING", "RUNNING", "IN_PROGRESS", +} + +_TRANSACTION_STATE_KEYS = { + "active_transaction", "transaction_status", "transaction_pre_validation", + "confirmation_required", "confirmation_received", "selected_tool_call", + "pending_tool_call", "pending_tool_clarification", "pending_domain_workflow", + "missing_parameters", "tool_policy_result", "next_state", "workflow_id", +} + + +def _has_live_transaction(state: dict[str, Any]) -> bool: + active = state.get("active_transaction") + active_status = active.get("status") if isinstance(active, dict) else None + status = str(active_status or state.get("transaction_status") or "").upper() + if status: + return status in _LIVE_TRANSACTION_STATUSES + return bool(state.get("confirmation_required") or state.get("pending_tool_call")) async def drain_pending_topics( @@ -34,6 +57,8 @@ async def drain_pending_topics( combined_mcp_results = list(state.get("mcp_results") or []) combined_rag_results = list(state.get("rag_results") or []) secondary_answers: list[str] = [] + state_patch: dict[str, Any] = {} + may_promote_deferred = not _has_live_transaction(state) agent_responses = list(state.get("agent_responses") or []) if not agent_responses and str(candidate or "").strip(): primary_agent = str( @@ -53,9 +78,15 @@ async def drain_pending_topics( }) for topic in topics: disposition = topic.get("disposition") + promoted = False if disposition == "defer": - remaining.append(topic) - continue + if not may_promote_deferred: + remaining.append(topic) + continue + topic = {**topic, "disposition": "execute", "status": "pending"} + disposition = "execute" + promoted = True + may_promote_deferred = False if disposition != "execute": handled.append({**topic, "status": "completed"}) continue @@ -123,14 +154,42 @@ async def drain_pending_topics( if rag_evidence not in combined_rag_results: combined_rag_results.append(rag_evidence) secondary_answers.append(answer) + if promoted: + agent_responses = [{**item, "primary": False} for item in agent_responses] agent_responses.append({ "operation_id": topic.get("operation_id"), "agent": topic.get("agent"), "intent": topic.get("intent"), "answer": answer, - "primary": False, + "primary": promoted, "status": "completed", }) + if promoted: + for key in _TRANSACTION_STATE_KEYS: + if key in (result or {}): + state_patch[key] = result[key] + state_patch.update({ + "route": topic.get("agent"), + "active_agent": topic.get("agent"), + "intent": topic.get("intent"), + "domain": topic.get("domain"), + "mcp_tools": topic.get("tools") or [], + # Replace the completed transaction's continuity snapshot so + # the next reply cannot be routed back to the previous agent. + "route_decision": { + "route": topic.get("agent"), + "agent": topic.get("agent"), + "intent": topic.get("intent"), + "domain": topic.get("domain"), + "mcp_tools": topic.get("tools") or [], + "next_state": (result or {}).get("next_state"), + "metadata": { + "multi_intent_secondary": True, + "promoted_from_pending_topics": True, + "operation_id": topic.get("operation_id"), + }, + }, + }) handled.append({**topic, "status": "completed"}) secondary_answers.extend(MultiIntentPlanner.public_messages(plan)) @@ -148,4 +207,5 @@ async def drain_pending_topics( handled_topics=handled, mcp_results=combined_mcp_results, rag_results=combined_rag_results, + state_patch=state_patch, ) diff --git a/agent_framework_oci/templates/agent_template_backend/app/workflows/agent_graph.py b/agent_framework_oci/templates/agent_template_backend/app/workflows/agent_graph.py index 33f65688..3a42d1c1 100644 --- a/agent_framework_oci/templates/agent_template_backend/app/workflows/agent_graph.py +++ b/agent_framework_oci/templates/agent_template_backend/app/workflows/agent_graph.py @@ -249,6 +249,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -267,6 +289,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -412,7 +438,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -576,6 +603,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -655,6 +683,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py b/agent_framework_oci/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py index bcc4e6a6..9cb626ad 100644 --- a/agent_framework_oci/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py +++ b/agent_framework_oci/templates/agent_template_backend_day_zero/app/workflows/agent_graph.py @@ -247,6 +247,28 @@ class AgentWorkflow: input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] + guardrail_active_transaction = ( + state.get("active_transaction") + if isinstance(state.get("active_transaction"), dict) + else {} + ) + guardrail_transaction_status = str( + guardrail_active_transaction.get("status") + or state.get("transaction_status") + or "" + ).strip().upper() + pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {} + if state.get("confirmation_required") and pending_call.get("tool_name"): + guardrail_transaction_status = "AWAITING_CONFIRMATION" + elif state.get("missing_parameters") and pending_call.get("tool_name"): + guardrail_transaction_status = "COLLECTING_PARAMETERS" + for mcp_result in reversed(list(state.get("mcp_results") or [])): + if not isinstance(mcp_result, dict): + continue + mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper() + if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: + guardrail_transaction_status = mcp_status + break await self.observer.emit_grl( "001", { @@ -265,6 +287,10 @@ class AgentWorkflow: "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, + "transaction_status": guardrail_transaction_status, + "missing_parameters": list(state.get("missing_parameters") or []), + "active_transaction": guardrail_active_transaction, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -410,7 +436,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -574,6 +601,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { @@ -653,6 +681,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], } diff --git a/agent_framework_oci/tests/unit/test_multi_intent_planner.py b/agent_framework_oci/tests/unit/test_multi_intent_planner.py index d463d6bd..e452983e 100644 --- a/agent_framework_oci/tests/unit/test_multi_intent_planner.py +++ b/agent_framework_oci/tests/unit/test_multi_intent_planner.py @@ -40,6 +40,28 @@ def test_transactional_intent_is_primary_even_when_mentioned_second(): assert [item.intent for item in plan.operations] == ["cancel", "invoice"] +def test_multiword_keyword_accepts_intermediate_qualifier_without_yaml_variant(): + plan = _planner().plan("quero cancelar meu pedido e preciso da segunda via") + assert plan is not None + assert [item.intent for item in plan.operations] == ["cancel", "invoice"] + + +def test_first_of_multiple_transactions_is_primary_and_next_is_deferred(): + planner = MultiIntentPlanner([ + IntentDefinition(name="invoice", agent="billing", priority=30, keywords=["fatura"], mcp_tools=["consultar_fatura"]), + IntentDefinition(name="return", agent="support", priority=20, keywords=["devolver pedido"], mcp_tools=["solicitar_devolucao"]), + IntentDefinition(name="cancel", agent="orders", priority=10, keywords=["cancelar pedido"], mcp_tools=["cancelar_pedido"]), + ], transactional_tools={"solicitar_devolucao", "cancelar_pedido"}) + + plan = planner.plan( + "quero consultar minha fatura. quero devolver pedido. quero cancelar pedido" + ) + + assert plan is not None + assert [item.intent for item in plan.operations] == ["return", "invoice", "cancel"] + assert [item.disposition for item in plan.operations] == ["execute", "execute", "defer"] + + def test_sentence_boundary_does_not_leak_previous_intent_into_knowledge_query(): planner = MultiIntentPlanner([ IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["fatura"]), diff --git a/agent_framework_oci/tests/unit/test_pending_topics_isolation.py b/agent_framework_oci/tests/unit/test_pending_topics_isolation.py index 618fad2a..37d18ed6 100644 --- a/agent_framework_oci/tests/unit/test_pending_topics_isolation.py +++ b/agent_framework_oci/tests/unit/test_pending_topics_isolation.py @@ -66,3 +66,52 @@ async def test_secondary_topic_does_not_inherit_primary_transaction_state(): ] assert result.rag_results[0]["metadata"]["provider"] == "kbdb" assert result.rag_results[0]["context"] == "Documento recuperado da base de conhecimento." + + +@pytest.mark.asyncio +async def test_deferred_transaction_is_promoted_after_primary_completes(): + async def cancellation_agent(state): + assert state["user_text"] == "quero cancelar meu pedido" + return { + "answer": "Você confirma o cancelamento do pedido PED-1001?", + "transaction_status": "AWAITING_CONFIRMATION", + "confirmation_required": True, + "pending_tool_call": { + "tool_name": "cancelar_pedido", + "arguments": {"order_id": "PED-1001"}, + }, + "active_transaction": { + "tool_name": "cancelar_pedido", + "status": "AWAITING_CONFIRMATION", + }, + } + + state = { + "answer": "A devolução foi registrada com sucesso.", + "route": "support_agent", + "active_agent": "support_agent", + "intent": "retail_support_exchange_return", + "transaction_status": "COMPLETED", + "pending_topics": [{ + "operation_id": "op-3", + "intent": "retail_order_cancel", + "agent": "orders_agent", + "domain": "retail", + "tools": ["consultar_pedido", "cancelar_pedido"], + "source_text": "quero cancelar meu pedido", + "disposition": "defer", + "status": "pending", + }], + } + + result = await drain_pending_topics( + state, state["answer"], {"orders_agent": cancellation_agent} + ) + + assert result.pending_topics == [] + assert result.state_patch["route"] == "orders_agent" + assert result.state_patch["intent"] == "retail_order_cancel" + assert result.state_patch["transaction_status"] == "AWAITING_CONFIRMATION" + assert result.state_patch["pending_tool_call"]["tool_name"] == "cancelar_pedido" + assert result.agent_responses[-1]["primary"] is True + assert result.agent_responses[-1]["agent"] == "orders_agent" diff --git a/app/workflows/agent_graph.py b/app/workflows/agent_graph.py index 0d6ef84d..64765a44 100644 --- a/app/workflows/agent_graph.py +++ b/app/workflows/agent_graph.py @@ -435,6 +435,34 @@ class AgentWorkflow: "PAUSED", "WAITING_INPUT", } + # The most recent MCP result is authoritative for a transaction + # promoted after another transaction completed in the same + # multi-intent turn. Checkpoint merge order can otherwise leave + # the previous COMPLETED status at the top level and cause input + # guardrails to reject the new transaction's short confirmation. + latest_mcp_transaction = next( + ( + result + for result in reversed(list(state.get("mcp_results") or [])) + if isinstance(result, dict) + and str(result.get("transaction_status") or "").strip().upper() + in nonterminal_tx_statuses + ), + None, + ) + if latest_mcp_transaction: + latest_status = str(latest_mcp_transaction.get("transaction_status") or "").strip().upper() + tx_status = latest_status + state["transaction_status"] = latest_status + if not active_tx or active_tx_status not in nonterminal_tx_statuses: + active_tx = { + "transaction_id": str(latest_mcp_transaction.get("transaction_id") or ""), + "tool_name": str(latest_mcp_transaction.get("tool_name") or ""), + "arguments": dict(latest_mcp_transaction.get("arguments") or {}), + "status": latest_status, + } + state["active_transaction"] = active_tx + active_tx_status = latest_status # A new active transaction has precedence over terminal evidence from # the previous transaction in the same session. Without this guard, # a stale state["transaction_status"] == COMPLETED can tombstone an @@ -549,6 +577,7 @@ class AgentWorkflow: "transaction_status": state.get("transaction_status"), "missing_parameters": list(state.get("missing_parameters") or []), "active_transaction": state.get("active_transaction") or {}, + "mcp_results": list(state.get("mcp_results") or []), }, ) for _decision in decisions: @@ -729,7 +758,8 @@ class AgentWorkflow: } if decision.method == "continuity" else {}, "pending_topics": ( (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] - if (decision.metadata or {}).get("multi_intent_plan") else [] + if (decision.metadata or {}).get("multi_intent_plan") + else list(state.get("pending_topics") or []) ), } @@ -953,6 +983,7 @@ class AgentWorkflow: "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), + **drained.state_patch, } context = { **self._output_guardrail_context({ @@ -1033,6 +1064,7 @@ class AgentWorkflow: "agent_responses": drained.agent_responses, "mcp_results": drained.mcp_results, "rag_results": drained.rag_results, + **drained.state_patch, } async def output_guardrails(self, state): diff --git a/tests/migration/test_multi_intent_contas.py b/tests/migration/test_multi_intent_contas.py index a857809a..fcd1be56 100644 --- a/tests/migration/test_multi_intent_contas.py +++ b/tests/migration/test_multi_intent_contas.py @@ -1,5 +1,8 @@ from agent_framework.routing.config_loader import load_intents, load_multi_intent_config +import pytest + from agent_framework.routing.multi_intent import MultiIntentPlanner +from agent_framework.routing.pending_topics import drain_pending_topics from agent_framework.mcp.tool_policy import ToolPolicyRegistry @@ -53,3 +56,55 @@ def test_duas_mais_caras_sao_calculadas_sobre_evidencia_autorizada(): selected = _select_relative_invoice_charges("as duas mais caras", args) assert [row["value"] for row in selected] == [29.90, 24.90] assert [row["charge_date"] for row in selected] == ["02/09/26", "04/09/26"] + + +@pytest.mark.asyncio +async def test_promocao_transacional_substitui_snapshot_de_continuidade_anterior(): + async def orders_handler(state): + assert state["intent"] == "retail_order_cancel" + return { + "answer": "Informe o número do pedido que deseja cancelar.", + "transaction_status": "COLLECTING_PARAMETERS", + "active_transaction": { + "tool_name": "cancelar_pedido", + "status": "COLLECTING_PARAMETERS", + }, + "missing_parameters": ["order_id"], + "pending_tool_call": { + "tool_name": "cancelar_pedido", + "arguments": {}, + }, + "next_state": "COLLECTING_PARAMETERS", + } + + drained = await drain_pending_topics( + { + "answer": "Devolução concluída.", + "route": "support_agent", + "active_agent": "support_agent", + "intent": "retail_support_exchange_return", + "transaction_status": "COMPLETED", + "route_decision": { + "route": "support_agent", + "agent": "support_agent", + "intent": "state:WAITING_SUPPORT_CONFIRMATION", + }, + "pending_topics": [{ + "operation_id": "op-3", + "agent": "orders_agent", + "intent": "retail_order_cancel", + "domain": "retail", + "tools": ["consultar_pedido", "cancelar_pedido"], + "source_text": "quero cancelar pedido", + "disposition": "defer", + }], + }, + "Devolução concluída.", + {"orders_agent": orders_handler}, + ) + + assert drained.state_patch["route"] == "orders_agent" + assert drained.state_patch["transaction_status"] == "COLLECTING_PARAMETERS" + assert drained.state_patch["route_decision"]["agent"] == "orders_agent" + assert drained.state_patch["route_decision"]["intent"] == "retail_order_cancel" + assert drained.state_patch["route_decision"]["metadata"]["promoted_from_pending_topics"] is True diff --git a/tests/migration/test_vloop_transaction_confirmation_bypass.py b/tests/migration/test_vloop_transaction_confirmation_bypass.py index 3d5eb6bc..1bf8973e 100644 --- a/tests/migration/test_vloop_transaction_confirmation_bypass.py +++ b/tests/migration/test_vloop_transaction_confirmation_bypass.py @@ -32,6 +32,48 @@ async def test_vloop_reads_awaiting_confirmation_from_active_transaction(): assert decision.metadata["mechanism"] == "deterministic_transaction_bypass" +@pytest.mark.asyncio +async def test_vloop_allows_repeated_parameter_during_transaction_collection(): + rail = LoopRail() + decision = await rail.evaluate( + "PED-1001", + { + "transaction_status": "COLLECTING_PARAMETERS", + "missing_parameters": ["order_id"], + "history_texts": ["PED-1001", "outra fala", "PED-1001"], + }, + ) + assert decision.allowed is True + assert decision.reason == "continuidade_transacional:COLLECTING_PARAMETERS" + assert decision.metadata["repeated"] is False + assert decision.metadata["mechanism"] == "deterministic_transaction_bypass" + assert decision.metadata["transaction_status"] == "COLLECTING_PARAMETERS" + + +@pytest.mark.asyncio +async def test_vloop_prefers_latest_live_mcp_transaction_over_stale_completed_status(): + rail = LoopRail() + decision = await rail.evaluate( + "sim", + { + "transaction_status": "COMPLETED", + "active_transaction": {"status": "COMPLETED"}, + "mcp_results": [ + {"tool_name": "devolver_pedido", "transaction_status": "COMPLETED"}, + { + "tool_name": "cancelar_pedido", + "transaction_status": "AWAITING_CONFIRMATION", + "awaiting_confirmation": True, + }, + ], + "history_texts": ["sim", "outra fala", "sim"], + }, + ) + assert decision.allowed is True + assert decision.metadata["mechanism"] == "deterministic_transaction_bypass" + assert decision.metadata["transaction_status"] == "AWAITING_CONFIRMATION" + + @pytest.mark.asyncio async def test_vloop_still_blocks_actual_repetition_outside_transaction_confirmation(): rail = LoopRail()