Recursive Language Model (RLM) plugin for deepseek-harness (dsh).
A port of the pi-agent RLM plugin (rlm.pi/pi-plugin/rlm) onto dsh-native infrastructure —
Cordis lifecycle, worker-thread code execution, stream-only LLM access. Zero duplicated logic:
the REPL is dsh-code-runtime, the model port is dsh-llm, persistence is the dsh session log.
Status: Phases 0–4 + headless e2e gate complete. 236/236 offline vitest specs green,
strict tsc clean, real-CLI e2e green via e2e/run.sh. See PLAN.md for the
port plan, verification log, and phase gates.
An RLM run keeps the long context OUT of the model's history (paper invariant #1): the model
interacts with it by writing TypeScript in ```repl fenced blocks. Programs run in a
worker thread; host-side bindings give them:
rlm.llm_query / llm_batch / rlm_query / rlm_batch— async-by-default sub-LLM fan-out (spawn →task_id→await_task), deduplicated by the session TaskLedger (exact-hash coalesce, ancestor-echo reject, Jaccard near-dup coalesce).rlm.finish({ summary })— the ONLY final-answer channel.env.set/get— durable JSON variables across blocks (host-side, decision D2).ctx.search / grep_context / get / outline / list / add_context— free local work over the context bundle.memory.*— durable cross-run note store (BM25 recall, leaf-model evolution).
The engine loop enforces the paper invariants: metadata-only history, token-budget cascade with
deterministic continuation handoffs (distillTrajectory), compaction/elision, LimitGuard caps,
and depth-capped recursion that degrades rlm_query → llm_query near the cap.
The loader imports absolute-path name specifiers directly — no publish step:
# $DSH_HOME/profiles/<name>/cordis.patch.yml
- insert:
# Required ONLY if your bundle stack has no code-runtime provider (dsh-base
# ships none; the headless bundle already inserts this exact entry).
# `pending (waiting for service: codeRuntime)` at boot means you need it.
- id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker-thread'
- id: rlm
name: /absolute/path/to/dsh-rlm/src/index.ts
config:
provider: deepseek-official # LLM route (see Config below)
model: deepseek-chatRequires Node ≥ 23.6 (the CLI loads the plugin's TypeScript source via type stripping —
which is also why the codebase forbids parameter properties). Bare @deepseek-ai/* imports
resolve through the profiles flat fallback ($DSH_HOME/profiles/node_modules); for local
development symlink them into this repo's node_modules.
Instead of the process-wide profile patch, author a preset: create
$DSH_HOME/.agent-presets/rlm/agent.cordis.yml as a copy of the shipped standard
composition with the rlm row appended (plain row — the plugin registers into the host
tools registry and provides no service, so no isolate realm is needed), plus a
preset.yml (name/description/order). The code-runtime host entry above is still
required. Keep the row OUT of the profile patch in this mode — registering the tool twice
(host + preset) is untested and may collide.
All fields optional; validated by Schemastery at load.
| Field | Default | Meaning |
|---|---|---|
provider |
"deepseek-official" |
Provider route for root and sub-calls |
model |
"deepseek-chat" |
Root model id |
leafModel |
model |
Leaf model for llm_query/llm_batch |
maxDepth |
4 |
Recursion cap; rlm_query degrades to llm_query at the cap |
maxIterations |
30 |
Root loop iteration cap |
execTimeoutS |
120 |
Per-block REPL wall-clock budget, seconds |
maxConcurrentSubcalls |
16 |
Process-wide leaf-call concurrency |
maxConcurrentChildren |
6 |
Per-depth child-run concurrency |
enableTokenBudget |
true |
Token-budget cascade + continuation handoffs |
contextLoader |
true |
Mount the context pipeline (ctx.* bindings) |
autoSeedCwd |
true |
Seed REPL context from the tool call's cwd |
enableMemory |
true |
Durable notes under memoryDir |
memoryDir |
run cwd | Memory directory |
injectNoteTokens |
2000 |
Token budget for the injected memory note |
evolveEvery |
8 |
Evolve the memory note every N completed runs |
Injected services: llm, tools, codeRuntime (Cordis enforces declaration at runtime).
src/client/index.ts is a Cordis client half (inject: ["slots"], named apply) registering
a keyed tool.call.toolview component that renders the run card. The tool's renderText
emits a machine-parsable summary line — RLM · 3 iter · 12,345→8,001 tok · 4.2s · $0.0012 —
from src/ui/summary.ts (the single spec: one format function, one parse function, a
round-trip test locking the pair). Generic clients and the card consume the same render.
./e2e/run.shBoots the real dsh CLI offline against a scripted LlmAdapter (e2e/stub-llm.mjs), mounts
this repo's source into a fresh profile, runs one agent turn, and asserts the zstd session
JSONL carries tool/result with isError:false, the one-iteration summary line, and the
engine's answer. SIGINT mid-run exits cleanly with no leaked workers.
src/
engine/ loop, budget cascade, ledger, limits, compaction, answer/history
fanout/ task registry, spawn/await handlers, leaf completions, admission gates
runtime/ ReplSession + binding bridge (rlm/env/ctx/memory namespaces), env store
context/ context merge/narrowing + local query tools (search/grep/get/outline)
llm/ completeStream — the ONE stream→completion fold (dsh LlmRuntime port)
memory/ durable-note store, BM25, hashing, replay
tool/ defineTool seam: schema, output.render, execute → engine
ui/ summary-line spec + pure block→card-model projection
client/ browser half: keyed toolview component + slots registration
prompts/ system contract + per-turn user prompts
text/ fence grammar, previews, truncation (wire contract — do not drift)
tests/ 29 spec files, 236 tests — fully offline
e2e/ profile template, scripted adapter, run.sh gate
pnpm install
pnpm typecheck # strict tsc (strict, noUncheckedIndexedAccess, verbatimModuleSyntax)
pnpm test # vitest — offline, deterministicRules honored: strict TS (no any, no !, no parameter properties), Result<T,E> at
boundaries, frozen configs, pre-sized collections, .join('') assembly, files under 1k lines
(largest: 398), DRY — one task registry, one stream fold, one claim path, one summary spec.
- rlm_test worker-level scenario replay (parked)
- Live mid-run fan-out tree in the client — blocked on a mid-call progress channel for plain tools (upstream harness work, not a local gap)
- Locale seat for the toolview; css-module styling (currently inline frozen styles)