Notes/brain-dump — contact that the proceedurally modeled into a PLAN below.
-
trace-writer should be part of core, not just eval.
-
console log streaming
-
log file streaming
-
logs
- inference logs
- gateway logs
- eval logs
-
WORKSPACE
- temp workspace
- live workspace
-
EVAL
- Oracle
- Should run through real gateway with intercepted output and tool response
- Test for common things seperately too
- Write a chapter called X about Y
- Write a wiki about X from chapter Y
- Write wiki for all characters in chapter X
- What happened in this chapter?
- Do you see any problems with this chapter?
- evaluate how complete _plan.md is.
- Oracle
-
Note about how strict i need the grammar to be
- Examples show "prefered" the output format
- Grammar shows the most "leaniant" valid format
- grammar enforce block level indented newlines
- grammar only needs to <tool_call> anywhere in the message and json is valid.
-
Eval potentially can be (Client + Gateway in one) or can be EvalGateway
-
storyteller examples
- you need a think block in frontmatter format on the md example files
- basically more target think blocks about the story itself.
- you need a think block in frontmatter format on the md example files
-
State tuning from examples should not need to reprocess if hash still matches
-
Should not keep the model in vram memory all the time if not in use (keep in normal RAM)
- Currently lives under
src/eval/and gates onTRACES_DIR = src/eval/.traces. Promote to core. Re-export fromsrc/eval/trace-writer.tsfor back-compat. - New home allows
pnpm eval,pnpm story(CLI), and eval/test runners to all log to the same.traces/root. - Keep
TraceWriterAPI stable:open,write,beginLine,append,endLine,separator,raw,verification,close.
- A1. Move file, update
TRACES_DIRto repo-root.traces/. - A2. Add re-export shim:
export * from "../core/trace-writer.ts"atsrc/eval/trace-writer.ts. - A3. Update
.gitignoreentry fromsrc/eval/.traces/→.traces/. - A4. Make the writer take an optional
pathargument so callers can choose location (default.traces/<ts>_<mode>.txt).
- The trace writer already calls
fs.fsyncSyncper token. Add aLogStreamthat pipes:- stdout → trace file
- stderr → trace file
- optionally, tee to a second sink (e.g. a web-socket during dev).
- Wire
LogStreamthrough:pnpm inference:start(inference logs)pnpm gateway:start(gateway logs)pnpm eval(eval logs)
- Behavior: live tail-able via
pnpm {inference,gateway,eval}:logs(already exists forinference:/gateway:) — addeval:logsand aeval:tail-logsfor parity.
- B1.
src/core/log-stream.ts— wraps aWriteStreamandprocess.stdout|stderr, forwards writes, supportsstop(). - B2. Hook
LogStreamintocli.tsrunGatewayandrunCli(mediated through a--log-file=...arg). - B3. Add
eval:logsandeval:tail-logstopackage.json(sourcing.eval.logwritten bypnpm eval).
- Today: workspace dirname is hard-coded in agent loops, eval, and CLI.
- Want two modes:
- temp: every session gets a unique worker dir under
.tmp/workspace/<ts>_<id>/(default forpnpm evalandpnpm tell --ephemeral). Auto-cleaned. - live: writes go under
<cwd>/workspace/<slug>/so the user can review the actual files afterward (default forpnpm chapter,pnpm plan).
- temp: every session gets a unique worker dir under
- Implementation:
src/core/workspace.tsexportsresolveWorkspace({ mode: 'live' | 'temp', slug })returning an absolute path and creating the directory.EvalControllercallsresolveWorkspace({ mode: 'temp', slug })and passes it through; CLI passes a--workspace=liveflag or pickstempfor--ephemeral.- The socket-side path on the gateway uses a path-segment sanitizer (
@filepath.txtstyle) so we never write outside the resolved root.
- C1.
src/core/workspace.ts+ tests. - C2. CLI argparse:
--workspace=live|temp. - C3. Eval defaults to
temp. - C4. Document in
AGENTS.md.
- Currently oracle uses
MockModeland never goes through web-rwkv. Replace with an EvalGateway that:- Spawns the gateway processes OR runs an HTTP model and intercepts the model layer.
- Has a "transcript" mode: feeds a fixed list of mock replies to the gateway via the
/rpc/injectadmin endpoint (add), then asserts on the resulting state.
- Two architectural variants:
- Client+Gateway in one: a test bin that imports
GatewayServer, hooks anEvalGatebetweenEngineand the wire, and runs eval in-process. SameEvalControllersemantics. - EvalGateway: a separate process (
pnpm eval-gateway) that listens on 3130 and is the target ofpnpm eval:live --gateway=3130.
- Client+Gateway in one: a test bin that imports
Standalone evals (each becomes a jsonl fixture that lives in src/eval/cases/):
- "Write a chapter called X about Y" → expects exactly one
writetochapter-XXX.mdand the body matchesY. - "Write a wiki about X from chapter Y" → expects a
readof chapter Y, then writes awiki/<category>/<X>.md. - "Write wiki for all characters in chapter X" → expect 1+
writecalls underwiki/character/. - "What happened in this chapter?" → expect a single assistant turn ending in
</tool_call>(no writes). - "Do you see any problems with this chapter?" → expect
read→ assistant text. - "Evaluate how complete _plan.md is" → expect
readof_plan.md→ assistant text.
Each case is one of:
- mock-mode: uses the EvalGateway with injected responses (oracle-style), asserts on
controller.runAgentHierarchyoutput. - live-mode: hits
:3130(inference:start) and asserts on actual model output.
Document this in AGENTS.md "Grammar strictness":
- Examples show the preferred output (every line indented, well-formed think block, etc.). They are the stylistic ground truth.
- Grammar is the most lenient valid format — it enforces:
- Block-level indentation for content lines (each non-empty line starts with
\t). - Anywhere in the message:
<think>…</think>and/or<tool_call>…</tool_call>. - JSON inside
<tool_call>parses as{name, arguments}.
- Block-level indentation for content lines (each non-empty line starts with
- Anything more than that is desirable but not required by the grammar. This means:
- Stylistic variations (no think block, multiple think blocks, free text) still parse.
- Mixture of tabs/spaces is OK as long as JSON validity holds — but the trainer/eval rewards the canonical format.
- Action: relax the validate rules in
eval-controller.ts(validateAssistantOutput) to only check the LEANIENT grammar contract — not the strict tab-only layout. KeepvalidateExampleFormatstrict so that examples never drift.
- D1.
src/eval/eval-gateway.ts— EvalGateway in-process mode. - D2.
src/eval/cases/*.jsonl— fixtures per scenario. - D3.
src/eval/story-creation.eval.ts— refactored to delegate to EvalController + cases. - D4. Grammar relaxation in
validateAssistantOutput; keepvalidateExampleFormatstrict. - D5. New
pnpm eval:casesandpnpm eval:cases:livescripts.
- Each example
.mdfile insrc/agents/storyteller/examples/story-*/gets a YAML frontmatter block at the top:--- think: | <one-paragraph narration about the STORY the file belongs to, what this file's role is, and what lane the model should stay in> --- # Chapter 1: ... - The
loadStorytellerExamplesloader reads frontmatter and injects thethinkcontent as the firstthinkentry for that file's example turn (so the rendered example prompt has the target think block in the right slot). - Goal: each rendered example shows the model exactly how to think about each step of a real story (not just generic "write chapter 2 deeper relationships" copy).
- E1. Update
src/agents/storyteller/examples.tsto read frontmatter. - E2. Add frontmatter to all
story-*/*.mdfiles (3 stories × ~6 files each = ~18 files). - E3. Eval still 40/40 (frontmatter doesn't break the GBNF format).
- The agent loop currently calls
model.process({systemPrompt, append: examples})every time. For long examples this is wasteful. - Add a content hash:
- Compute
SHA256(examples + systemPrompt). - Cache the processed answer/tokenization result keyed by hash.
- On cache hit: skip the
processcall, return the cached session-id.
- Compute
- Use a tiny on-disk cache under
.cache/state-tune/<hash>.bin— survives restarts.
- F1.
src/core/state-tune-cache.ts— get/set by content hash. - F2. Wire into
AgentLoopconstructor or firstruncall. - F3.
--no-cacheflag for tests that need fresh reprocessing. - F4. Test: same examples loaded twice → exactly one process call.
- Right now
NativeRwkvModel.initkeeps the model pinned in VRAM forever. - Want: regular-RAM resident base weights; vulkan/cuda context only on demand.
- Approach:
- Split
initinto two phases:loadToRam()— read safetensors into host memory once.bindToGpu()— copy buffers to VRAM and compile the pipeline (called on first generation, or background-prefetched).unbindFromGpu()— release VRAM when idle (resetIdleTimertriggers after--idle-vram-secs=NNseconds).
- The Rust binding already exposes the underlying
Instance/ context; exposebind/unbindfromlib.rsand re-wrap inNativeRwkvModel.
- Split
- G1.
lib.rs: addbindGpu(),unbindGpu(),isGpuBound()exports. - G2.
native-rwkv-model.ts: add the same methods + an idle timer in the singleton. - G3. CLI:
--idle-vram-secs=NN(default 300s = 5 min). - G4. Eval: assert that an idle period evicts VRAM and a new request rebinds (smoke test only, not blocking CI).
- F (state-tune cache) — small, isolated, has clear win.
- A (trace-writer to core) — small refactor; touches every eval path. Do early.
- B (log streaming) — needs A done first.
- D.iii (grammar strictness docs + relax validator) — quick.
- C (workspace modes) — touches a lot of code paths.
- D.i/ii (oracle-via-gateway + targeted cases) — biggest eval change.
- E (storyteller frontmatter) — content, no architecture.
- G (VRAM residency) — biggest perf change; do last to avoid rework.
pnpm typecheckpnpm eval— oracle, expect 40/40pnpm test:grammar— three grammar suitespnpm test:trace— 23/23pnpm test:agent— 11/11- new tests written alongside each chunk