new feature: frontend with separated responses

This commit is contained in:
2026-09-11 11:59:46 -03:00
parent e6bc243cf7
commit a2fe158d62
18 changed files with 567 additions and 33 deletions

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -667,6 +695,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -667,6 +695,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -665,6 +693,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -667,6 +695,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -665,6 +693,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -665,6 +693,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -665,6 +693,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -665,6 +693,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -667,6 +695,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -667,6 +695,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -338,9 +338,43 @@ class LoopRail(Guardrail):
code = "VLOOP" code = "VLOOP"
stage = "input" 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: 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() 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 repeated = history.count(normalized) >= 2 if normalized else False
return RailDecision( return RailDecision(
code=self.code, code=self.code,

View File

@@ -358,30 +358,38 @@ class LoopRail(Guardrail):
@staticmethod @staticmethod
def _transaction_status(ctx: dict[str, Any]) -> str: def _transaction_status(ctx: dict[str, Any]) -> str:
status = str(ctx.get("transaction_status") or "").strip().upper() nonterminal = {
if status: "COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING",
return status "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") active_transaction = ctx.get("active_transaction")
if isinstance(active_transaction, dict): if isinstance(active_transaction, dict):
return str(active_transaction.get("status") or "").strip().upper() active_status = str(active_transaction.get("status") or "").strip().upper()
return "" 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: async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
ctx = _ctx(context) ctx = _ctx(context)
transaction_status = self._transaction_status(ctx) transaction_status = self._transaction_status(ctx)
# A short confirmation (for example ``sim``/``não``) may legitimately # Parameter values and short confirmations may legitimately repeat in
# appear several times in the same session because each transaction has # the same session. VLOOP must not consume input owned by the active
# its own confirmation boundary. VLOOP protects against conversational # transaction contract; extraction/confirmation remains responsible
# repetition; it must not consume the input that belongs to an active # for deciding whether the value is valid.
# transaction awaiting confirmation. The transaction runtime/classifier if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
# remains responsible for deciding whether the utterance is actually a
# valid confirm/reject response.
if transaction_status == "AWAITING_CONFIRMATION":
return RailDecision( return RailDecision(
code=self.code, code=self.code,
allowed=True, allowed=True,
reason="continuidade_transacional:AWAITING_CONFIRMATION", reason=f"continuidade_transacional:{transaction_status}",
sanitized_text=text, sanitized_text=text,
metadata={ metadata={
"history_window": len(list(ctx.get("history_texts") or [])[-6:]), "history_window": len(list(ctx.get("history_texts") or [])[-6:]),

View File

@@ -69,17 +69,30 @@ class MultiIntentPlanner:
def _intent_for_clause(self, clause: str) -> IntentDefinition | None: def _intent_for_clause(self, clause: str) -> IntentDefinition | None:
normalized = self._normalize(clause) 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]] = [] matches: list[tuple[int, int, IntentDefinition]] = []
for intent in self.intents: for intent in self.intents:
for keyword in intent.keywords: for keyword in intent.keywords:
token = self._normalize(keyword) token = self._normalize(keyword)
keyword_tokens = re.findall(r"[\w]+", token, flags=re.UNICODE) 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( matched = bool(
keyword_tokens keyword_tokens
and ( and (
(len(keyword_tokens) == 1 and keyword_tokens[0] in clause_tokens) (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: if matched:
@@ -146,7 +159,7 @@ class MultiIntentPlanner:
if len(operations) < 2: if len(operations) < 2:
return None return None
transactional = [item for item in operations if self._is_transactional(item)] transactional = [item for item in operations if self._is_transactional(item)]
if len(transactional) == 1: if transactional:
primary_operation = transactional[0] primary_operation = transactional[0]
operations.sort(key=lambda item: item is not primary_operation) operations.sort(key=lambda item: item is not primary_operation)
for operation in operations[1:]: for operation in operations[1:]:

View File

@@ -16,6 +16,29 @@ class PendingTopicDrain:
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
mcp_results: list[dict[str, Any]] mcp_results: list[dict[str, Any]]
rag_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( async def drain_pending_topics(
@@ -34,6 +57,8 @@ async def drain_pending_topics(
combined_mcp_results = list(state.get("mcp_results") or []) combined_mcp_results = list(state.get("mcp_results") or [])
combined_rag_results = list(state.get("rag_results") or []) combined_rag_results = list(state.get("rag_results") or [])
secondary_answers: list[str] = [] 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 []) agent_responses = list(state.get("agent_responses") or [])
if not agent_responses and str(candidate or "").strip(): if not agent_responses and str(candidate or "").strip():
primary_agent = str( primary_agent = str(
@@ -53,9 +78,15 @@ async def drain_pending_topics(
}) })
for topic in topics: for topic in topics:
disposition = topic.get("disposition") disposition = topic.get("disposition")
promoted = False
if disposition == "defer": if disposition == "defer":
remaining.append(topic) if not may_promote_deferred:
continue remaining.append(topic)
continue
topic = {**topic, "disposition": "execute", "status": "pending"}
disposition = "execute"
promoted = True
may_promote_deferred = False
if disposition != "execute": if disposition != "execute":
handled.append({**topic, "status": "completed"}) handled.append({**topic, "status": "completed"})
continue continue
@@ -123,14 +154,42 @@ async def drain_pending_topics(
if rag_evidence not in combined_rag_results: if rag_evidence not in combined_rag_results:
combined_rag_results.append(rag_evidence) combined_rag_results.append(rag_evidence)
secondary_answers.append(answer) secondary_answers.append(answer)
if promoted:
agent_responses = [{**item, "primary": False} for item in agent_responses]
agent_responses.append({ agent_responses.append({
"operation_id": topic.get("operation_id"), "operation_id": topic.get("operation_id"),
"agent": topic.get("agent"), "agent": topic.get("agent"),
"intent": topic.get("intent"), "intent": topic.get("intent"),
"answer": answer, "answer": answer,
"primary": False, "primary": promoted,
"status": "completed", "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"}) handled.append({**topic, "status": "completed"})
secondary_answers.extend(MultiIntentPlanner.public_messages(plan)) secondary_answers.extend(MultiIntentPlanner.public_messages(plan))
@@ -148,4 +207,5 @@ async def drain_pending_topics(
handled_topics=handled, handled_topics=handled,
mcp_results=combined_mcp_results, mcp_results=combined_mcp_results,
rag_results=combined_rag_results, rag_results=combined_rag_results,
state_patch=state_patch,
) )

View File

@@ -249,6 +249,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -267,6 +289,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -655,6 +683,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -247,6 +247,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] 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( await self.observer.emit_grl(
"001", "001",
{ {
@@ -265,6 +287,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "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: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (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, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -653,6 +681,7 @@ class AgentWorkflow:
"agent_responses": drained.agent_responses, "agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -40,6 +40,28 @@ def test_transactional_intent_is_primary_even_when_mentioned_second():
assert [item.intent for item in plan.operations] == ["cancel", "invoice"] 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(): def test_sentence_boundary_does_not_leak_previous_intent_into_knowledge_query():
planner = MultiIntentPlanner([ planner = MultiIntentPlanner([
IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["fatura"]), IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["fatura"]),

View File

@@ -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]["metadata"]["provider"] == "kbdb"
assert result.rag_results[0]["context"] == "Documento recuperado da base de conhecimento." 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"