Aleph
Concepts

Thinker (Prompt Engine)

Thinker is Aleph's prompt construction engine: ~28 fine-grained PromptLayers with priority + the stable_summary / live_status dual-track cache discipline, plus the prompt_contract reachability / ratchet checks.

The thinker module is Aleph's prompt construction engine. It assembles the SYSTEM prompt at every turn and maintains the runtime memory (recall_context) and transient tail (transient_recall) in parallel. After Round 9 it uses ~28 fine-grained PromptLayers instead of the old 6-layer mental model; each layer has a priority and a short contract, assembled by prompt_pipeline.

Design Philosophy

  1. Fine-grained layers — ~28 independent PromptLayers each own one concern (agent_role / environment / mcp_instructions / extra_files / guidelines / security / voice_mode / graph_topology / curated_memory / recall_context / ...). R10 ("intelligence lives in the prompt") demands each layer is independently reviewable.
  2. Priority ordering — every layer exposes a priority() integer; the pipeline renders in ascending order so cache-friendly (unchanging) content sits earlier and dynamic (timestamped / counted) content sits later.
  3. Stable / live dual track — the SYSTEM-prompt portion (stable_summary) is the cache prefix and never carries countdowns; live countdowns (deadline / next-wake) ride the transient tail message, so the cache READ 0.1× / WRITE 1.25× asymmetry is not triggered by countdown byte changes.
  4. Provider-specific rendering — the same content is rendered differently for OpenAI / Anthropic / Ollama; thinker::prompt_pipeline reads the provider name and picks the matching RenderStyle (XML / fenced / inline) to assemble.
  5. Contract guardsprompt_contract::reachable_layers guarantees every layer either appears when enabled or carries a reason onto the whitelist; scaffold_bytes_ratchet only ever shrinks the byte count (any upward move requires an explicit commit-time justification); aleph-server prompt-size reports the live byte count.

Architecture

┌─────────────────────────────────────────────┐
│                  Thinker                     │
├─────────────────────────────────────────────┤
│                                             │
│  Priority 0–800:   static identity / config / knowledge
│  Priority 800–1500: static / cache-friendly
│  Priority 1500–1720: static / slow-changing
│  Priority 1720:     RuntimeContextLayer (dynamic
│                    — token / counter / session name)
│  Priority >1720:   transient recall (transient
│                    tail only)
└─────────────────────────────────────────────┘

PromptPipeline::assemble(provider, input) renders every enabled layer in priority order into a String injected as SYSTEM prompt; the transient_recall portion takes a separate path through HarnessDeps::recall_context and never enters SYSTEM.


Core Components

PromptLayer Trait

#[async_trait]
pub trait PromptLayer: Send + Sync {
    fn name(&self) -> &'static str;
    fn priority(&self) -> i32;
    async fn render(&self, input: &ThinkerContext) -> Result<String>;
    /// Default inferred by contract.rs; when a layer shouldn't appear
    /// it returns false and goes on the "reason whitelist" path.
    fn is_reachable(&self, input: &ThinkerContext) -> bool { true }
}

Each layer owns:

  • priority(): assembly order; within the same priority the order is stable / deterministic
  • render(&ctx): pure function / IO-free (no LLM calls allowed); failure degrades to "this layer is omitted" with a warn
  • is_reachable(&ctx): default true; a layer whose config disables it or that is not on the whitelist returns false and a reason is recorded on prompt_contract::reachable_layers whitelist

Built-in Layer Inventory (after Round 9, ~28 layers)

Ascending by priority:

