code wiki / (root) / nx_lorebook.nx

nx_lorebook.nx source

↩ module page · 202 lines · 10327 B

1// nx_lorebook.nx -- THE LOREBOOK PLANE (companionchat CC3): keyed world entries as DATA, injected into the chat turn ONLY when 2// the turn touches one of an entry's trigger terms, under an insertion budget that REFUSES LOUDLY instead of truncating. 3// Rows (knowledge/lorebook.conf -- daemon cwd first, then the nishihost tree, the gen_chat_budget.conf idiom): 4// key|trigger1, trigger2, ...|entry text (# comments and blank lines skipped) 5// @budget N (total bytes the [WORLD] block may carry, header included) 6// A row with fewer than three fields is skipped and COUNTED as malformed, never guessed at. Matching is an ASCII 7// case-insensitive substring of a trimmed trigger inside the user turn (non-ASCII case folding is deliberately not done -- 8// a documented imprecision, not an accident); an entry fires at most once per turn however many of its triggers match; a 9// turn that touches no trigger writes ZERO bytes (the neg-control). When the fired entries would exceed the budget, NOTHING 10// is injected partially: out carries one refusal line naming the bytes wanted and the budget, and stats[LB_ST_REFUSED]=1, so 11// the persona is never silently cut and the caller can log the refusal rather than the model reading half a fact. 12// Contract symbol: em_lorebook (the companionchat.matrix CC3 watch). Composed by nx_companion_mind into the chat turn. 13// stats[]: LB_ST_ROWS parsed rows, LB_ST_MALFORMED, LB_ST_FIRED, LB_ST_WANTED bytes, LB_ST_BUDGET effective, LB_ST_REFUSED 0/1. 14// Gate: nx_lorebook_gate (fixture confs under /tmp/<gate>/, written at run time). license_tier: ORIGINAL 15import "nx_syscalls.nx" 16 17const LB_CONF_REL: *u8 = "knowledge/lorebook.conf" as *u8 18const LB_CONF_ABS: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/lorebook.conf" as *u8 19// the default block budget when the conf carries no @budget row: a quarter of the composed persona's 16 KiB prompt buffer 20// is the most a side-plane may take from the model's window before it crowds out the state and memory blocks 21const LB_BUDGET_DEFAULT: i64 = 4096 22const LB_STATS_N: i64 = 8 23const LB_ST_ROWS: i64 = 0 24const LB_ST_MALFORMED: i64 = 1 25const LB_ST_FIRED: i64 = 2 26const LB_ST_WANTED: i64 = 3 27const LB_ST_BUDGET: i64 = 4 28const LB_ST_REFUSED: i64 = 5 29// the refusal line must fit; below this the caller's buffer cannot even carry the refusal, so nothing is written 30const LB_REFUSE_MIN_CAP: i64 = 256 31const LB_ACC_HEADROOM: i64 = 4096 32const LB_NUM_CAP: i64 = 24 33const LB_DEC_BASE: i64 = 10 34const LB_CH_NL: i64 = 10 35const LB_CH_CR: i64 = 13 36const LB_CH_SPACE: i64 = 32 37const LB_CH_HASH: i64 = 35 38const LB_CH_AT: i64 = 64 39const LB_CH_PIPE: i64 = 124 40const LB_CH_COMMA: i64 = 44 41const LB_CH_0: i64 = 48 42const LB_CH_9: i64 = 57 43const LB_CH_A_UP: i64 = 65 44const LB_CH_Z_UP: i64 = 90 45const LB_CASE_DELTA: i64 = 32 46const LB_BUDGET_KEY: *u8 = "@budget" as *u8 47const LB_HEAD: *u8 = "[WORLD -- facts about the world this conversation just touched; treat them as true and use the exact detail]\n" as *u8 48const LB_ITEM: *u8 = "- " as *u8 49const LB_SEP: *u8 = ": " as *u8 50const LB_REFUSE_HEAD: *u8 = "[WORLD refused: the entries this turn touched need " as *u8 51const LB_REFUSE_MID: *u8 = " bytes against a lorebook budget of " as *u8 52const LB_REFUSE_TAIL: *u8 = " -- raise @budget in lorebook.conf or split the entries; nothing was injected partially]\n" as *u8 53 54func lb_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 55func lb_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64 = off; var i: i64 = 0; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o } 56func lb_catn(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64 = off; var i: i64 = 0; while i < n { dst[o] = src[i]; o = o + 1; i = i + 1 } return o } 57func lb_itoa(dst: *u8, off: i64, v: i64) -> i64 { 58 let t: *u8 = sys_mmap(LB_NUM_CAP); var m: i64 = v; var k: i64 = 0 59 if m == 0 { t[0] = LB_CH_0 as u8; k = 1 } 60 while m > 0 { t[k] = (LB_CH_0 + (m % LB_DEC_BASE)) as u8; m = m / LB_DEC_BASE; k = k + 1 } 61 var o: i64 = off 62 while k > 0 { k = k - 1; dst[o] = t[k]; o = o + 1 } 63 return o 64} 65func lb_lower(c: i64) -> i64 { if c >= LB_CH_A_UP { if c <= LB_CH_Z_UP { return c + LB_CASE_DELTA } } return c } 66 67// ASCII case-insensitive: first index of needle[0..nn) in hay[0..hn), or -1. Flag-exit loops, never a clobbered cursor. 68func lb_ci_find(hay: *u8, hn: i64, needle: *u8, nn: i64) -> i64 { 69 if nn <= 0 { return 0 - 1 } 70 var i: i64 = 0 71 while i + nn <= hn { 72 var j: i64 = 0 73 var same: i64 = 1 74 var scan: i64 = 1 75 while scan == 1 { 76 if j >= nn { scan = 0 } else { 77 if lb_lower(hay[i + j] as i64) != lb_lower(needle[j] as i64) { same = 0; scan = 0 } else { j = j + 1 } 78 } 79 } 80 if same == 1 { return i } 81 i = i + 1 82 } 83 return 0 - 1 84} 85 86// 1 when any comma-separated, space-trimmed trigger in trig[0..tn) occurs in the turn; 0 otherwise. Empty triggers never match. 87func lb_triggers_hit(trig: *u8, tn: i64, umsg: *u8, umlen: i64) -> i64 { 88 var hit: i64 = 0 89 var s: i64 = 0 90 var i: i64 = 0 91 while i <= tn { 92 var atend: i64 = 0 93 if i == tn { atend = 1 } else { if (trig[i] as i64) == LB_CH_COMMA { atend = 1 } } 94 if atend == 1 { 95 var a: i64 = s 96 var e: i64 = i 97 var trim: i64 = 1 98 while trim == 1 { if a < e { if (trig[a] as i64) == LB_CH_SPACE { a = a + 1 } else { trim = 0 } } else { trim = 0 } } 99 trim = 1 100 while trim == 1 { if e > a { if (trig[e - 1] as i64) == LB_CH_SPACE { e = e - 1 } else { trim = 0 } } else { trim = 0 } } 101 if e > a { if lb_ci_find(umsg, umlen, ((trig as i64) + a) as *u8, e - a) >= 0 { hit = 1 } } 102 s = i + 1 103 } 104 i = i + 1 105 } 106 return hit 107} 108 109// THE CONTRACT against a named conf. Writes the [WORLD] block (or the refusal line) into out, NUL-terminated; returns bytes. 110func em_lorebook_from(path: *u8, umsg: *u8, umlen: i64, out: *u8, cap: i64, stats: *i64) -> i64 { 111 var k: i64 = 0 112 while k < LB_STATS_N { stats[k] = 0; k = k + 1 } 113 stats[LB_ST_BUDGET] = LB_BUDGET_DEFAULT 114 if cap > 0 { out[0] = 0 as u8 } 115 if umlen <= 0 { return 0 } 116 if cap <= 1 { return 0 } 117 let lp: *i64 = sys_mmap(16) as *i64 118 lp[0] = 0 119 let b: *u8 = sys_read_file(path, lp) 120 if (b as i64) == 0 { return 0 } 121 let n: i64 = lp[0] 122 if n <= 0 { return 0 } 123 // fired entries accumulate here; bounded by the conf's own size plus the per-entry decoration, so it cannot overflow 124 let acc: *u8 = sys_mmap(n + LB_ACC_HEADROOM) 125 var ao: i64 = 0 126 var ls: i64 = 0 127 var i: i64 = 0 128 while i <= n { 129 var atend: i64 = 0 130 if i == n { atend = 1 } else { if (b[i] as i64) == LB_CH_NL { atend = 1 } } 131 if atend == 1 { 132 var le: i64 = i 133 if le > ls { if (b[le - 1] as i64) == LB_CH_CR { le = le - 1 } } 134 if le > ls { 135 let c0: i64 = b[ls] as i64 136 if c0 != LB_CH_HASH { 137 if c0 == LB_CH_AT { 138 if lb_ci_find(((b as i64) + ls) as *u8, le - ls, LB_BUDGET_KEY, lb_len(LB_BUDGET_KEY)) == 0 { 139 var v: i64 = 0 140 var seen: i64 = 0 141 var q: i64 = ls + lb_len(LB_BUDGET_KEY) 142 while q < le { let d: i64 = b[q] as i64; if d >= LB_CH_0 { if d <= LB_CH_9 { v = v * LB_DEC_BASE + (d - LB_CH_0); seen = 1 } } q = q + 1 } 143 if seen == 1 { if v > 0 { stats[LB_ST_BUDGET] = v } } 144 } 145 } else { 146 var p1: i64 = 0 - 1 147 var p2: i64 = 0 - 1 148 var q2: i64 = ls 149 while q2 < le { if (b[q2] as i64) == LB_CH_PIPE { if p1 < 0 { p1 = q2 } else { if p2 < 0 { p2 = q2 } } } q2 = q2 + 1 } 150 if p2 < 0 { stats[LB_ST_MALFORMED] = stats[LB_ST_MALFORMED] + 1 } else { 151 stats[LB_ST_ROWS] = stats[LB_ST_ROWS] + 1 152 if p2 + 1 < le { 153 if lb_triggers_hit(((b as i64) + p1 + 1) as *u8, p2 - p1 - 1, umsg, umlen) == 1 { 154 stats[LB_ST_FIRED] = stats[LB_ST_FIRED] + 1 155 ao = lb_cat(acc, ao, LB_ITEM) 156 ao = lb_catn(acc, ao, ((b as i64) + ls) as *u8, p1 - ls) 157 ao = lb_cat(acc, ao, LB_SEP) 158 ao = lb_catn(acc, ao, ((b as i64) + p2 + 1) as *u8, le - p2 - 1) 159 acc[ao] = LB_CH_NL as u8 160 ao = ao + 1 161 } 162 } 163 } 164 } 165 } 166 } 167 ls = i + 1 168 } 169 i = i + 1 170 } 171 if stats[LB_ST_FIRED] == 0 { return 0 } 172 // the effective budget is the smaller of the conf's and the caller's buffer -- one refusal shape, both bounds honest 173 if cap - 1 < stats[LB_ST_BUDGET] { stats[LB_ST_BUDGET] = cap - 1 } 174 let wanted: i64 = lb_len(LB_HEAD) + ao 175 stats[LB_ST_WANTED] = wanted 176 if wanted > stats[LB_ST_BUDGET] { 177 stats[LB_ST_REFUSED] = 1 178 var ro: i64 = 0 179 if cap > LB_REFUSE_MIN_CAP { 180 ro = lb_cat(out, 0, LB_REFUSE_HEAD) 181 ro = lb_itoa(out, ro, wanted) 182 ro = lb_cat(out, ro, LB_REFUSE_MID) 183 ro = lb_itoa(out, ro, stats[LB_ST_BUDGET]) 184 ro = lb_cat(out, ro, LB_REFUSE_TAIL) 185 } 186 out[ro] = 0 as u8 187 return ro 188 } 189 var o: i64 = lb_cat(out, 0, LB_HEAD) 190 o = lb_catn(out, o, acc, ao) 191 out[o] = 0 as u8 192 return o 193} 194 195// THE CONTRACT: the daemon-cwd conf when it exists and is non-empty, else the nishihost tree's. 196func em_lorebook(umsg: *u8, umlen: i64, out: *u8, cap: i64, stats: *i64) -> i64 { 197 let lp: *i64 = sys_mmap(16) as *i64 198 lp[0] = 0 199 let b: *u8 = sys_read_file(LB_CONF_REL, lp) 200 if (b as i64) != 0 { if lp[0] > 0 { return em_lorebook_from(LB_CONF_REL, umsg, umlen, out, cap, stats) } } 201 return em_lorebook_from(LB_CONF_ABS, umsg, umlen, out, cap, stats) 202}