Releases: wangkant/personagent
Release list
v0.2.0 — a reply loses a glyph, never its voice
The headline: an unsupported character now degrades to a missing glyph,
never to silence. The reply validator is still a fail-closed whitelist —
that is a token-leak defence and it stays — but "reject" used to mean "drop
the whole reply", and a whitelist narrow enough to catch a chat template is
also narrow enough to catch ok ❤️ sure. Measured against the old
validator, ordinary replies with emoji, curly quotes, an ellipsis or a
katakana word produced "": the user saw nothing on the turn they cared
about.
Added
- A three-tier reply character policy: STRIP / MAP / ALLOW. Emoji,
variation selectors, ZWJ sequences and decorative symbol blocks are
stripped (the reply survives minus the glyph); curly quotes, dashes and
no-break spaces are mapped to their ASCII spelling; and named letter
ranges join the whitelist itself, each with a written reason. A code point
named in no tier still drops the reply — adding a script stays a
deliberate act. The policy ships with its own suite
(tests/test_textproc.py): a leak corpus that must stay silenced under
the widest style a persona can express, plus a test-of-the-test that
fails if the corpus could no longer detect an over-broad widening. - Six more scripts on the default path. Kana, Hangul, Cyrillic, Greek,
Arabic and Latin-with-diacritics are how languages are spelled, not
registers a persona opts into:café later,нет проблемandなるほど
are content now. Thai, Hebrew, Devanagari and friends still fail closed
until someone names them. - Per-persona character opt-ins (
ReplyStyle), with a card to carry
them. A new optionalPERSONA_CARD_FILE(defaultpersona.card.json)
may declare{"reply_style": {"emoji": true, "charsets": ["music"], "max_chars": 320}}. Optional charsets are ellipsis, music and arrows —
registers, not languages. Every malformed value fails toward the narrow
default, and the arrows opt-in buys narration (s1 → s2), not a frame:
an arrow hugging a bare token (←persona→) is rejected by shape, so the
opt-in cannot be used to smuggle a template past the whitelist. - A persona
[style]declaration block. A persona document may end
with a[style]block declaring six register knobs (length,vent,
recs,good_news,particles,fatigue). The block is parsed against
a single knob table and stripped from the prose, so raw configuration
never reaches the model as persona text; prose-shaped lines, unclosed or
repeated blocks and orphan closers all resolve toward keeping the
persona's sentences. current_tz_offset_h— a per-turn timezone contextvar for gateway
embedders whose users are not all in the deployment'sTZ_OFFSET_HOURS.
Changed
- The per-turn reply ceiling rose from 500 to 800 characters, and
truncation got a visible seam. The ceiling is also the per-turn
exfiltration bound, so it moved deliberately: the widest length band's
English reading did not fit under 500, which turned the band into a
truncation machine. A cut reply now ends in a visible...rather than
pretending it was whole. - The trusted trailer, and honesty about being an AI. The system prompt
now ends with<trusted_directives>— application-authored text a persona
document cannot displace — and the persona sits in an unforgeable
<persona>region above it. The directives carry the safety exceptions
and an affirmative honesty clause; the engine no longer instructs any
persona to deny being an AI, in either chat path. - Gateway transport hardening. The gateway-conversation LRU warns once
instead of per message, skips evicting a conversation whose lock is
currently held, and releases waiters on eviction; private DM history is
capped instead of growing without bound.
Fixed
- Pacing survives CRLF, and no bubble is a wall.
\r\nbreaks are
honored by the splitter instead of leaking\rinto bubbles; a run of
punctuation can no longer produce a zero-length bubble, and discarding an
all-whitespace chunk no longer discards the hard break it carried. - Emoji modifiers no longer drop the whole reply. U+FE0F, U+200D,
keycaps, flags and skin-tone modifiers survived the old emoji strip,
reached the whitelist, and silenced the turn.
The engine work below was ported back from the maintainer's private fork of
this engine; the entries above are that sync. What follows was already on
main awaiting release.
Fixed
-
The benchmark's blind judging is now actually blind — and actually judges.
Four measurement defects, found by running the thing: the "blind" inbox spelled
the arm out in everyitem_id; a run the judge scored 5-across-the-board (zero
variance) was plotted as a tidy curve instead of being refused; the model's
PASSsentinel was graded as if someone had typed the word (polluting both
the learning material and the judged sample); and a reply-only judge rated a
drafted apology letter 5/5 "like a friend offering a script" because without
the chat context, over-formality is invisible. Item ids are opaque digests
now;ingestnames void runs (zero variance, silent-rate imbalance,
no-feedback on-arm,--style fullceilings) and exits 2; PASS collapses to
silence and silence is counted per arm instead of judged; the judge sees the
scenario context (identical for both arms) and rates against the persona
register, not mere human-plausibility. -
An empty reply with
finish_reason=lengthis retried once at 4x the
budget. A reasoning model can spend the whole token budget on hidden
chain-of-thought and emit nothing visible; every turn came back empty with
only a terse warning. The retry recovers the turn and the log now names the
likely cause (model choice) and the fix. -
The self-evaluator scores register, not "quality" — and the learning
trigger moved to match. Measured three times: a "Here you go: [drafted
apology]" reply got 4/5 ("slightly formal") from the quality-framed prompt,
a bolted-on "blatant tells cap at 2" anchor was talked around ("AI-like,
though not blatant" -> 4), and the same model that rated the same letter
5/5 as a quality-evaluator rated it 3 as a register-judge -- the frame, not
the model, was the problem. The eval prompt now defines the persona register
and scores against it (5 = the register, 3 = drifting into
helpful-assistant, 1 = broke character). Validated on 8 known-label
replies: every known tell scored exactly 3, every casual line 5. Because 3
now means "assistant drift",EVOLVE_THRESHOLDdefaults to 3 -- with the
old default of 2 the loop would still collect nothing. -
Thinking-mode models no longer silently skip the JSON reply protocol.
Measured on the real reply path: with thinking on, the model treats its
hidden reasoning channel as having satisfied the protocol'sreasoning
field and emits only the bare chat line -- 9 of 17 @-directed turns were
dropped whole by the fail-closed parser. JSON-protocol call sites now send
response_format={"type":"json_object"}(0 drops in 52 measured turns and
a 10-turn live check), and the budget-starvation retry also fires on a
truncated-but-non-empty JSON, which used to vanish without even a length
warning. Bare text is still never accepted by the parser: the protocol
boundary stays fail-closed. -
Token budgets raised for reasoning models, and
disable_thinkingis now
real. Hidden thinking tokens bill againstmax_tokens, so the old
budgets starved: the reply path truncated about 1 turn in 10, and the
web-search decision at 150 tokens could not even fit its tool call -- with
thinking on that endpoint rarely emits tool calls at any budget, so the
search gate now disables thinking outright. Reply 1200->3000, gate
600->1500, search decision 150->800, evolve draft 600->2000, self-eval
800->1500, reaction adjudication 400->1000, sticker tagging 200->600 and
40->300 (thinking off), default cap 2048->4096. Thedisable_thinking
parameter, previously documented as ignored, now maps to the endpoint's
thinking switch.
Added
- Six assistant-bait scenario families (
rec-request,tech-help,
explain-bait,decision-bait,task-bait,plan-bait, 18 train + 12
holdout). The original families are all easy social chatter; none exercised
the style rules the loop is supposed to re-derive. Each new family baits the
weak-styled model into a register the persona forbids. tools/scenario_probe.py— calibrates candidate scenarios against the
real model before they earn a place in the benchmark: reports each
scenario's self-eval and blind-judge score so curation is evidence, not
intuition.--judge openai— routes blind judging to any OpenAI-compatible
endpoint (BENCH_JUDGE_BASE_URL/BENCH_JUDGE_API_KEY, falling back to
theDEEPSEEK_*vars). A failed judge call is dropped, never backfilled
with a neutral 3 — a fabricated middle score manufactures the "no
difference" verdict the benchmark exists to test for. Theanthropic
backend now behaves the same way.
Recording something and being changed by it are now separate acts. A reaction is
evidence. An adjudication creates a candidate. Promotion grants a
candidate authority over future replies. Rollback or supersession takes that
authority away without erasing the history.
Changed
- No automatic signal writes a retrieval pool any more. An accepted
correction, an accepted retry and a positive reaction previously landed in
runtime/feedback.<lang>.jsonlorruntime/examples.<lang>.jsonl— the
correction and retry paths on the strength of one signal each. All four
automatic channels (reaction correction, reaction rejection, retry-completion,
self-e...