LayerPriorityRole
agent_role0agent identity / behavior contract (R8 source)
profile30current agent profile
soul60soul.md injection
identity_files90identity files (CLAUDE.md / soul.md / ...)
mcp_instructions120MCP integration instructions
environment150runtime env summary (OS / cwd / model)
runtime_capabilities180current provider's capabilities
runtime_context1720the only dynamic SYSTEM layer: token count / terminate reason / session name
curated_memory800stable segment of MemoryEnvelope
recall_contexttransient tailtransient_recall message (NOT in SYSTEM)
graph_topology750governance graph render (governed sessions only)
voice_mode1710voice-mode prompt (voice turns only)
multi_step_conduct300multi-step behavior rules
operational_guidelines350operational guardrails
guidelines400general rules
security450redlines / safety policies
mcp_protocol200MCP protocol instructions
provider_guidance500provider-specific render hints
skill_instructions550skill SKILL.md injection
tool_runtime_state600tool runtime state
protocol_tokens650protocol tokens
citation_standards700citation standards
standing_goal720current standing goal (if any)
memory_protocol730memory protocol instructions
plan_prompt880current welded Strategy render
extra_files920current session's extra files
execution_plan950current scratchpad execution list
doctor_repair_hint1100/doctor repair hint (conditional)
session_context_guide780session-level guidance

RuntimeContextLayer is the only layer in SYSTEM carrying per-turn digits (current_total_tokens / terminate_reason / session_key) — every turn changes them, so it must be the last layer in SYSTEM to minimize cache-rewriting cost; all other 27 layers are cache-friendly content and sit strictly before 1720.

ThinkerContext

pub struct ThinkerContext {
    pub session_key: SessionKey,
    pub agent_id: AgentId,
    pub voice: Option<VoiceContext>,         // voice turn context
    pub running: RunningStats,              // runtime stats (turns / tokens / time)
    pub profile: Option<ProfileView>,
    pub skills: Vec<SkillView>,
    pub goals: Vec<GoalView>,
    pub strategies: Vec<StrategyView>,
    pub diagnostics: Vec<DiagnosticView>,   // from doctor
    pub memory: Option<MemoryEnvelope>,     // already-recalled envelope
    pub session_recent: Vec<ChatMessageView>, // current session recent
    pub plan: Option<PlanView>,             // current welded Strategy render
    pub vault: Option<VaultView>,           // vault secrets (encrypted)
    pub mcp: Option<McpView>,               // current session's active MCP servers
    pub todo: Option<TodoView>,             // current execution list
    pub runtime: RuntimeView,                // OS / cwd / model / counters
    pub scratchpad: ScratchpadView,         // execution list + objective
}

render(&ctx) reads this context, renders what it cares about, and never does IO. VoiceContext is injected by voice_mode::get(session_key); MemoryEnvelope is assembled by MemoryContextProvider::build_envelope; PlanView is pulled by StrategyLayer; TodoView is pulled by ScratchpadLayer.

PromptPipeline Assembly

PromptPipeline::assemble(provider, input) iterates layers in priority-ascending order, calls each layer.render(&input), and joins the result with newlines into a single string. Failed layers log a warn but don't abort (fail-soft); prompt_contract validates that every enabled layer either appears or is on the "reason whitelist".

pub struct PromptPipeline {
    layers: Vec<Arc<dyn PromptLayer>>,
}

Construction lives in bin/aleph-server/commands/start/builder/constructor/mod.rs::build_thinker_pipeline:

PromptPipeline::new(vec![
    Box::new(AgentRoleLayer),         // priority 0
    Box::new(ProfileLayer),           // priority 30
    Box::new(SoulLayer),              // priority 60
    Box::new(IdentityFilesLayer),     // priority 90
    Box::new(McpInstructionsLayer),   // priority 120
    Box::new(EnvironmentLayer),       // priority 150
    Box::new(McpProtocolLayer),       // priority 200
    Box::new(RuntimeCapabilitiesLayer), // priority 180
    Box::new(MultiStepConductLayer),  // priority 300
    Box::new(OperationalGuidelinesLayer), // priority 350
    Box::new(GuidelinesLayer),        // priority 400
    Box::new(SecurityLayer),          // priority 450
    Box::new(ProviderGuidanceLayer),  // priority 500
    Box::new(SkillInstructionsLayer),  // priority 550
    Box::new(ToolRuntimeStateLayer),  // priority 600
    Box::new(ProtocolTokensLayer),    // priority 650
    Box::new(CitationStandardsLayer), // priority 700
    Box::new(StandingGoalLayer),      // priority 720
    Box::new(MemoryProtocolLayer),    // priority 730
    Box::new(GraphTopologyLayer),     // priority 750
    Box::new(CuratedMemoryLayer),     // priority 800
    Box::new(PlanLayer),              // priority 880
    Box::new(ExtraFilesLayer),        // priority 920
    Box::new(ExecutionPlanLayer),     // priority 950
    Box::new(DoctorRepairHintLayer),  // priority 1100
    Box::new(VoiceModeLayer),         // priority 1710
    Box::new(RuntimeContextLayer),    // priority 1720
])

