Adding transaction state

This commit is contained in:
2026-08-21 17:56:22 -03:00
parent 838a0b916f
commit 1c069b560d
11 changed files with 518 additions and 36 deletions

54
.idea/workspace.xml generated
View File

@@ -47,6 +47,8 @@
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/guardrails/rails.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/models.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/registry.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/registry.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/registry.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/registry.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/tool_policy.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/tool_policy.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/tool_router.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/mcp/tool_router.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/enterprise_router.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/enterprise_router.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/enterprise_router.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/routing/enterprise_router.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/runtime.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/runtime.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/runtime.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/runtime.py" afterDir="false" />
@@ -60,6 +62,7 @@
<change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/routing.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/routing.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/routing.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/routing.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tool_policies.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tool_policies.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tool_policies.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tool_policies.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tools.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tools.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tools.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend/config/tools.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/app/state.py" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/app/state.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/routing.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/routing.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/routing.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/routing.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/tool_policies.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/tool_policies.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/tool_policies.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/agent_framework_oci/templates/agent_template_backend_day_zero/config/tool_policies.yaml" afterDir="false" />
@@ -78,6 +81,7 @@
<change beforePath="$PROJECT_DIR$/config/prompts/contestation.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/prompts/contestation.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/prompts/contestation.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/prompts/contestation.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/prompts/vas.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/prompts/vas.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/prompts/vas.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/prompts/vas.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/routing.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/routing.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/routing.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/routing.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/tool_policies.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/tool_policies.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/tools.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/tools.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/tools.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/tools.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/contas_mcp/servers/contas_mcp_server/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/contas_mcp/servers/contas_mcp_server/main.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/contas_mcp/servers/contas_mcp_server/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/contas_mcp/servers/contas_mcp_server/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tests/migration/test_original_contestation_validation.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/migration/test_original_contestation_validation.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/tests/migration/test_original_contestation_validation.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/migration/test_original_contestation_validation.py" afterDir="false" />
@@ -122,32 +126,31 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;, "ModuleVcsDetector.initialDetectionPerformed": "true",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;, "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;, "RunOnceActivity.typescript.service.memoryLimit.init": "true",
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, "Shell Script.agent_contas.executor": "Run",
&quot;Shell Script.agent_contas.executor&quot;: &quot;Run&quot;, "Shell Script.agent_frontend.executor": "Run",
&quot;Shell Script.agent_frontend.executor&quot;: &quot;Run&quot;, "Shell Script.agent_template_backend.executor": "Run",
&quot;Shell Script.agent_template_backend.executor&quot;: &quot;Run&quot;, "Shell Script.mcp gateway.executor": "Run",
&quot;Shell Script.mcp gateway.executor&quot;: &quot;Run&quot;, "Shell Script.mcp servers.executor": "Run",
&quot;Shell Script.mcp servers.executor&quot;: &quot;Run&quot;, "git-widget-placeholder": "main",
&quot;git-widget-placeholder&quot;: &quot;main&quot;, "kotlin-language-version-configured": "true",
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;, "last_opened_file_path": "D:/MSI_Projects/agent_framework_oci",
&quot;last_opened_file_path&quot;: &quot;D:/MSI_Projects/agent_framework_oci&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "nodejs_package_manager_path": "npm",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings",
&quot;settings.editor.selected.configurable&quot;: &quot;project.propVCSSupport.DirectoryMappings&quot;, "vue.rearranger.settings.migration": "true"
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}</component> }]]></component>
<component name="RunManager" selected="Shell Script.agent_contas"> <component name="RunManager" selected="Shell Script.agent_contas">
<configuration name="agent_contas" type="ShConfigurationType"> <configuration name="agent_contas" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="wsl bash -lc &quot;source .venv/bin/activate &amp;&amp; python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload&quot;" /> <option name="SCRIPT_TEXT" value="wsl bash -lc &quot;source .venv/bin/activate &amp;&amp; python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload&quot;" />
@@ -231,6 +234,7 @@
<workItem from="1786739151301" duration="953000" /> <workItem from="1786739151301" duration="953000" />
<workItem from="1787159165094" duration="10002000" /> <workItem from="1787159165094" duration="10002000" />
<workItem from="1787241513239" duration="1241000" /> <workItem from="1787241513239" duration="1241000" />
<workItem from="1787272670429" duration="822000" />
</task> </task>
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders"> <task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
<option name="closed" value="true" /> <option name="closed" value="true" />

View File

