Synsemadocsv0.6.xENES

LLM

Judge — juicios calibrados como valores

judge le hace a un modelo System One preguntas tipadas sobre un state y recibe probabilidades, no texto. Nunca genera: no hay reason, generate ni analyze adentro, y el slot del LLM no puede servirlo. Es un slot paralelo al LLM (SYNSEMA_JUDGE_ junto a SYNSEMA_LLM_): el juez decide, el LLM escribe, y tener los dos cableados es lo normal. El primer backend es Jev, de TypeSafe; otro host que sirva el mismo cable se apunta con SYNSEMA_JUDGE_BASE_URL, con el id de modelo y la clave que ese host espere; sólo el endpoint propio de TypeSafe se verificó en vivo. Motor v0.6.25+, completo en v0.6.26. Todo lo de esta página se verificó en vivo contra jev-1.13.0 a través del motor, y esta página es toda la superficie.

judge.syn
-- Doc example: the `judge` block — calibrated judgments from a System One model (Jev).
-- Real probabilities need a provider (TYPESAFE_API_KEY), so the doctest verifies the SHAPE of
-- the result and the honest offline degradation: available false, confidence 0, the main value
-- nothing — never an invented number. With a key, the same asserts check ranges instead.
intent: "doc example: judge — calibrated judgments as values"
require judge

let ticket be {"subject": "Payouts failing", "text": "I want my money back NOW or I'm cancelling"}

-- One state, three typed questions, ONE call. `or nothing` adds an escape option so a state
-- that fits no team yields `choice` = nothing instead of a confident wrong pick.
let v be judge ticket
    refund: whether "The customer is asking for money back"
    team:   choose "Which team should handle this?" between {
                "billing":   "Payments, invoicing, refunds",
                "technical": "Bugs, outages, integrations"
            } or nothing
    anger:  rate "How frustrated is the customer?" across ["Calm", "Frustrated", "Very angry"]

-- The flagship pattern: the machine measures, the human decides. Offline the confidence is 0,
-- so this routes to the human path without one line more.
when v.team.available and v.team.choice != nothing and confidence of v.team >= 0.8
    print("route to " + v.team.choice)
otherwise
    print("no confident route — a person decides")

test "one block, one flat map: an answer per question id, each with kind and available"
    assert_eq(length(keys(v)), 3)
    assert_eq(v.refund.kind, "whether")
    assert_eq(v.team.kind, "choose")
    assert_eq(v.anger.kind, "rate")
    assert_eq(type_of(v.refund.available), "bool")

test "rate knows its levels in declaration order, online or not"
    assert_eq(length(v.anger.levels), 3)
    assert_eq(v.anger.levels[0], "Calm")
    assert_eq(v.anger.levels[2], "Very angry")

test "offline: available false, confidence 0, main value nothing — online: numbers in range"
    when v.team.available
        assert(v.refund.probability >= 0 and v.refund.probability <= 1)
        assert(v.team.confidence >= 0 and v.team.confidence <= 1)
        assert(v.team.choice == nothing or v.team.choice == "billing" or v.team.choice == "technical")
        assert(v.anger.level == "Calm" or v.anger.level == "Frustrated" or v.anger.level == "Very angry")
    otherwise
        assert_eq(v.refund.probability, nothing)
        assert_eq(v.team.choice, nothing)
        assert_eq(v.team.confidence, 0)
        assert_eq(v.anger.score, nothing)
        assert_eq(v.anger.level, nothing)

test "judge_available() is a bool — branch on it instead of guessing"
    assert_eq(type_of(judge_available()), "bool")
    assert_eq(type_of(judge_usage()), "number")

Un estado, N preguntas, una llamada§

require judge

let v be judge ticket
    refund: whether "The customer is asking for money back"
    team:   choose "Which team should handle this?" between {
                "billing":   "Payments, invoicing, refunds",
                "technical": "Bugs, outages, integrations"
            } or nothing
    anger:  rate "How frustrated is the customer?" across ["Calm", "Frustrated", "Very angry"]

