nx_actor_role_llm_v2.nx source
↩ module page · 266 lines · 10430 B
1// nx_actor_role_llm_v2.nx -- REAL transformer-forward LLM actor.
2//
3// Where [[nx_actor_role_llm]] drives a tiny 3-step micro-pipeline
4// (embed + matmul + sample) over synthetic toy weights, THIS adapter
5// wraps the full Llama-class runner [[nx_llm_run_v2]] -- multi-layer
6// transformer forward including RoPE + RMSNorm + multi-head attention
7// + SwiGLU FFN + output projection + temperature + top_k + sample.
8//
9// Cooperative phases:
10// Phase 0 (INIT) : reserved
11// Phase 1 (RUN) : single call to nx_llm_generate_one_v2(spec, gguf,
12// hdr, bpe, prompt, ..., prng, rope_base, attn_scale)
13// -- this is the monolithic generate-one-token step
14// Phase 2 (EMIT) : fanout LLM_TOKEN with the generated token if
15// positive; else fanout ERROR_REPORT with verdict id
16// Phase 3 (DONE) : terminal
17//
18// V1 of this adapter is single-shot (one token per actor lifetime).
19// V2 will add a reset_for_next_token loop similar to nx_actor_role_llm.
20//
21// Composes [[project-conductor-arc-phases-c-d-integrate-with-nishi-ai
22// -2026-05-19]] -- the LLM substrate the other agent built becomes
23// callable from inside the parallel-companion actor system.
24
25import "nx_syscalls.nx"
26import "nx_tier.nx"
27import "nx_model_spec.nx"
28import "nx_gguf.nx"
29import "nx_bpe.nx"
30import "nx_llm_run_v2.nx"
31import "nx_actor.nx"
32import "nx_message.nx"
33
34// ===== Sealed enum: NxLlmV2ActorPhase =============================
35
36const NX_LV_PHASE_INIT: nx_int = 0
37const NX_LV_PHASE_RUN: nx_int = 1
38const NX_LV_PHASE_EMIT: nx_int = 2
39const NX_LV_PHASE_DONE: nx_int = 3
40const NX_LV_PHASE_N: nx_int = 4
41
42// ===== Sealed enum: NxLlmV2ActorVerdict ===========================
43
44const NX_LV_V_STEPPED: nx_int = 0
45const NX_LV_V_COMPLETED: nx_int = 1
46const NX_LV_V_FAILED_RUN: nx_int = 2
47const NX_LV_V_INVALID: nx_int = 3
48const NX_LV_V_NULL: nx_int = 4
49const NX_LV_V_N: nx_int = 5
50
51// ===== Struct: NxLlmV2ActorCtx ====================================
52//
53// All inputs caller-owned (spec / gguf / bpe / prompt). Adapter
54// stores the returned token + the underlying runner verdict for
55// caller inspection.
56
57struct NxLlmV2ActorCtx {
58 spec: *NxModelSpec,
59 gguf_buf: *u8,
60 hdr: *NxGgufHeader,
61 bpe: *NxBpeVocab,
62 prompt_text: *u8,
63 prompt_len: nx_int,
64 temperature_q10: nx_int,
65 top_k: nx_int,
66 prng_state: *i64,
67 rope_base: nx_int,
68 attn_scale_q10: nx_int,
69 runner_result: nx_int, // raw return: >=0 = token, <0 = -verdict
70 runner_verdict: nx_int, // decoded verdict (NX_LR2_OK on success)
71 current_phase: nx_int,
72 last_step_verdict: nx_int,
73}
74
75const NX_LV_CTX_BYTES: nx_int = 120 // 15 fields * 8
76
77// ===== Validators =================================================
78
79func nx_lv_phase_is_valid(p: nx_int) -> nx_int {
80 if p < 0 { return 0 }
81 if p >= NX_LV_PHASE_N { return 0 }
82 return 1
83}
84
85func nx_lv_v_is_valid(v: nx_int) -> nx_int {
86 if v < 0 { return 0 }
87 if v >= NX_LV_V_N { return 0 }
88 return 1
89}
90
91// ===== Constructor ================================================
92
93func nx_lv_actor_new(spec: *NxModelSpec,
94 gguf_buf: *u8,
95 hdr: *NxGgufHeader,
96 bpe: *NxBpeVocab,
97 prompt_text: *u8,
98 prompt_len: nx_int,
99 temperature_q10: nx_int,
100 top_k: nx_int,
101 prng_state: *i64,
102 rope_base: nx_int,
103 attn_scale_q10: nx_int) -> *NxLlmV2ActorCtx {
104 if (spec as i64) == 0 { return 0 as *NxLlmV2ActorCtx }
105 if (gguf_buf as i64) == 0 { return 0 as *NxLlmV2ActorCtx }
106 if (hdr as i64) == 0 { return 0 as *NxLlmV2ActorCtx }
107 if (bpe as i64) == 0 { return 0 as *NxLlmV2ActorCtx }
108 if (prompt_text as i64) == 0 { return 0 as *NxLlmV2ActorCtx }
109 if (prng_state as i64) == 0 { return 0 as *NxLlmV2ActorCtx }
110 if prompt_len <= 0 { return 0 as *NxLlmV2ActorCtx }
111 if temperature_q10 <= 0 { return 0 as *NxLlmV2ActorCtx }
112 if top_k <= 0 { return 0 as *NxLlmV2ActorCtx }
113 if rope_base <= 1 { return 0 as *NxLlmV2ActorCtx }
114 if attn_scale_q10 <= 0 { return 0 as *NxLlmV2ActorCtx }
115 let raw: *u8 = sys_mmap(NX_LV_CTX_BYTES)
116 let ctx: *NxLlmV2ActorCtx = raw as *NxLlmV2ActorCtx
117 ctx.spec = spec
118 ctx.gguf_buf = gguf_buf
119 ctx.hdr = hdr
120 ctx.bpe = bpe
121 ctx.prompt_text = prompt_text
122 ctx.prompt_len = prompt_len
123 ctx.temperature_q10 = temperature_q10
124 ctx.top_k = top_k
125 ctx.prng_state = prng_state
126 ctx.rope_base = rope_base
127 ctx.attn_scale_q10 = attn_scale_q10
128 ctx.runner_result = 0
129 ctx.runner_verdict = NX_LR2_OK
130 ctx.current_phase = NX_LV_PHASE_INIT
131 ctx.last_step_verdict = NX_LV_V_STEPPED
132 return ctx
133}
134
135// ===== Phase: RUN -- monolithic call into v2 runner ===============
136
137func _lv_step_run(ctx: *NxLlmV2ActorCtx) -> nx_int {
138 let r: nx_int = nx_llm_generate_one_v2(
139 ctx.spec, ctx.gguf_buf, ctx.hdr, ctx.bpe,
140 ctx.prompt_text, ctx.prompt_len,
141 ctx.temperature_q10, ctx.top_k,
142 ctx.prng_state, ctx.rope_base, ctx.attn_scale_q10)
143 ctx.runner_result = r
144 if r >= 0 {
145 ctx.runner_verdict = NX_LR2_OK
146 return NX_LV_V_STEPPED
147 }
148 // r < 0 means error verdict (encoded as 0 - verdict_id)
149 ctx.runner_verdict = 0 - r
150 return NX_LV_V_FAILED_RUN
151}
152
153// ===== Phase: EMIT ================================================
154
155func _lv_step_emit(ctx: *NxLlmV2ActorCtx,
156 bus: *NxMessageBus,
157 sender_actor_id: nx_int,
158 now_us: nx_size) -> nx_int {
159 if (bus as i64) == 0 { return NX_LV_V_STEPPED }
160 if ctx.runner_result >= 0 {
161 // success: send LLM_TOKEN with token id
162 nx_ms_send_fanout(bus, sender_actor_id, NX_MS_KIND_LLM_TOKEN,
163 ctx.runner_result as nx_size, 8, now_us)
164 }
165 if ctx.runner_result < 0 {
166 // failure: send ERROR_REPORT with verdict id
167 nx_ms_send_fanout(bus, sender_actor_id, NX_MS_KIND_ERROR_REPORT,
168 ctx.runner_verdict as nx_size, 8, now_us)
169 }
170 return NX_LV_V_STEPPED
171}
172
173// ===== Public step ================================================
174
175func nx_lv_actor_step(ctx: *NxLlmV2ActorCtx,
176 sched: *NxActorScheduler,
177 bus: *NxMessageBus,
178 actor_id: nx_int,
179 now_us: nx_size) -> nx_int {
180 if (ctx as i64) == 0 { return NX_LV_V_NULL }
181 if ctx.current_phase >= NX_LV_PHASE_DONE { return NX_LV_V_COMPLETED }
182 var v: nx_int = NX_LV_V_STEPPED
183 if ctx.current_phase == NX_LV_PHASE_INIT { v = NX_LV_V_STEPPED }
184 if ctx.current_phase == NX_LV_PHASE_RUN { v = _lv_step_run(ctx) }
185 if ctx.current_phase == NX_LV_PHASE_EMIT { v = _lv_step_emit(ctx, bus, actor_id, now_us) }
186 ctx.last_step_verdict = v
187 // Advance phase even if RUN failed -- caller still wants the EMIT
188 // path so the error gets reported through the message bus.
189 ctx.current_phase = ctx.current_phase + 1
190 if (sched as i64) != 0 { nx_ac_step(sched, actor_id, 1000, now_us) }
191 if ctx.current_phase >= NX_LV_PHASE_DONE {
192 if (sched as i64) != 0 {
193 if v == NX_LV_V_FAILED_RUN {
194 // FAIL the scheduler actor only if RUN failed -- caller can
195 // distinguish "completed with token" from "completed with error".
196 nx_ac_fail(sched, actor_id)
197 }
198 if v != NX_LV_V_FAILED_RUN {
199 nx_ac_complete(sched, actor_id)
200 }
201 }
202 return NX_LV_V_COMPLETED
203 }
204 return NX_LV_V_STEPPED
205}
206
207// ===== Autoregressive reset =======================================
208//
209// Matches the v1 adapter's reset_for_next_token discipline. Caller
210// generated token N via this actor and wants token N+1 from the
211// same context, typically with an updated prompt that includes token
212// N's bytes. V2 of THIS reset takes a NEW prompt buffer + length
213// because the runner is monolithic (it doesn't expose KV cache to the
214// adapter), so each "next token" is a full re-tokenize + forward.
215//
216// Future v3 will compose with an adapter-level KV-cache primitive so
217// the reset can carry hidden state without re-running the prefix.
218//
219// Refuses if ctx is not in DONE state OR new prompt is null/empty.
220
221func nx_lv_actor_reset_for_next_token(ctx: *NxLlmV2ActorCtx,
222 sched: *NxActorScheduler,
223 actor_id: nx_int,
224 new_prompt: *u8,
225 new_prompt_len: nx_int) -> nx_int {
226 if (ctx as i64) == 0 { return NX_LV_V_NULL }
227 if ctx.current_phase < NX_LV_PHASE_DONE { return NX_LV_V_INVALID }
228 if (new_prompt as i64) == 0 { return NX_LV_V_INVALID }
229 if new_prompt_len <= 0 { return NX_LV_V_INVALID }
230 ctx.prompt_text = new_prompt
231 ctx.prompt_len = new_prompt_len
232 ctx.current_phase = NX_LV_PHASE_INIT
233 ctx.runner_result = 0
234 ctx.runner_verdict = NX_LR2_OK
235 ctx.last_step_verdict = NX_LV_V_STEPPED
236 // Restore scheduler actor from COMPLETED / FAILED back to READY.
237 if (sched as i64) != 0 {
238 let a: *NxActor = nx_ac_find(sched, actor_id)
239 if (a as i64) != 0 { a.state = NX_AC_STATE_READY }
240 }
241 return NX_LV_V_STEPPED
242}
243
244// ===== Accessors ==================================================
245
246func nx_lv_actor_phase(ctx: *NxLlmV2ActorCtx) -> nx_int {
247 if (ctx as i64) == 0 { return NX_LV_PHASE_DONE }
248 return ctx.current_phase
249}
250
251func nx_lv_actor_token(ctx: *NxLlmV2ActorCtx) -> nx_int {
252 if (ctx as i64) == 0 { return -1 }
253 if ctx.runner_result < 0 { return -1 }
254 return ctx.runner_result
255}
256
257func nx_lv_actor_runner_verdict(ctx: *NxLlmV2ActorCtx) -> nx_int {
258 if (ctx as i64) == 0 { return NX_LR2_ERR_BAD_SPEC }
259 return ctx.runner_verdict
260}
261
262func nx_lv_actor_is_done(ctx: *NxLlmV2ActorCtx) -> nx_int {
263 if (ctx as i64) == 0 { return 0 }
264 if ctx.current_phase >= NX_LV_PHASE_DONE { return 1 }
265 return 0
266}