Priority 1720 is last on purposeRuntimeContextLayer is the only dynamic SYSTEM layer (per-turn digits), every other 27 layer is cache-friendly content and sits strictly before 1720. Anthropic's prompt cache is prefix-keyed, so a single byte change forces the entire conversation prefix to be re-WRITTEN at 1.25× cost instead of READ at 0.1×.


The Two Rulers (src/thinker/prompt_contract.rs)

R10's "intelligence in the prompt, not in the loop" gets two explicit metric rulers:

pub const fn reachable_layers(pipeline: &PromptPipeline) -> Vec<(&str, bool, Option<String>)>;
pub fn scaffold_bytes_ratchet(input: &ThinkerContext) -> Result<usize, ContractError>;
  • reachable_layers — list every enabled layer; a layer with is_reachable == false must carry a reason onto the whitelist (whitelist_disabled(name, reason)), otherwise the test fails.
  • scaffold_bytes_ratchet — compute the current ThinkerContext's SYSTEM-prompt scaffold-only byte count (excluding RuntimeContext / recall_context / transient_recall), compare to the last recorded value, only allow shrinking; any increase must be explicitly whitelisted.
  • prompt-size CLI subcommand (aleph-server prompt-size): reports the current scaffold byte count + per-layer byte distribution + remaining headroom to the R10 redline CEILING.

prompt-size is also used by the loop / goal continuation prompt path (continuation calls build_system_prompt which runs the same contract); the R10 redline CEILING is one shared byte budget for both harness lines and SYSTEM prompt.


prompt_pipeline (introduced in Round 9)

PromptPipeline replaces the old Thinker::build_prompt, unifying the ~28 layers:

pub struct PromptPipeline {
    layers: Vec<Box<dyn PromptLayer>>,
}

impl PromptPipeline {
    pub fn new(layers: Vec<Box<dyn PromptLayer>>) -> Self;
    pub fn assemble(&self, input: &ThinkerContext) -> Result<String>;
    pub fn with_layers(self, layers: Vec<Box<dyn PromptLayer>>) -> Self;
}

ProviderRenderStyle (xml / fenced / inline) decides per-layer wrapping by provider — PromptPipeline::render_section(layer, ctx, style) is the single chokepoint.


Streaming State Machine

thinker::streaming provides the per-token state machine BlockState:

pub struct BlockState {
    pub buffer: String,
    pub current_block: Option<BlockType>,
}

impl BlockState {
    pub fn feed(&mut self, token: &str) -> Vec<BlockEvent> { /* ... */ }
}

BlockType has five variants: Text / ToolCall / Code / Thinking / Image. BlockState::feed parses an incoming token and emits BlockEvents (new block / current block continues / current block ends). The Panel-side renders the two-stage caption with this state machine (see interfaces/webchat/src/views/chat/state/streaming.rs).


Provider-Specific Rendering