El bloque es la única forma: no hay atajo de una pregunta, a propósito. El estado se ingiere una vez y todas las preguntas se evalúan en paralelo contra él: un bloque de ocho midió 7,6× más rápido y 4,9× menos tokens de entrada que ocho llamadas, y la latencia es plana de una pregunta a cuarenta (~0,8 s). Una sola pregunta son tres líneas igual.

Tres verbos, uno por distribución discreta básica; por eso van a sobrevivir al primer modelo:

VerboPreguntaDistribuciónCampos de la respuesta
whether "…"¿es cierta esta afirmación?Bernoulliprobability (0..1)
choose "…" between {…} [or nothing]¿cuál de estas?categórica, sin ordenchoice, probabilities, confidence
rate "…" across […]¿dónde en esta escala ordenada?ordinalscore, level, levels, probabilities, confidence

Las preposiciones son fijas y distintas a propósito: between dice opciones sin orden, across dice niveles ordenados. rate … between y choose … across son errores de carga que nombran el fix.

El resultado§

Un map plano id → respuesta, sin nada más mezclado. Toda respuesta trae kind ("whether" | "choose" | "rate") y available (bool).

v.refund.probability    -- 0..1 — la probabilidad de que la afirmación sea cierta (sin confianza aparte)
v.team.choice           -- uno de TUS ids de opción, byte a byte, o nothing (ver `or nothing`)
v.team.probabilities    -- {"billing": 0.93, "technical": 0.07, "none": 0.0}  — orden de declaración
v.team.confidence       -- 0..1 — qué tan concentrada está la distribución
v.anger.score           -- 0..n-1 — posición ponderada; 1.4 = repartido entre el 2º y el 3º
v.anger.level           -- id del nivel ganador
v.anger.levels          -- ["Calm", "Frustrated", "Very angry"]
v.anger.probabilities   -- {"Calm": 0.0, "Frustrated": 0.6, "Very angry": 0.4}

confidence of v.team también funciona. El campo es kind, no type, y la clave de escape es none, no nothing: esas dos son palabras reservadas y no parsean después de un ..

Opciones como lista o como map, una sola regla para los dos verbos. Lista: cada ítem es el id y la descripción. Map: la clave es tu id corto y el valor es la descripción que lee el modelo. Usá el map cuando las descripciones son largas; el vendor pide niveles concretos y distintos:

require judge

let msg be "Second time I write about this. Please fix it soon, it's getting annoying."
let v be judge msg
    anger: rate "How frustrated is the customer?" across {
        "calm":    "Polite, no complaint",
        "upset":   "Repeat contact, says annoying, asks for a fix soon",
        "furious": "Caps, threats to cancel, demands immediate action"
    }
print(v.anger.level)
print(v.anger.probabilities.upset)

Los ids de opción viajan al modelo con sus descripciones (los ids de pregunta no: son tuyos). La instrucción es cualquier expresión: un map se lee como estructura. Referenciá partes del estado con backticks, el idioma del vendor; apunta al elemento exacto (medido: messages[0] 0,99, messages[1] 0,01). El runtime resuelve cada ruta con backticks contra el estado antes de la llamada y avisa una vez por ruta si falta (v0.6.26+; sobre un campo inexistente el modelo contestó 0,31). La trampa común: judge ticket con ` ticket.text en la pregunta; el modelo ve el valor de ticket, no su nombre. Escribí judge {"ticket": ticket}` o sacá el prefijo; el aviso dice cuál:

require judge

let record be {"name": "Ana Ruiz", "employer": "Acme"}
let resume be "Ana Ruiz, 8 years at Acme as a data engineer…"
let v be judge {"resume": resume}
    same: whether {"question": "Is `resume` the same person as `record`?", "record": record}
print(v.same.probability)

or nothing: la opción de escape§