@@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False):
selected_tool_call: dict[str, Any] selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any] pending_tool_call: dict[str, Any]
active_transaction: dict[str, Any] active_transaction: dict[str, Any]
last_transaction: dict[str, Any]
transaction_status: str transaction_status: str
transaction_evidence: list[dict[str, Any]] transaction_evidence: list[dict[str, Any]]
last_transaction_evidence: dict[str, Any] last_transaction_evidence: dict[str, Any]

View File

@@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False):
selected_tool_call: dict[str, Any] selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any] pending_tool_call: dict[str, Any]
active_transaction: dict[str, Any] active_transaction: dict[str, Any]
last_transaction: dict[str, Any]
transaction_status: str transaction_status: str
transaction_pre_validation: dict[str, Any] transaction_pre_validation: dict[str, Any]
transaction_evidence: list[dict[str, Any]] transaction_evidence: list[dict[str, Any]]

View File

@@ -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/`

View File

@@ -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/`

View File

@@ -213,6 +213,33 @@ class EnterpriseRouter:
or (previous_intent and not previous_intent.startswith("state:") and candidate.intent != previous_intent) or (previous_intent and not previous_intent.startswith("state:") and candidate.intent != previous_intent)
) )
if different: if different:
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 = {
**(candidate.metadata or {}), **(candidate.metadata or {}),
"transaction_interruption": "intent_shift", "transaction_interruption": "intent_shift",
@@ -222,6 +249,17 @@ class EnterpriseRouter:
"interruption_source": "configured_routing", "interruption_source": "configured_routing",
} }
return candidate 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 return None
if not (self.enable_llm_router and self.llm is not None): if not (self.enable_llm_router and self.llm is not None):

View File

@@ -208,6 +208,21 @@ rag:
| `RUNTIME_TIMEOUT` | Timeout geral | resposta controlada | | `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 ## Requisitos Não Funcionais
| Categoria | Requisito | | Categoria | Requisito |
@@ -233,6 +248,8 @@ rag:
- [ ] Output guardrails executam antes da resposta final. - [ ] Output guardrails executam antes da resposta final.
- [ ] Judges geram JudgeResult. - [ ] Judges geram JudgeResult.
- [ ] Memória e checkpoint são persistidos conforme provider. - [ ] 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. - [ ] Erros geram NOC e resposta controlada.

View File

@@ -215,6 +215,21 @@ dataset:
groundedness: 0.70 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 ## Testes
| Teste | Escopo | | Teste | Escopo |
@@ -273,6 +288,7 @@ dataset:
## Critérios de Aceite ## Critérios de Aceite
- [ ] Novo agente é criado sem alterar core do framework. - [ ] 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`. - [ ] Configuração ocorre por YAML e `.env`.
- [ ] Agente usa BusinessContext. - [ ] Agente usa BusinessContext.
- [ ] Agente acessa MCP por router/gateway. - [ ] Agente acessa MCP por router/gateway.

View File

@@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False):
selected_tool_call: dict[str, Any] selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any] pending_tool_call: dict[str, Any]
active_transaction: dict[str, Any] active_transaction: dict[str, Any]
last_transaction: dict[str, Any]
transaction_status: str transaction_status: str
transaction_pre_validation: dict[str, Any] transaction_pre_validation: dict[str, Any]
transaction_evidence: list[dict[str, Any]] transaction_evidence: list[dict[str, Any]]

View File

@@ -55,7 +55,7 @@ def _active_tx(status="COLLECTING_PARAMETERS", arguments=None):
@pytest.mark.asyncio @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): async def test_matrix_collecting_parameter_answers_keep_transaction(message, tmp_path):
router = _router(tmp_path) router = _router(tmp_path)
state = { 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 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 @pytest.mark.asyncio
async def test_matrix_missing_next_state_recovers_active_transaction_before_stickiness(tmp_path): async def test_matrix_missing_next_state_recovers_active_transaction_before_stickiness(tmp_path):
router = _router(tmp_path) router = _router(tmp_path)

View File

@@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False):
selected_tool_call: dict[str, Any] selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any] pending_tool_call: dict[str, Any]
active_transaction: dict[str, Any] active_transaction: dict[str, Any]
last_transaction: dict[str, Any]
pending_tool_clarification: dict[str, Any] pending_tool_clarification: dict[str, Any]
pending_domain_workflow: dict[str, Any] pending_domain_workflow: dict[str, Any]
business_workflows_executed: list[str] business_workflows_executed: list[str]