# Decisões semânticas em pipelines de agentes, com o Jev

> O T25 pergunta ao Jev, da TypeSafe, e deixa uma política determinística decidir o que a probabilidade significa.

- Por: Djan Magno
- Publicado em: 2026-09-26
- Atualizado em: 2026-09-26
- URL: https://t25.io/blog/decisoes-semanticas-no-t25-com-jev/
- Engenharia · decisões semânticas, System One, Jev, shadow mode, agentes de código, política determinística

> **Resumo:** > - Decisões semânticas em pipelines de agentes, aqui, são perguntas tipadas ao Jev. A resposta é Choice, Score ou Noul: uma probabilidade, não um parágrafo.
> - O padrão é camada desligada, `provider: fake` e `mode: shadow`. A opinião vai para `semantic-decisions.jsonl` ao lado da heurística, com `policyAction: observe`.
> - Chave ausente, prazo de 1500 ms estourado ou JSON inválido: a heurística segue e o erro fica no registro. Fato ausente não vira "não".
> - O veredito de review continua em `evaluateReview()`. O merge na main continua humano.

Quando o QA fecha um critério, o T25 não pede um parágrafo sobre a prova. Pergunta `evidence.relation_to_criterion.v1`: a evidência apoia, contradiz, ou não diz nada. A resposta é um Choice com três probabilidades. Esse contrato é o que chamamos de decisões semânticas em pipelines de agentes.

## Decisões semânticas em pipelines de agentes: o que perguntamos