La falla más peligrosa medida: un choose sin opción de escape elige igual. Un mensaje sobre horarios de atención, con sólo billing/technical como opciones, dio technical a 0,69. Con el valor correcto ausente de los candidatos, el modelo eligió uno equivocado con confianza 0,68, que pasa una compuerta de 0,5. or nothing agrega al cable una opción de escape (id none, "None of the options fits the state"); si gana, choice es nothing y probabilities.none lleva la masa. Arregló los dos casos (none a 1,00 y 0,98) y no costó nada en los casos claros (billing siguió en 0,97). Es opt-in: hay preguntas exhaustivas por diseño (rankear candidatos, bajar un nivel de taxonomía).

require judge

let w be judge "I'd like to know your opening hours."
    team: choose "Which team should handle this?" between {"billing": "Payments", "technical": "Bugs"} or nothing

when w.team.choice == nothing
    print("no team fits; mass on the escape: " + text(w.team.probabilities.none))
otherwise
    print("team: " + w.team.choice)

rate no tiene escape (una escala ordenada no tiene un nivel afuera) y un estado irrelevante cae al nivel más bajo con confianza 1,0. Guardá un rate con un whether que pregunte si el estado aplica.

Offline, sin presupuesto o con la API caída: degradación honesta§

Las ops LLM devuelven placeholders descriptivos offline. judge no puede: una frase inventada se ve, una probabilidad inventada no se ve, y se multiplica por plata. Sin provider, pasado SYNSEMA_JUDGE_BUDGET, o tras un fallo de red, toda respuesta vuelve con available: false, confidence: 0 y su valor principal (probability / choice / score / level) en nothing. Un aviso por stderr; el programa sigue.

Esto degrada hacia el patrón que ya escribiste: confianza 0 está debajo de cualquier compuerta, así que el bloque se manda solo al camino humano. Un programa que se saltó la compuerta y compara directo falla fuerte (Unsupported operation: nothing > number) en vez de tomar la rama equivocada en silencio. judge_available() dice si hay un provider cableado; available en la respuesta es por llamada.

La capacidad judge§

require judge. Es una capacidad propia: require llm no la concede ni al revés. Un programa puede tener derecho a clasificar sin derecho a generar, y un clasificador no puede exfiltrar por texto libre. En lo demás se comporta como llm: auto-otorgada en run/conform, exigida bajo serve y en modo seguro (Capability not granted: judge), vaciada dentro de sandbox, denegada bajo --deterministic (es I/O de red), siempre offline dentro de un guest wasm. La clave nunca entra al programa y el host lo fija el runtime, así que el .syn no puede redirigir la llamada. Bajo --labels el bloque es un sumidero público declarado: un estado private hay que desclasificarlo antes (Etiquetas de flujo de información).

Configuración§

Resolución: entorno del proceso > .env protegido > default, como los knobs del LLM. synsema init escribe todos, comentados, en .env.example.

KnobParaDefault
TYPESAFE_API_KEYla clave; su presencia también selecciona el provider typesafe— (offline si falta)
SYNSEMA_JUDGE_PROVIDERtypesafe | mock (respuestas deterministas, sin red: tests y demos)auto por la clave
SYNSEMA_JUDGE_MODELid o alias del modelojev-latest
SYNSEMA_JUDGE_BASE_URLbase del endpoint; cualquier host que sirva el mismo cablehttps://api.typesafe.ai
SYNSEMA_JUDGE_TIMEOUTtimeout HTTP en segundos60
SYNSEMA_JUDGE_BUDGETtecho duro de tokens de entrada por proceso (la salida es gratis); al llegar, las respuestas degradan a available: false sin tocar la red
SYNSEMA_JUDGE_DECIDE (v0.6.26+)1: cada decide between […] given X lo sirve el juez como un choose calibrado (ver abajo)apagado