Different providers parse the same content differently — Anthropic wants XML, OpenAI wants fenced code, Ollama wants plain text. PromptLayer::render returns plain text (without <example> / ``` wrapping); the wrapping is decided by PromptPipeline::render_section per ProviderRenderStyle:

pub enum ProviderRenderStyle { Xml, Fenced, Inline }
  • Anthropic / XML: each layer is wrapped in <section name="<layer_name>">…</section>; inner content is not re-escaped
  • OpenAI / Fenced: each layer is wrapped in ```<layer_name>\n…\n```; ``` inside content must be re-escaped
  • Ollama / Inline: each layer is plain text with a single-line --- <layer_name> --- heading

style_for_provider(provider_name) is called once at pipeline assembly; the result is cached on the pipeline struct.


Key Source Files

  • src/thinker/mod.rs — module entry, re-exports PromptPipeline / ThinkerContext / PromptLayer
  • src/thinker/prompt_pipeline.rsPromptPipeline and layer assembly
  • src/thinker/prompt_contract.rsreachable_layers / scaffold_bytes_ratchet / whitelist_disabled
  • src/thinker/prompt_layer.rsPromptLayer trait + Box<dyn PromptLayer> wrapping
  • src/thinker/render_style.rsProviderRenderStyle three variants
  • src/thinker/streaming.rsBlockState streaming state machine
  • src/thinker/soul.rs — soul.md parsing
  • src/thinker/identity_files.rs — identity files injection
  • src/thinker/context.rsThinkerContext field definitions
  • src/thinker/runtime_context.rsRuntimeContextLayer (priority 1720, the only dynamic SYSTEM layer)
  • src/thinker/nudges.rs — 9 model-facing prompt strings (MAX_STEPS_HINT / MAX_OUTPUT_TOKENS_RESUME_NUDGE / INTERRUPTION_NOTE etc.), centralized after the harness purged them
  • src/thinker/security_context.rsSecurityLayer
  • src/thinker/curated_memory.rsCuratedMemoryLayer (assembles the stable segment of MemoryEnvelope)
  • src/thinker/recall_context.rsRecallContext and transient-tail assembly
  • src/thinker/project_instructions.rs — various layer helpers
  • src/thinker/xml_util.rs — XML escaping / wrapping
  • src/thinker/interaction.rsInteractionManifest channel-capability awareness
  • src/thinker/prompt_budget.rsPromptBudget (cap layer count per provider)
  • src/thinker/layers/{mod,voice_mode,graph_topology,curated_memory,doctor_repair_hint,multi_step_conduct,operational_guidelines,security,runtime_capabilities,protocol_tokens,citation_standards,plan,standing_goal,plan_prompt,execution_plan,extra_files,profile,provider_guidance,guidelines,environment,tool_runtime_state,security_session,supervisor_verification,tool_layer}.rs — 20+ concrete layers

26.7.x Addendum

Round 9 Layer Reorganization

26.7.x split the old 6-layer mental model (Identity / Skills / Memory / Tools / Rules / Conversation) into ~28 fine-grained PromptLayers, each independently reviewable, individually disable-able, and whitelist-able. R10 ("intelligence in the prompt, not in the loop") requires every layer to have its own contract and priority.

Two Rulers (R10 Redline)

prompt_contract::reachable_layers + scaffold_bytes_ratchet turn the R10 redline from "hand-calculated" into code-measurable. aleph-server prompt-size reports live bytes + per-layer distribution, putting R10 within reach. The CEILING ratchet is shared between harness and thinker: the SYSTEM-prompt byte ceiling and the_harness_line_budget_does_not_grow's CEILING are the same ruler (R10's single byte budget for two subsets).

Voice Mode Layer

26.7.x VoiceModeLayer (priority 1710) injects voice-turn prompt rules — including "fix ASR-transcribed homophones" and "preserve spoken filler as user pragmatic signal". The VoiceContext field is injected by voice_mode::get() (active-only; the transcribed flag is set by inbound_router).

transient_recall & Cache Friendliness

26.7.1+: dual-track design — stable MemoryEnvelope goes into the SYSTEM prompt (cache-friendly), live countdowns (deadline / next-wake) go into the transient tail message (past the cache breakpoint, byte changes only cost themselves). Mirrors looping::LoopState's stable_summary vs live_status — the same design, thinker and looping share the R10 cache discipline.

Prompt Cache Discipline

RuntimeContextLayer is deliberately last (priority 1720) — the only SYSTEM-prompt layer that carries per-turn digits (token count / terminate reason / session_key). All other ~27 layers are cache-friendly content (identity / profile / soul / environment / security / plan etc.) and sit strictly before 1720. Anthropic's prompt cache is prefix-keyed, so a single byte change forces the entire conversation prefix to be re-WRITTEN at 1.25× cost instead of READ at 0.1× — a single countdown byte change would wipe the cache benefit for the whole conversation.

See Also

On this page