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.
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 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.
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
levelgravado é 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 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 sai da máquina. 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 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
- Uma pergunta por julgamento, com o enum que o seu código já sabe tratar.
- Mande só as chaves que a pergunta lista. O repositório fica onde está.
- Falhe fechado no parse. Corpo inválido é erro, guardado à parte do registro válido.
- Persista a heurística e a probabilidade. Não devolva a heurística dentro do
state. - 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.