Reference notes for the wire-compat hardening pass: what Laya actually does,
read out of the source at laya/laya/ (the repo this workspace vendors
alongside knot/). docs/COMPAT.md states the knot-side contract; this file
records the evidence it is built from. Nothing here is an issue tracker — it
is the behaviour being ported.
Agent.predictbuilds the payload as{"model": "laya-rl-agent", "answers": ..., "usage": ...}(agent.py:1409, batch pathagent.py:1330);Router.predictthen appendsresult["routing"] = dict(decision)(router.py:947), andRouter.predict_batchappends it per result (router.py:1304).shortlist(when the embedding shortlist runs) is appended after that.- Per-answer dicts:
typeis serialized first, then the answer fields — choice:choice,probabilities,confidence,answer_confidence,action; score insertslegendbeforeprobabilities; noul:noul,confidence,answer_confidence,action.window(long-doc deciding window) follows, and gate fields (low_confidence,abstention,abstention_threshold) are appended last because they are assigned after the answer dict exists. usagein a single-shot predict (agent.py:1389):input_tokens,output_tokens,state_tokens,state_tokens_dropped,truncated,truncated_questions, thenoptionsonly when the head budget actually collapsed option spans (collapsed_options).- HTTP body: Starlette serializes with
json.dumps(..., ensure_ascii=False)and compact separators —{"k":v}— so the wire bytes are compact, raw UTF-8, keys in dict insertion order. The spaced", "/": "separators are only the prompt-side rendering (below).
RouteDecisionrenders asmodel,repo,reason,detection,workflow,workflowlast andnullwhen no workflow matched.repois_repo_str(DEFAULT_MODELS[key])(router.py:63): the repo for plain checkpoints,repo/subfolderfor the subfoldered ones (convaiinnovations/laya,.../laya/multilingual,.../laya/typed-decisions).detectionislang.analyse(state)(never omitted;nullwhen there is no analysis).analyse(lang.py:644) returnsscript,script_profile,language,is_english,language_undecided,diacritic_rate,non_latin_fraction,mixed_segment— that order.- Script bookkeeping is first-appearance ordered with
latinspecial-cased:_script_counts(lang.py:287) insertslatinlast;_profile_from_counts(lang.py:329) listslatinfirst, then first-appearance order, dropping zero counts;_script_from_countstakes Pythonmax, i.e. the first maximum in iteration order, so named scripts win ties againstlatin. _analyse_textroundsnon_latin = round(1.0 - prof.get("latin", 0.0), 4)before comparing thresholds (lang.py:571), so the wire value and the comparison value are the same rounded number.
predict_batchwith no question ids returns{"model": "laya-rl-agent", "answers": {}, "usage": {"input_tokens": 0, "output_tokens": 0}}per state (agent.py:1331) — two usage keys only — and routing is appended by the router as usual. No tokenization, no forward pass.
apply_confidence_gate(results, min_confidence)(confidence.py:104): thresholdNone→ nothing written at all; otherwise theflag_low_confidencepass (confidence.py:70) first — skipped formc == 0.0, since nothing can fall below it — then the abstention pass writesabstentionandabstention_threshold = float(mc)on every answer (mc == 0.0only skips the flag, not this).abstentionvalues:"abstained"when the answer was flagged,"unevaluated"when the gate confidence isNone, else"passed".- Gate confidence (
_gate_confidence,confidence.py:38):answer_confidenceif it is a finite non-bool number, elseconfidence, elseNone— andround(x, 4)is applied before the comparison, so a decimal threshold compares against the 4-decimal figure Python computed. Agent.predict_longcalls the gate withNone(agent.py:1698): a window loop has no single confidence to gate on, and every answer still reports that it ran ungated.
- Single-window short-circuit:
len(state_ids) <= budget→ a plainsystem_oneplususage.windows = 1(agent.py:1616area). - Multi-window: usage is aggregated generically (
agent.py:1674area) — numeric fields (including bools, which Python sums as ints, sotruncatedbecomes a count 0..N) are summed across windows, dict fields (options) are shallow-merged, everything else (notablytruncated_questions) is replaced by the last window's value; thenoutput_tokensis forced to 0 andwindows = len(results). - Answer choice:
noultakes the strongest window (maxofnoul),choice/scoretake the most confident (maxofanswer_confidence); the deciding answer carrieswindow = {index, token_start, token_end, count}.
serialize_state, criterion values and non-stringinstructionsgo throughjson.dumps(..., ensure_ascii=False)with Python's default separators —", "and": ". Strings pass through untouched;null/empty-containerinstructionsbecome empty text and are rejected as 422s by_check_question(agent.py:848area).- Python scalar reprs reach the prompt text:
True/False(nottrue),1.0for floats,1e-05-style scientific notation.
- Question ids must be non-empty (
_check_question,agent.py:857). - Choice criteria are a dict or a list of labels; the list normalizes to
{str(label): None}. Labels are checked structurally first (null and list/dict labels → 422), then for duplicates with Python equality — so[1, 1.0]and[True, 1]are one key. Both the option text and the answer/probability keys usestr(label), which is why a bool label echoes as"True". - Noul criteria must be a dict keyed (case-insensitively) only
true/false(agent.py:960area);_to_internalthen lowercases the keys, so{"True": ...}reachescrit.get("true")while any other key is a 422. - Score criteria are a list, non-empty, no null levels.
min_confidencemust be a float in[0.0, 1.0](same 400/422 wording on the HTTP and MCP surfaces).- Batch endpoint caps states at 64 and empty
statesis a 400. - Response JSON uses compact separators; errors are
{"detail": ...}with Laya's status mapping (413/415/422/503/…).