O catálogo mora em `src/decisions/questions.ts`. Cada id tem versão (`v1`) e um tipo. O Jev, da TypeSafe, é um modelo System One. Um POST em `https://api.typesafe.ai/v1/systemone` manda um `state` e várias perguntas; as respostas tipadas voltam na mesma passada. A [documentação pública](https://docs.typesafe.ai/introduction) descreve três primitivas. Noul é a probabilidade de um sim, entre 0 e 1. Choice escolhe uma opção da lista que nós enviamos e devolve a distribuição. Score coloca o estado numa rubrica e também devolve `confidence`.

Cinco famílias cobrem o julgamento que um `if` não fecha:

| Família | Pergunta | Tipo | O que está em jogo |
|---|---|---|---|
| Contexto | `context.candidate_helps.v1` | Noul | Este arquivo ajuda os critérios, ou só enche o prompt? |
| Progresso | `progress.looks_like_workaround.v1` | Noul | A mudança corrige o produto, ou enfraquece o teste? |
| Evidência | `evidence.relation_to_criterion.v1` | Choice | A prova apoia, contradiz ou não fala do critério? |
| Atenção | `attention.finding_lacks_context.v1` | Noul | O finding cita critério ou arquivo, ou alguém precisa abrir o resto? |
| Roteamento | `routing.adapter.v1` | Choice | Qual CLI da allowlist deve tentar este papel? |

O QA dispara `evidence.relation_to_criterion.v1` uma vez por critério (`supports`, `contradicts`, `says_nothing`). Numa troca de estágio, o progresso manda quatro Nouls: se o diff ainda ataca o critério aberto, se a mudança parece contorno de teste, se a impressão digital do erro se repete, e se o QA descreve o mesmo buraco. Contexto olha no máximo os oito primeiros caminhos de `relatedFiles`. Com mais de três skills, entra `context.skill_candidate_helps.v1`.

`quality.action.v1` e `quality.scope.v1` rodam depois. `decideQualityAction` já escolheu retrabalho, novo QA ou humano. A escolha do código entra em `heuristic` com probabilidade 1 na ação tomada e 0 nas outras, e esse Choice não vai no `state`.

## Por que probabilidade tipada audita melhor que texto livre

Texto livre carrega a prova e a conclusão no mesmo bloco. No review isso já nos custou: o modelo escreve `APPROVE` e, linhas abaixo, lista um finding dentro do escopo. Quem decide é `evaluateReview()`, relendo os findings. O caso está em [O APPROVE do modelo não é o merge](https://t25.io/blog/o-approve-do-modelo-nao-e-o-merge/).

Uma resposta System One não abre esse segundo canal. `parseProviderAnswer()`, em `src/decisions/answers.ts`, recusa o que sai do contrato:

- Noul fora de `[0, 1]` não entra.
- Choice fora do enum enviado não entra. Chave de probabilidade que não está no enum também não.
- As probabilidades do enum têm de somar 1, com folga de `0.02`.
- Score sem distribuição válida não entra. O `level` gravado é a opção de maior probabilidade, calculada aqui.

Duas linhas com o mesmo `questionId` e a mesma `questionVersion` ficam no mesmo eixo. Mudar o enunciado é outra versão, não um ajuste silencioso do histórico.

## Shadow, active e o fallback

A seção `semantic_decisions` da [configuração](https://t25.io/docs/configuration.html) nasce desligada. Provider `fake`, `mode: shadow`, roteamento `manual`, modelo pinado em `jev-1.13.0`. O fake responde dentro do processo. Teste não chama a TypeSafe.

`createSemanticDecisionEngine()` calcula a heurística em toda observação. Com `provider: jev` e os fatos obrigatórios presentes, a chamada cabe em `timeout_ms` (1500 ms). O relógio cobre a chamada inteira, inclusive até dois retries de HTTP 429 e 529. Sem chave, no timeout, num status inesperado ou num JSON que não parseia, a linha fica com `providerError`, `jev: null` e a heurística intacta. `observeSemantic()` captura a exceção e a tarefa continua.

Fato obrigatório ausente não é enviado ao Jev. `projectObservationState()` grava `{ absent: true }`, marca `factsStatus: insufficient` e não chama o Jev. Em `composeAttention()`, resposta nula ou fato insuficiente vira `unknown`, e `unknown` pede uma pessoa.

O YAML aceita `mode: active`. A linha gravada continua `mode: "shadow"` e `policyAction: "observe"`. `policyActionFor()` não devolve outra ação, e `assertNoControlFlow()` recusa o resto. Promover uma ação para fora de shadow exige `assertPromotionAllowed()`: comparação pareada, replay de holdout, adjudicação humana, valor incremental e um teste de que o caminho segue sem o Jev.

O opt-in que já pode mudar uma escolha de execução é `routing: automatic`, com a camada ligada e provider `jev`. `selectExperimentAdapter()` troca o CLI se a `confidence` de `routing.adapter.v1` é pelo menos `0.7` e a opção está na allowlist do papel. Resposta incompleta, confiança baixa, opção fora da lista ou um único CLI autorizado: fica o primeiro adapter configurado, com `fallbackReason` (`manual`, `incomplete_answers`, `low_confidence`, `not_authorized` ou `kept_default`). O Score `routing.role_difficulty.v1` só desce faixa no mesmo CLI (dois degraus em `trivial`, um em `easy`, zero em `moderate` ou `hard`), com o mesmo piso. `SKILL_SUGGESTION_PROTOCOL.ready` é `false`, então a pergunta sobre skills pode ser registrada e a lista do prompt continua inteira (`fallbackReason: protocol_not_ready`).

## O que cada linha guarda

Cada linha de `semantic-decisions.jsonl` é um `semantic-decision.v1`: tarefa, `questionId` versionado, provider, digest SHA-256 do estado, heurística, resposta ou `null`, `policyAction`, erro, `responseModel`, `factsStatus` e chaves ausentes. O log guarda os últimos 4000 registros. Linha inválida vai para `semantic-decisions.invalid.jsonl`.

Em `DONE`, `FAILED` ou `CANCELLED`, `stampLaterOutcome()` preenche o que ainda estava vazio (`accepted`, `failed`, `cancelled`). A resposta original fica. Dá para ver depois se um Noul alto coincidiu com aceite ou com falha.

O estado enviado é uma projeção. `send_code: true` quebra o YAML; o único valor aceito é `false`. A allowlist descarta `diff`, `patch`, `content`, `source`, `body` e `prompt`. Linha com cara de código vira `[CODE_OMITTED]`. Segredo passa pelo redator. Payload ainda bloqueado não é perguntado. A chave fica fora do YAML, em `T25_JEV_API_KEY` ou em `jev-secret.json` modo `0600`. `t25 jev show` e `GET /api/v1/jev` dizem se há chave, nunca o valor.

O modelo padrão é `jev-1.13.0`. A [página pública de modelos](https://docs.typesafe.ai/models) avisa que o alias `jev-latest` passa a apontar para outro id quando sai um release, e que as respostas podem mudar junto. Limiar calibrado numa versão fica pinado nela. `responseModel` guarda o id que respondeu.

## Onde o limiar mora, e por que paramos nele

A documentação pública do Noul deixa o limiar no programa e manda o valor do meio para uma pessoa. `noulBand()` faz isso: `0.7` ou mais é positivo, `0.3` ou menos é negativo, o meio é incerto. `composeAttention()` lê a relação evidência/critério, se o finding tem contexto, e se a impressão digital repete um incidente do projeto. Um incerto devolve `unknown`. Contradição, finding opaco ou incidente repetido devolve `needs_human`. `skip_human` só sai com o conjunto inteiro decisivo e sem esses sinais. Esse veredito não abre pull request e não faz merge.

O merge na main continua com quem responde pelo repositório. O veredito de review continua em `evaluateReview()`. A spec só vira plano com aprovação humana. A fábrica roda na máquina de quem a instalou e chama os CLIs que essa pessoa já assina. Enquanto uma prova do gate falta, a heurística é a decisão.

## Como usar o mesmo contrato em outro pipeline

1. Uma pergunta por julgamento, com o enum que o seu código já sabe tratar.
2. Mande só as chaves que a pergunta lista. O repositório fica onde está.
3. Falhe fechado no parse. Corpo inválido é erro, guardado à parte do registro válido.
4. Persista a heurística e a probabilidade. Não devolva a heurística dentro do `state`.
5. Escreva o limiar no código, com uma faixa do meio que chama uma pessoa. Estoure o prazo e siga na heurística.

## Perguntas frequentes

### O Jev aprova o pull request ou faz o merge?

A resposta entra como sinal. O review passa por `evaluateReview()`, que recalcula o veredito a partir dos findings. O merge na main continua uma ação humana, depois dos checks obrigatórios do repositório.

### O que acontece se o Jev expirar no meio da tarefa?

A heurística fica. A linha ganha `providerError` e `jev: null`. A tarefa segue. Na atenção, resposta ausente conta como `unknown` e pede uma pessoa.

### Por que não pedir um veredito em texto e fazer parse?

O texto junta a prova e a conclusão, e o parse vira outro lugar para a conclusão se esconder. Choice, Score e Noul devolvem um valor no enum que nós definimos, mais a distribuição. Dá para rejeitar a linha, comparar tarefas e mover o limiar sem reinterpretar um parágrafo.

### Dá para rodar o T25 sem chave do Jev?

Sim. O padrão é a camada desligada e `provider: fake`. `t25 jev show` mostra provider, modo e se existe chave. Apontar o provider para `jev` é opt-in, pela CLI, por `PUT /api/v1/jev` com admin, ou pelo YAML, e o modo de partida continua shadow.
