5.8 KiB
SPEC-001 — Responder + response envelope
Characterization of the current (v2) responder contract in
fjerkroa_bot/ai_responder.py. The model answers with a JSON
envelope: answer, answer_needed, channel, staff, picture,
picture_edit, hack. FDB-005 will replace the transport of this
contract (structured outputs); the behavioral requirements below
survive that change unless marked otherwise.
ENV-01 — Model answer reaches the user (coverage: feature)
When the model envelope carries a non-empty answer and
answer_needed is true, AIResponder.send() returns an AIResponse
with that answer text and answer_needed == True. This is the core
loop: user talks, bot answers.
ENV-02 — Suppressed answer stays silent (coverage: feature)
When the model envelope sets answer_needed to false (and the
message is not direct, mentions nobody, and no staff note is set),
the returned AIResponse has answer_needed == False. The bot may
observe without butting in.
ENV-03 — Staff note forces delivery (coverage: feature)
When the envelope carries a non-null staff text and a non-null
answer, the returned response preserves the staff text and has
answer_needed == True. Staff alerts must never be silently
dropped.
ENV-04 — Direct messages are always answered (coverage: feature)
When the incoming AIMessage is marked direct and the model
returns a non-null answer, answer_needed is forced to True
regardless of the model's own answer_needed. A user addressing the
bot directly gets a reply.
ENV-05 — Short-path rules skip the model (coverage: feature)
When a configured short-path [channel-regex, user-regex] pair
matches the message, send() appends the message to history, trims
history to the limit, persists it, and returns an empty response
(answer None, answer_needed False) without calling the model.
Cheap archival of noisy channels.
ENV-06 — Malformed model output is repaired (coverage: withdrawn — successor ENV-18)
Withdrawn 2026-07-13 with FDB-005: strict structured outputs make the repair model obsolete. Malformed output now counts as a failed attempt — see ENV-18.
ENV-07 — History is trimmed to the limit (coverage: feature)
After a completed exchange, len(history) <= history-limit holds.
Trimming happens both before the model call and after appending the
new question/answer pair.
ENV-08 — Markdown links are unwrapped (coverage: feature)
In the final answer text, [label](url) becomes url and
@[label](url) becomes label. Discord renders raw URLs; markdown
link syntax from the model reads as noise.
ENV-09 — Missing channel falls back to the message channel (coverage: feature)
When the envelope channel is null/none/empty, the response channel
is the channel the message came from.
ENV-10 — System prompt template substitution (coverage: test)
message() substitutes {date} (YYYY-MM-DD), {time}, {memory}
(the assembled memory block — legacy memory string while the
structured memory is inactive, see MEM-10) in the system prompt;
{news} is replaced with the news file content when the configured
file exists and stays literal when it does not.
ENV-11 — Per-channel history shrink prefers busy channels (coverage: test)
shrink_history_by_one() removes the oldest entry whose channel has
more than history-per-channel (default 3) entries; when no channel
exceeds the cap, the oldest entry overall is removed.
ENV-12 — Exhausted retries raise, attempts are spaced (coverage: test)
When the model returns no usable answer three times in a row,
send() raises RuntimeError. Consecutive attempts are separated by
an exponential-backoff sleep — failures never hammer the API
back-to-back (D1).
ENV-13 — Reaction-clear events are recorded (coverage: test)
on_reaction_clear(message, reactions) — the discord.py signature —
records the clearing in the channel's memory. (D7: the previous
handler declared (reaction, user) and crashed on dispatch.)
ENV-14 — update_memory persists its argument (coverage: withdrawn — successor SPEC-002)
Withdrawn 2026-07-13 with FDB-007: the per-answer memory rewrite
(update_memory/memoize) is deleted; structured memory (MEM-01+)
replaces it. The D12 defect died with the code.
ENV-15 — retry-model is used after a rate limit (coverage: test)
After a rate-limited attempt, the next chat() attempt uses the
configured retry-model instead of model; a successful attempt
switches back. (D2: the fallback was assigned to a local and never
took effect.)
ENV-16 — Model calls are never served from a disk cache (coverage: test)
openai_chat/openai_image call the client every time. The pickle
response cache (openai_chat.dat) is a test-era artifact and must
not exist in the production path (D3/D4).
ENV-17 — IGDB tool execution runs in a worker thread (coverage: test)
_execute_igdb_function executes the synchronous IGDB library off
the event loop (worker thread), returning identical results. The
event loop keeps serving Discord events during lookups (D5).
ENV-18 — Malformed or refused output is a failed attempt (coverage: test)
Model output is requested as schema-validated JSON (strict structured
outputs). Output that still fails to parse, or a model refusal,
counts as a failed attempt (backoff + retry per ENV-12) — there is no
repair model, no fix() path, no relaxed-JSON fallback. Replaces
ENV-06.
ENV-19 — The envelope schema is pinned (coverage: test)
Every chat call carries response_format = strict JSON schema named
envelope with exactly the fields answer, answer_needed,
channel, staff, picture, picture_edit, hack — all required,
additionalProperties: false, nullable where the protocol allows
null. Tool-followup calls carry the same format.