diff --git a/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/state.py b/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/state.py index 7d929a8..1d82e35 100644 --- a/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/state.py +++ b/Tuning-Performance/Transaction_Evidence/agent_template_backend/app/state.py @@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False): selected_tool_call: dict[str, Any] pending_tool_call: dict[str, Any] active_transaction: dict[str, Any] + last_transaction: dict[str, Any] transaction_status: str transaction_evidence: list[dict[str, Any]] last_transaction_evidence: dict[str, Any] diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py index ff1d474..6955508 100644 --- a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py @@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False): selected_tool_call: dict[str, Any] pending_tool_call: dict[str, Any] active_transaction: dict[str, Any] + last_transaction: dict[str, Any] transaction_status: str transaction_pre_validation: dict[str, Any] transaction_evidence: list[dict[str, Any]] diff --git a/docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md b/docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md new file mode 100644 index 0000000..0202898 --- /dev/null +++ b/docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md @@ -0,0 +1,224 @@ +# Guia do Desenvolvedor — Estado e Transações Multi-turno + +Este documento define o contrato operacional para transações multi-turno no Agent Framework OCI. Ele é normativo para hosts e templates que utilizam `AgentRuntime`, checkpoint LangGraph e tools transacionais. + +## 1. Objetivo + +Uma transação pode atravessar vários turnos. Exemplo: + +```text +Usuário: quero cancelar o pedido +Framework: informe o número do pedido +Usuário: PED-1001 +Framework: confirma o cancelamento? +Usuário: sim +Framework: executa a tool +``` + +O framework precisa preservar a transação entre todos esses turnos sem depender de reclassificação por LLM, keyword routing ou reextração de parâmetros já obtidos. + +## 2. Fonte canônica do estado transacional + +O estado canônico da transação em andamento é `active_transaction`. + +```python +active_transaction: dict[str, Any] +last_transaction: dict[str, Any] +``` + +Todo `AgentState` usado por um host que habilita transações multi-turno **DEVE** declarar os dois campos. Como o LangGraph usa o schema do state para persistência/checkpoint, um campo criado apenas dinamicamente pelo runtime não é um contrato durável seguro. + +Exemplo mínimo: + +```python +from typing import Any, TypedDict + +class AgentState(TypedDict, total=False): + # ...campos normais... + selected_tool_call: dict[str, Any] + pending_tool_call: dict[str, Any] + active_transaction: dict[str, Any] + last_transaction: dict[str, Any] + transaction_status: str + missing_parameters: list[str] + confirmation_required: bool + confirmation_received: bool +``` + +## 3. Papel de cada campo + +| Campo | Papel | Regra | +|---|---|---| +| `active_transaction` | Fonte canônica da transação ativa | Deve sobreviver a checkpoint/resume enquanto a transação estiver ativa. | +| `last_transaction` | Snapshot da última transação terminal | Usado para auditoria, evidência e continuidade controlada; não reativa automaticamente a transação. | +| `transaction_status` | Estado lógico atual | Ex.: `COLLECTING_PARAMETERS`, `AWAITING_CONFIRMATION`, `COMPLETED`, `CANCELLED`, `OUT_OF_SCOPE`. | +| `missing_parameters` | Parâmetros ainda necessários | Deve refletir o estado canônico da transação, não apenas a mensagem corrente. | +| `selected_tool_call` | Estado auxiliar/compatibilidade | Não deve substituir `active_transaction` como fonte canônica. | +| `pending_tool_call` | Estado auxiliar/compatibilidade | Pode ser usado por compatibilidade, mas não como latch principal. | +| `next_state` | Orientação de roteamento do workflow | Ajuda a manter o nó/agente correto durante coleta/confirmação. | +| `transaction_pre_validation` | Evidência de pré-validação | Mantém resultado de validação antes da confirmação/execução. | +| `transaction_evidence` | Evidências da execução | Mantém resultados e trilha de execução da transação. | + +## 4. Ciclo de vida recomendado + +```text +IDLE + ↓ intenção transacional +COLLECTING_PARAMETERS + ↓ parâmetros completos +PRE_VALIDATION (quando configurado) + ↓ elegível +AWAITING_CONFIRMATION + ↓ confirmação positiva +EXECUTING + ↓ +COMPLETED +``` + +Saídas terminais alternativas: + +```text +CANCELLED +OUT_OF_SCOPE +FAILED +``` + +O runtime pode representar algumas fases internamente sem um `transaction_status` público separado. O requisito é preservar o latch e não perder argumentos já coletados. + +## 5. Merge incremental de parâmetros + +Uma resposta posterior deve complementar a transação existente, nunca recriá-la apenas a partir do texto atual. + +```python +existing = dict((state.get("active_transaction") or {}).get("arguments") or {}) +new_values = {"valor": "71.99"} +arguments = {**existing, **new_values} +``` + +Exemplo esperado: + +```text +Turno 1: subject = "TIM CTRL Redes Sociais 8.0" +Turno 2: valor = "71.99" +Resultado: subject + valor permanecem disponíveis +``` + +## 6. Precedência de roteamento durante transação + +Quando existe `active_transaction` em `COLLECTING_PARAMETERS`, a mensagem deve primeiro ser avaliada como possível resposta aos parâmetros pendentes. + +Precedência normativa: + +1. parâmetro pendente claramente preenchido → continuar a transação; +2. cancelamento/abandono explícito → cancelar a transação; +3. nova intenção inequívoca → interromper a transação e rotear; +4. keyword genérica do mesmo domínio/agente → **não** interromper a transação; +5. mensagem ambígua → manter a transação e clarificar. + +Exemplos: + +| Estado atual | Mensagem | Resultado correto | +|---|---|---| +| `retail_order_cancel`, falta `order_id` | `PED-1001` | Continua cancelamento e preenche `order_id`. | +| `retail_order_cancel`, falta `order_id` | `o pedido é o PED-1001` | Continua cancelamento; `pedido` não deve virar tracking. | +| contestação, falta `valor` | `R$ 71,99` | Continua contestação e preenche `valor`. | +| cancelamento pendente | `esquece, quero ver minha fatura` | Interrupção explícita permitida. | +| cancelamento pendente | `quero rastrear pedido` | Mudança inequívoca para tracking permitida. | + +## 7. Checkpoint e retomada + +Antes de executar roteamento normal, o host deve restaurar o checkpoint usando a mesma identidade de conversa (`tenant_id`, `agent_id`, `session_id`/`conversation_key` conforme contrato do host). + +Após a restauração: + +```text +active_transaction existe + ↓ +status ativo? + ↓ sim +retomar a transação antes de keyword routing / continuity LLM +``` + +Um estado `COLLECTING_PARAMETERS` sem `active_transaction` deve ser tratado como inconsistência de estado e observado/diagnosticado; não deve silenciosamente reiniciar a tool a partir da mensagem corrente. + +## 8. O que pertence ao framework e ao agente + +Framework: + +- persistência do latch; +- merge de argumentos; +- estados de coleta/confirmação; +- precedência de retomada; +- confirmação determinística; +- idempotência e evidência; +- checkpoint/resume. + +Agente: + +- definição das tools de domínio; +- parâmetros obrigatórios e mensagens de domínio; +- regras de elegibilidade específicas; +- pre-validation específica, quando houver; +- resposta final ao cliente. + +O agente não deve implementar um segundo motor transacional paralelo ao `AgentRuntime`. + +## 9. Checklist para novos hosts/templates + +- [ ] `AgentState` declara `active_transaction`. +- [ ] `AgentState` declara `last_transaction`. +- [ ] `transaction_status` e `missing_parameters` fazem parte do state quando usados. +- [ ] O host usa checkpoint compatível com o schema do state. +- [ ] A mesma `conversation_key` é usada entre turnos da mesma conversa. +- [ ] Parâmetros já coletados são mesclados com novos valores. +- [ ] Respostas a parâmetros têm precedência sobre keyword routing genérico. +- [ ] Mudança explícita de intenção continua possível. +- [ ] O agente usa `transaction_state_patch(state)` ao retornar respostas transacionais quando o template o exige. +- [ ] Existem testes multi-turno para coleta, confirmação, interrupção e resume. + +## 10. Testes regressivos mínimos + +```text +A. cancelamento de pedido +1. "quero cancelar pedido" +2. "o pedido é o PED-1001" +Esperado: continua retail_order_cancel; não vira retail_order_tracking. + +B. contestação +1. "não contratei TIM CTRL Redes Sociais 8.0" +2. "R$ 71,99" +Esperado: subject e valor chegam juntos à pre-validation. + +C. interrupção explícita +1. iniciar transação e deixar parâmetro pendente +2. "esquece, quero ver minha fatura" +Esperado: transação é interrompida e nova intenção é roteada. + +D. checkpoint/resume +1. iniciar transação +2. persistir/checkpoint +3. reconstruir execução usando a mesma conversation_key +4. fornecer o parâmetro faltante +Esperado: active_transaction é restaurado e concluído sem reiniciar a tool. +``` + +## 11. Anti-patterns + +- reconstruir a transação somente a partir da última mensagem; +- usar `selected_tool_call` como única fonte do latch; +- remover `active_transaction` do `AgentState` por parecer redundante; +- permitir uma keyword genérica como `pedido` interromper coleta de `order_id`; +- armazenar parâmetros apenas em variáveis locais do nó; +- duplicar confirmação transacional no prompt do agente; +- limpar o latch antes do estado terminal. + +## 12. Referências no projeto + +- `specs/SPEC-002-Agent-Runtime.md` +- `specs/SPEC-010-Agent-Development.md` +- `templates/agent_template_backend/app/state.py` +- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py` +- `libs/agent_framework/src/agent_framework/routing/enterprise_router.py` +- `Tuning-Performance/Deterministic_Transactional_Workflow/` +- `Tuning-Performance/Transaction_Pre_Validation/` +- `Tuning-Performance/Transaction_Evidence/` diff --git a/docs/TRANSACTION_STATE_DEVELOPER_GUIDE_en.md b/docs/TRANSACTION_STATE_DEVELOPER_GUIDE_en.md new file mode 100644 index 0000000..c7b1552 --- /dev/null +++ b/docs/TRANSACTION_STATE_DEVELOPER_GUIDE_en.md @@ -0,0 +1,160 @@ +# Developer Guide — Multi-turn Transaction State + +This document defines the operational contract for multi-turn transactions in Agent Framework OCI. It is normative for hosts and templates that use `AgentRuntime`, LangGraph checkpoints, and transactional tools. + +## 1. Goal + +A transaction may span multiple turns: + +```text +User: cancel my order +Framework: provide the order number +User: PED-1001 +Framework: confirm cancellation? +User: yes +Framework: execute the tool +``` + +The framework must preserve the transaction across all turns without relying on LLM reclassification, keyword routing, or re-extraction of parameters already collected. + +## 2. Canonical transaction state + +The canonical in-flight transaction is `active_transaction`. + +```python +active_transaction: dict[str, Any] +last_transaction: dict[str, Any] +``` + +Every `AgentState` used by a host that enables multi-turn transactions **MUST** declare both fields. LangGraph uses the state schema for checkpoint persistence, so a field created dynamically by the runtime alone is not a safe durable contract. + +Minimal example: + +```python +from typing import Any, TypedDict + +class AgentState(TypedDict, total=False): + # ...normal fields... + selected_tool_call: dict[str, Any] + pending_tool_call: dict[str, Any] + active_transaction: dict[str, Any] + last_transaction: dict[str, Any] + transaction_status: str + missing_parameters: list[str] + confirmation_required: bool + confirmation_received: bool +``` + +## 3. Field responsibilities + +| Field | Responsibility | Rule | +|---|---|---| +| `active_transaction` | Canonical in-flight transaction | Must survive checkpoint/resume while active. | +| `last_transaction` | Snapshot of the latest terminal transaction | Used for audit/evidence; does not automatically reactivate a transaction. | +| `transaction_status` | Current logical status | E.g. `COLLECTING_PARAMETERS`, `AWAITING_CONFIRMATION`, `COMPLETED`, `CANCELLED`, `OUT_OF_SCOPE`. | +| `missing_parameters` | Parameters still required | Must reflect canonical transaction state, not only the current message. | +| `selected_tool_call` | Auxiliary/backward-compatible state | Must not replace `active_transaction` as canonical state. | +| `pending_tool_call` | Auxiliary/backward-compatible state | May support compatibility but is not the primary latch. | +| `next_state` | Workflow routing guidance | Keeps the correct node/agent during collection/confirmation. | +| `transaction_pre_validation` | Pre-validation evidence | Stores validation before confirmation/execution. | +| `transaction_evidence` | Execution evidence | Stores results and the transaction execution trail. | + +## 4. Recommended lifecycle + +```text +IDLE + ↓ transactional intent +COLLECTING_PARAMETERS + ↓ complete parameters +PRE_VALIDATION (when configured) + ↓ eligible +AWAITING_CONFIRMATION + ↓ positive confirmation +EXECUTING + ↓ +COMPLETED +``` + +Alternative terminal outcomes include `CANCELLED`, `OUT_OF_SCOPE`, and `FAILED`. + +## 5. Incremental parameter merge + +A later answer must complement the existing transaction instead of rebuilding it from the latest text only. + +```python +existing = dict((state.get("active_transaction") or {}).get("arguments") or {}) +new_values = {"amount": "71.99"} +arguments = {**existing, **new_values} +``` + +Previously collected arguments must remain available on subsequent turns. + +## 6. Routing precedence during an active transaction + +When `active_transaction` is in `COLLECTING_PARAMETERS`, the message must first be evaluated as a possible answer to pending parameters. + +Normative precedence: + +1. clearly fills a pending parameter → continue transaction; +2. explicit cancel/abandon → cancel transaction; +3. unambiguous new intent → interrupt and route; +4. generic keyword in the same domain/agent → **do not** interrupt; +5. ambiguous message → keep transaction and clarify. + +| Current state | Message | Correct result | +|---|---|---| +| `retail_order_cancel`, missing `order_id` | `PED-1001` | Continue cancellation and fill `order_id`. | +| `retail_order_cancel`, missing `order_id` | `the order is PED-1001` | Continue cancellation; `order` must not switch to tracking. | +| contestation, missing `amount` | `R$ 71.99` | Continue contestation and fill amount. | +| pending cancellation | `forget it, show my bill` | Explicit interruption is allowed. | +| pending cancellation | `track my order` | Unambiguous shift to tracking is allowed. | + +## 7. Checkpoint and resume + +Before normal routing, restore the checkpoint with the same conversation identity (`tenant_id`, `agent_id`, `session_id`/`conversation_key` according to the host contract). + +An active transaction must be resumed before generic keyword routing or LLM continuity. `COLLECTING_PARAMETERS` without `active_transaction` should be treated as inconsistent state and diagnosed rather than silently restarting the tool. + +## 8. Framework vs. agent responsibility + +Framework owns latch persistence, argument merge, collection/confirmation states, resume precedence, deterministic confirmation, idempotency/evidence, and checkpoint/resume. + +The agent owns domain tools, required parameters, domain messages, domain eligibility/pre-validation, and customer-facing final responses. It must not create a parallel transaction engine. + +## 9. New host/template checklist + +- [ ] `AgentState` declares `active_transaction`. +- [ ] `AgentState` declares `last_transaction`. +- [ ] `transaction_status` and `missing_parameters` are declared when used. +- [ ] Checkpoint provider is compatible with the state schema. +- [ ] The same conversation identity is reused across turns. +- [ ] New parameters are merged with previously collected arguments. +- [ ] Pending parameter answers take precedence over generic keyword routing. +- [ ] Explicit intent shifts remain possible. +- [ ] Transactional agent responses propagate `transaction_state_patch(state)` where required by the template. +- [ ] Multi-turn tests cover collection, confirmation, interruption, and resume. + +## 10. Minimum regression tests + +Test order cancellation with a pending `order_id`, contestation with a subject collected on the first turn and amount on the second, explicit interruption to a different intent, and checkpoint/resume using the same conversation identity. + +## 11. Anti-patterns + +- rebuilding the transaction from only the latest message; +- using `selected_tool_call` as the only latch source; +- removing `active_transaction` because it appears redundant; +- allowing a generic keyword such as `order` to interrupt `order_id` collection; +- keeping parameters only in node-local variables; +- duplicating transaction confirmation in the agent prompt; +- clearing the latch before a terminal state. + +## 12. Project references + +- `specs/SPEC-002-Agent-Runtime.md` +- `specs/SPEC-010-Agent-Development.md` +- `templates/agent_template_backend/app/state.py` +- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py` +- `libs/agent_framework/src/agent_framework/routing/enterprise_router.py` +- `Tuning-Performance/Deterministic_Transactional_Workflow/` +- `Tuning-Performance/Transaction_Pre_Validation/` +- `Tuning-Performance/Transaction_Evidence/` diff --git a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py index a731756..469b76a 100644 --- a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py +++ b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py @@ -213,16 +213,54 @@ class EnterpriseRouter: or (previous_intent and not previous_intent.startswith("state:") and candidate.intent != previous_intent) ) if different: - candidate.metadata = { - **(candidate.metadata or {}), - "transaction_interruption": "intent_shift", - "interrupted_state": state_decision.next_state, - "interrupted_agent": state_decision.agent, - "interrupted_intent": started_intent or previous_intent, - "interruption_source": "configured_routing", - } - return candidate - return None + tx_status = str(state.get("transaction_status") or "").strip().upper() + missing = list(state.get("missing_parameters") or []) + same_agent = candidate.agent == state_decision.agent + matched_keyword = str((candidate.metadata or {}).get("matched_keyword") or "").strip() + informative_tokens = [ + token + for token in self._keyword_tokens(matched_keyword) + if len(token) > 1 + ] + + # Durante coleta de parâmetros, uma keyword genérica de uma única + # palavra do MESMO agente não pode preemptar a transação. Ex.: + # ``o pedido é o PED-1001`` enquanto ``order_id`` está pendente. + # Nesse caso ``pedido`` pode casar com ``retail_order_tracking``, + # mas a mensagem é perfeitamente compatível com a resposta ao + # parâmetro solicitado. Keywords mais específicas (duas ou mais + # palavras informativas) continuam aptas a representar mudança + # explícita de intenção. Se o roteador LLM estiver habilitado, + # deixamos a decisão semântica abaixo desempatar o caso fraco. + weak_same_agent_keyword_during_collection = ( + tx_status == "COLLECTING_PARAMETERS" + and bool(missing) + and same_agent + and len(informative_tokens) <= 1 + ) + + if not weak_same_agent_keyword_during_collection: + candidate.metadata = { + **(candidate.metadata or {}), + "transaction_interruption": "intent_shift", + "interrupted_state": state_decision.next_state, + "interrupted_agent": state_decision.agent, + "interrupted_intent": started_intent or previous_intent, + "interruption_source": "configured_routing", + } + return candidate + + logger.debug( + "Keyword transacional fraca não preemptou coleta de parâmetro: " + "keyword=%r intent=%s missing=%s", + matched_keyword, + candidate.intent, + missing, + ) + # Não retorne aqui: se houver LLM router, ele pode confirmar uma + # mudança semântica real; sem LLM, a transação permanece ativa. + else: + return None if not (self.enable_llm_router and self.llm is not None): return None diff --git a/specs/SPEC-002-Agent-Runtime.md b/specs/SPEC-002-Agent-Runtime.md index ae3b532..9670c10 100644 --- a/specs/SPEC-002-Agent-Runtime.md +++ b/specs/SPEC-002-Agent-Runtime.md @@ -208,6 +208,21 @@ rag: | `RUNTIME_TIMEOUT` | Timeout geral | resposta controlada | + +## Contrato Durável de Estado Transacional + +Hosts que utilizam `AgentRuntime` com transações multi-turno DEVEM declarar no `AgentState` os campos `active_transaction` e `last_transaction`. O primeiro é a fonte canônica da transação em andamento e deve sobreviver a checkpoint/resume; o segundo mantém o snapshot da última transação terminal. + +```python +active_transaction: dict[str, Any] +last_transaction: dict[str, Any] +``` + +`selected_tool_call` e `pending_tool_call` são campos auxiliares/compatibilidade e não substituem o latch canônico. Durante `COLLECTING_PARAMETERS`, a retomada da transação e o consumo de parâmetros pendentes têm precedência sobre keyword routing genérico. Uma mudança de intenção só deve interromper a transação quando for inequívoca ou explicitamente solicitada pelo usuário. + +O contrato completo, ciclo de vida, precedência de roteamento, checklist e testes regressivos estão em [`docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md`](../docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md). + + ## Requisitos Não Funcionais | Categoria | Requisito | @@ -233,6 +248,8 @@ rag: - [ ] Output guardrails executam antes da resposta final. - [ ] Judges geram JudgeResult. - [ ] Memória e checkpoint são persistidos conforme provider. +- [ ] Hosts transacionais declaram `active_transaction` e `last_transaction` no `AgentState`. +- [ ] Durante `COLLECTING_PARAMETERS`, respostas a parâmetros pendentes têm precedência sobre keyword routing genérico. - [ ] Erros geram NOC e resposta controlada. diff --git a/specs/SPEC-010-Agent-Development.md b/specs/SPEC-010-Agent-Development.md index 93e4d59..5b46f7a 100644 --- a/specs/SPEC-010-Agent-Development.md +++ b/specs/SPEC-010-Agent-Development.md @@ -215,6 +215,21 @@ dataset: groundedness: 0.70 ``` + +## Contrato obrigatório para agentes transacionais + +Ao criar um agente que usa tools transacionais do framework, o desenvolvedor não deve criar um motor paralelo de coleta/confirmação. Deve reutilizar `AgentRuntime` e garantir que o `AgentState` do host mantenha o latch durável: + +```python +active_transaction: dict[str, Any] +last_transaction: dict[str, Any] +``` + +Durante uma transação ativa, parâmetros já coletados são preservados e novos valores são mesclados incrementalmente. Em `COLLECTING_PARAMETERS`, uma resposta que satisfaz um parâmetro pendente tem precedência sobre keywords genéricas. Mudanças de intenção explícitas e inequívocas continuam permitidas. + +Antes de publicar um novo template/host, execute os cenários multi-turno descritos no [`Transaction State Developer Guide`](../docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md). + + ## Testes | Teste | Escopo | @@ -273,6 +288,7 @@ dataset: ## Critérios de Aceite - [ ] Novo agente é criado sem alterar core do framework. +- [ ] Se houver transações multi-turno, `AgentState` declara `active_transaction` e `last_transaction`. - [ ] Configuração ocorre por YAML e `.env`. - [ ] Agente usa BusinessContext. - [ ] Agente acessa MCP por router/gateway. diff --git a/templates/agent_template_backend/app/state.py b/templates/agent_template_backend/app/state.py index ff1d474..6955508 100644 --- a/templates/agent_template_backend/app/state.py +++ b/templates/agent_template_backend/app/state.py @@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False): selected_tool_call: dict[str, Any] pending_tool_call: dict[str, Any] active_transaction: dict[str, Any] + last_transaction: dict[str, Any] transaction_status: str transaction_pre_validation: dict[str, Any] transaction_evidence: list[dict[str, Any]] diff --git a/tests/test_transaction_state_regression_matrix.py b/tests/test_transaction_state_regression_matrix.py index cf63fe0..6861030 100644 --- a/tests/test_transaction_state_regression_matrix.py +++ b/tests/test_transaction_state_regression_matrix.py @@ -55,7 +55,7 @@ def _active_tx(status="COLLECTING_PARAMETERS", arguments=None): @pytest.mark.asyncio -@pytest.mark.parametrize("message", ["PED-1001", "12345", "R$ 71,99", "10/09/2026"]) +@pytest.mark.parametrize("message", ["PED-1001", "o pedido é o PED-1001", "12345", "R$ 71,99", "10/09/2026"]) async def test_matrix_collecting_parameter_answers_keep_transaction(message, tmp_path): router = _router(tmp_path) state = { @@ -75,6 +75,25 @@ async def test_matrix_collecting_parameter_answers_keep_transaction(message, tmp assert (decision.metadata or {}).get("transaction_interruption") is None +@pytest.mark.asyncio +async def test_matrix_specific_same_agent_intent_shift_still_preempts_collection(tmp_path): + router = _router(tmp_path) + state = { + "user_text": "quero rastrear pedido", + "sanitized_input": "quero rastrear pedido", + "next_state": "COLLECTING_ORDER_PARAMETERS", + "transaction_status": "COLLECTING_PARAMETERS", + "missing_parameters": ["order_id"], + "active_transaction": _active_tx(arguments={}), + "active_agent": "orders_agent", + "intent": "state:COLLECTING_ORDER_PARAMETERS", + "route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"}, + } + decision = await router.route(state) + assert decision.intent == "retail_order_tracking" + assert decision.metadata["transaction_interruption"] == "intent_shift" + + @pytest.mark.asyncio async def test_matrix_missing_next_state_recovers_active_transaction_before_stickiness(tmp_path): router = _router(tmp_path)