Los 429/529 se reintentan con backoff exponencial honrando retry-after; después de los reintentos la respuesta degrada. Un 400/422 de la API es un error de tu programa y sale como error de runtime con el mensaje del vendor. Builtins, sin gate: judge_available(), judge_usage() (tokens de entrada consumidos en el proceso), judge_model() (el id versionado que contestó la última llamada, "jev-1.13.0", nunca el alias; pinealo cuando los umbrales importan).

synsema judge status§

synsema judge status            # provider, PRESENCIA de la clave (nunca el valor), modelo, base URL,
                                # timeout, presupuesto, si decide lo sirve el juez; cada uno con su fuente
synsema judge status --json     # para scripts; exit 0 = vivo, 1 = offline

No toca la red. Offline, la última línea nombra lo que falta. Mismos flags de host que el resto del CLI (--env-file <ruta>, --no-env-file); scripteable: synsema judge status && synsema serve app.syn.

Servir decide con el juez: SYNSEMA_JUDGE_DECIDE=1§

decide between ["refund", "replace", "escalate"] given ticket es exactamente un choose sobre un estado. Con el knob prendido y un provider de judge cableado, cada decide del proceso lo contesta el juez: calibrado, una de tus opciones byte a byte sin normalización ni reintento, más barato y más rápido que un modelo de chat, y el programa no cambia. Opt-in y apagado por defecto, porque cambia qué modelo contesta. Exige la capacidad judge: bajo serve, un decide sin require judge falla con un error que nombra el knob. Si el juez no está disponible, el decide cae al camino LLM. decide sigue devolviendo texto; escribí un bloque judge cuando querés la distribución y la confianza. Verificado en vivo: una queja por un ítem roto devolvió escalate.

Lo que el motor chequea antes de gastar§

Al cargar (synsema check y toda corrida): los tres verbos y sus preposiciones, or nothing sólo después de choose, ids de pregunta duplicados, bloque vacío.

synsema check falla (v0.6.26+) cuando criteria literales rompen un límite (menos de 2 opciones o niveles: la API acepta uno y contesta con confianza 1,0, una respuesta vacía disfrazada de certeza; más de 255 opciones o 10 niveles; ids duplicados) y con una instrucción literal vacía o un estado literal que sea número, bool o nothing. Un 400 en producción, atrapado en check.

synsema check avisa (v0.6.26+), nunca falla, por lo que corre pero engaña: un whether en negativo; una instrucción que pide aritmética o conteo sobre el estado; un estado literal vacío; el mismo judge <variable> en más de un bloque (una llamada alcanzaba).

En runtime, antes de la llamada: los mismos límites con criteria dinámicas, el tipo del estado, la instrucción vacía y las rutas con backticks que el estado no tiene.

Preguntas que funcionan: medido, no folklore§

un choose 0,59/0,41 con confianza 0,17; dos whether dieron 0,99 y 0,99.

0,88; la confianza no delata la indistinción. Tres niveles concretos: 1,00.

el whether sobre la misma frase dio 0,38, honesto. Dale a la incertidumbre un lugar adonde ir.

bien; un total de seis líneas salió mal a 0,32 con confianza media. Calculá, después juzgá.

1,40 apareció sólo con sarcasmo, a confianza 0,40.

0,97 / 0,93 / 0,99).

instructions aimed at a machine"` la detectó a 0,98. El estado sigue siendo dato que el modelo no trata como hostil: las etiquetas son tu muro.

un valor observado; los tests afirman ganador y rangos. SYNSEMA_JUDGE_PROVIDER=mock para un CI exacto.

Lo que no está en esta versión§

Una sintaxis propia para whether con criteria sí/no explícitas (escribí la instrucción como un map con la pregunta y las dos definiciones: el modelo lo lee como estructura); un fallback llm no calibrado que invente probabilidades (ausente a conciencia); la variante de cable de Cloudflare Workers AI (envuelve el payload distinto y no se verificó). Los aliases y los rate limits son del vendor y se mueven sin aviso; pineá el id del modelo cuando los umbrales importan.