feat(ia): Execuções diz se o Jev decidiu ou só observou

A linha do Jev dizia "Medido pelo Jev. Se ele está em observação, quem decide
é a IA de sempre." — não dizia o que aconteceu naquela mensagem. O worker
passa a gravar origem_da_escolha "jev" quando a nota dele decidiu e
"jev_observacao" quando decidiu a IA de sempre, e Execuções diz o fato: "O Jev
decidiu." / "O Jev observou; quem decidiu foi a IA de sempre."

A linha da medição sai depois da decisão: antes dela o worker não sabia — em
observação com a IA de sempre caída, é a nota do Jev que decide. Sem CHECK na
coluna (texto livre desde a 0128): nenhuma migration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015PWGNXXzCQURHMALcEMmkC
This commit is contained in:
melgarafael
2026-09-23 23:41:12 -03:00
co-authored by Claude Opus 5.5
parent e4c193bdf6
commit fcf0456853
8 changed files with 43 additions and 15 deletions
+13 -1
View File
@@ -134,10 +134,22 @@ describe("GET /api/v1/ai/runs", () => {
expect(falha.consequencia).toBe(PONTO_POR_ID.get("sentiment_classify")?.sintomaDeFalha);
expect(falha.oQueFazer).toBe(O_QUE_FAZER_DO_JEV.jev_credencial_invalida);
// "Medido pelo Jev" seria falso na única linha em que ninguém mediu.
// "O Jev decidiu" seria falso na única linha em que ninguém mediu.
expect(falha.porQueEsteModelo).toBe(JEV_FALHOU_SEM_RESERVA);
});
it("a linha do Jev diz o fato: decidiu, ou só observou", async () => {
linhas = [
linha({ provider: "typesafe", model: "typesafe/jev-1.13.0", origem_da_escolha: "jev" }),
linha({ provider: "typesafe", model: "typesafe/jev-1.13.0", origem_da_escolha: "jev_observacao" }),
];
const { corpo } = await pedir("?provider=typesafe");
expect(corpo.data.execucoes.map((e: { porQueEsteModelo: string }) => e.porQueEsteModelo)).toEqual([
"O Jev decidiu.",
"O Jev observou; quem decidiu foi a IA de sempre.",
]);
});
it("observação com a IA de sempre caída: a falha dela não afirma consequência que não houve", async () => {
linhas = [linha({ status: "erro", error_code: "provedor_indisponivel", origem_da_escolha: "jev_cobriu" })];
const { corpo } = await pedir();
+1 -1
View File
@@ -144,7 +144,7 @@ export async function GET(req: NextRequest): Promise<Response> {
? (ponto?.sintomaDeFalha ?? null)
: null,
oQueFazer: l.status === "erro" ? (O_QUE_FAZER[l.error_code ?? ""] ?? null) : null,
// A linha de falha do Jev só existe quando ninguém mediu: "Medido pelo Jev"
// A linha de falha do Jev só existe quando ninguém mediu: "O Jev decidiu"
// seria falso justamente nela.
porQueEsteModelo:
l.origem_da_escolha === "jev" && l.status === "erro"
+1
View File
@@ -22,6 +22,7 @@ const TEXTOS = [
...Object.values(O_QUE_FAZER_DO_JEV),
...Object.values(AVISO_DO_JEV),
EXPLICACAO_DA_ORIGEM.jev,
EXPLICACAO_DA_ORIGEM.jev_observacao,
EXPLICACAO_DA_ORIGEM.reserva_do_jev,
EXPLICACAO_DA_ORIGEM.jev_cobriu,
JEV_FALHOU_SEM_RESERVA,
+1 -1
View File
@@ -34,7 +34,7 @@ export const O_QUE_FAZER_DO_JEV: Readonly<Record<`jev_${MotivoComRede}`, string>
/**
* O "por que este modelo" da linha de FALHA do Jev em Execuções — a única que
* ele grava, quando ninguém mediu. A frase da origem `jev` ("Medido pelo Jev")
* ele grava, quando ninguém mediu. A frase da origem `jev` ("O Jev decidiu.")
* seria falsa justamente nela.
*/
export const JEV_FALHOU_SEM_RESERVA =
+7 -4
View File
@@ -45,8 +45,10 @@ export type OrigemDaEscolha =
| "variavel_de_ambiente"
| "herdado_de_quem_chamou"
| "padrao_da_organizacao"
/** O Jev mediu (decidindo, ou observando ao lado da IA de sempre). */
/** O Jev mediu e a nota dele decidiu. Também a linha de falha do Jev (ver Execuções). */
| "jev"
/** Observação: o Jev mediu ao lado da IA de sempre, e quem decidiu foi ela. */
| "jev_observacao"
/** O Jev estava ligado e não respondeu: a IA de sempre mediu no lugar dele. */
| "reserva_do_jev"
/** Observação: a IA de sempre falhou, e a nota do Jev, já medida, decidiu. */
@@ -60,9 +62,10 @@ export const EXPLICACAO_DA_ORIGEM: Record<OrigemDaEscolha, string> = {
"Herdado de quem disparou a chamada — o agente publicado, ou o roteador de intenção.",
padrao_da_organizacao: "Usando o padrão da organização.",
fixo_do_produto: "O produto resolve este ponto sozinho — não há modelo a escolher.",
// Uma frase para os dois modos: em observação o Jev mede e NÃO decide, e
// "o Jev decidiu" seria falso justamente no modo de comparar os dois.
jev: "Medido pelo Jev. Se ele está em observação, quem decide é a IA de sempre.",
// Duas origens, uma por desfecho: a frase única ("se ele está em observação,
// quem decide é…") não dizia o que aconteceu NAQUELA mensagem.
jev: "O Jev decidiu.",
jev_observacao: "O Jev observou; quem decidiu foi a IA de sempre.",
reserva_do_jev: "O Jev não respondeu; a IA de sempre mediu no lugar dele.",
jev_cobriu: "A IA de sempre falhou, mas o Jev já tinha medido esta mensagem: nada se perdeu.",
};
+2 -3
View File
@@ -3283,9 +3283,8 @@ export const DICIONARIO: Traducoes = {
es: "El producto resuelve este punto por sí solo; no hay modelo que elegir.",
},
// O Jev em IA › Execuções e na Central (`lib/ai/decisao/textos.ts`).
"Medido pelo Jev. Se ele está em observação, quem decide é a IA de sempre.": {
es: "Medido por Jev. Si está en observación, quien decide es la IA de siempre.",
},
"O Jev decidiu.": { es: "Jev decidió." },
"O Jev observou; quem decidiu foi a IA de sempre.": { es: "Jev observó; quien decidió fue la IA de siempre." },
"O Jev não respondeu; a IA de sempre mediu no lugar dele.": {
es: "Jev no respondió; la IA de siempre midió en su lugar.",
},
@@ -462,7 +462,8 @@ describe("o Jev no worker de clima", () => {
sentiment_jev_model: "jev-1.13.0",
});
expect(linhasDoJev(banco)).toHaveLength(1);
expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: "jev" });
// Execuções diz o que aconteceu NESTA mensagem: o Jev observou, não decidiu.
expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: "jev_observacao" });
expect(linhasDaIaDeSempre(banco)).toHaveLength(1);
expect(linhasDaIaDeSempre(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: null });
expect(alertas(rpcs)[0]!["p_payload"]).toMatchObject({ sentiment_engine: "llm" });
@@ -539,7 +540,8 @@ describe("o Jev no worker de clima", () => {
expect(resultado).toEqual({ skipped: false, sentiment_score: 0 });
expect(generateObject).toHaveBeenCalledTimes(1);
expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok" });
// Em observação, mas foi a nota dele que decidiu: a linha diz "jev", não "jev_observacao".
expect(linhasDoJev(banco)[0]).toMatchObject({ status: "ok", origem_da_escolha: "jev" });
expect(linhasDaIaDeSempre(banco)[0], "a falha da IA de sempre segue visível em Execuções").toMatchObject({
status: "erro",
// Sem consequência na tela: o Jev já tinha medido.
+14 -3
View File
@@ -265,11 +265,21 @@ export async function processSentiment(event: EventRow): Promise<SentimentResult
: null;
if (clima?.ok) {
await fecharAvisoDoJev(admin, event.organization_id);
}
/**
* A linha da medição do Jev sai DEPOIS da decisão, com a origem do que
* aconteceu: "jev" quando a nota dele decidiu, "jev_observacao" quando a IA
* de sempre decidiu. Antes da decisão a linha não sabia — em observação com a
* IA de sempre caída, é a nota do Jev que decide.
*/
const registrarMedicaoDoJev = (decidiu: boolean): void => {
if (!clima?.ok) return;
logInvocation({
...comum,
provider: "typesafe",
model: `typesafe/${clima.modelo}`,
origem_da_escolha: "jev",
origem_da_escolha: decidiu ? "jev" : "jev_observacao",
prompt_tokens: clima.tokensDeEntrada,
completion_tokens: clima.tokensDeSaida,
latency_ms: clima.latenciaMs,
@@ -284,8 +294,7 @@ export async function processSentiment(event: EventRow): Promise<SentimentResult
}),
finish_reason: null,
});
await fecharAvisoDoJev(admin, event.organization_id);
}
};
const jevFalhouNaRede = clima !== null && !clima.ok && clima.tentouRede ? clima : null;
/** `reservaMediu` é o que o corpo do aviso afirma — por isso só se sabe no fim. */
@@ -413,6 +422,8 @@ export async function processSentiment(event: EventRow): Promise<SentimentResult
}
}
registrarMedicaoDoJev(decisao.engine === "jev");
// ── Merge sentiment into messages.metadata ────────────────────────────
const existingMetadata = (message.metadata as Record<string, unknown> | null) ?? {};
const updatedMetadata = {