Skip to main content

When to Add a Module

The runtime has 15 modules. Adding a 16th is a significant decision — do it when a new function doesn’t fit any existing region and is important enough to be a permanent part of the pipeline.
Before adding a module, consider whether the function belongs in an existing module. If it’s a new tool, it probably belongs in cerebellum as a capability. If it’s a new safety check, it belongs in amygdala. New modules are for genuinely new cognitive functions.

Step 1: Create the Folder

Follow the one-thing-per-folder convention:
src/main/runtime/my-module/my-module.ts

Step 2: Define the Class

Every module follows the same constructor pattern:
import { Corpus } from '@main/runtime/corpus/corpus'

export interface MyModuleOpts {
  workspaceRoot: string
  corpus: Corpus
}

export class MyModule {
  private readonly workspaceRoot: string
  private readonly corpus: Corpus

  constructor(opts: MyModuleOpts) {
    this.workspaceRoot = opts.workspaceRoot
    this.corpus = opts.corpus

    // Subscribe to events this module cares about
    this.corpus.on('message.received', (payload) => this.handleMessage(payload))
  }

  async doSomething(input: string): Promise<MyResult> {
    // Module logic here

    // Emit events for other modules to react to
    this.corpus.emit('my-module.completed', { input, result })
    return result
  }

  private handleMessage(payload: MessagePayload): void {
    // React to events from other modules
  }
}

Step 3: Define Corpus Events

Add your module’s events to the CorpusEvent enum in corpus.ts:
// src/main/runtime/corpus/corpus.ts

export type CorpusEvents = {
  // ... existing events ...

  // My module events
  'my-module.started': { input: string }
  'my-module.completed': { input: string; result: MyResult }
  'my-module.failed': { input: string; error: string }
}
Name events as module-name.past-tense-verb. This makes the event log read like a story: “message received, context built, safety checked, response streamed.”

Step 4: Wire into Agent

Import and instantiate your module in agent.ts:
// src/main/runtime/agent.ts

import { MyModule } from '@main/runtime/my-module/my-module'

export class Agent {
  private readonly myModule: MyModule

  constructor(opts: AgentOpts) {
    // ... existing modules ...

    this.myModule = new MyModule({
      workspaceRoot: opts.workspaceRoot,
      corpus: this.corpus,
    })
  }

  async processMessage(message: string): Promise<void> {
    // The pipeline — call your module at the right stage
    const context = await this.prefrontal.buildContext(message)
    const filtered = await this.ras.filter(context)

    // Example: call your module after context is built
    await this.myModule.doSomething(filtered)

    const response = await this.thalamus.stream(filtered)
    // ... rest of pipeline
  }
}
Where in the pipeline your module runs depends on what it does:
StageWhen to insert
Before prefrontalInput preprocessing, routing
After prefrontal, before thalamusContext enrichment, filtering
After thalamus, before brocaOutput processing, validation
After brocaPost-response actions, logging

Step 5: Add Workspace Storage (if needed)

If your module needs persistent state, create a folder in the defaults:
src/defaults/workspace/brain/my-module/
Read and write markdown files for state:
import { readFile, writeFile } from 'fs/promises'
import { join } from 'path'

async loadState(): Promise<MyState> {
  const path = join(this.workspaceRoot, 'brain', 'my-module', 'state.md')
  const content = await readFile(path, 'utf-8')
  return this.parseState(content)
}

async saveState(state: MyState): Promise<void> {
  const path = join(this.workspaceRoot, 'brain', 'my-module', 'state.md')
  await writeFile(path, this.serializeState(state), 'utf-8')
}

Design Rules

All inter-module communication goes through corpus events. This keeps the dependency graph flat and makes modules independently testable.
// WRONG — direct import creates coupling
import { Hippocampus } from '@main/runtime/hippocampus/hippocampus'

// RIGHT — communicate via events
this.corpus.emit('memory.store-requested', { content })
this.corpus.on('memory.stored', (payload) => { /* ... */ })
Each module does exactly one thing. If you’re adding two unrelated functions, that’s two modules (or one of them belongs in an existing module).
Human-readable, git-versionable, LLM-parseable. Never use binary formats or JSON for workspace state. Markdown with YAML frontmatter is the standard.
Your module should be testable by mocking only the corpus. If you need to mock five other things, your module has too many dependencies.
If your module needs configuration, read it from config.json via the workspace helper. Don’t invent a new config mechanism.
import { readConfig } from '@main/workspace/config'

const config = await readConfig(this.workspaceRoot)
const myModuleConfig = config.myModule ?? defaults

Testing

Unit test your module in isolation by mocking the corpus:
import { describe, it, expect, vi } from 'vitest'
import { MyModule } from './my-module'

describe('MyModule', () => {
  function createMockCorpus() {
    const handlers = new Map<string, Function>()
    return {
      on: vi.fn((event, handler) => handlers.set(event, handler)),
      emit: vi.fn(),
      // Helper to simulate incoming events
      simulate: (event: string, payload: unknown) => handlers.get(event)?.(payload),
    }
  }

  it('emits completed event after processing', async () => {
    const corpus = createMockCorpus()
    const mod = new MyModule({
      workspaceRoot: '/tmp/test-workspace',
      corpus: corpus as any,
    })

    await mod.doSomething('test input')

    expect(corpus.emit).toHaveBeenCalledWith(
      'my-module.completed',
      expect.objectContaining({ input: 'test input' })
    )
  })

  it('reacts to incoming events', () => {
    const corpus = createMockCorpus()
    new MyModule({ workspaceRoot: '/tmp/test-workspace', corpus: corpus as any })

    corpus.simulate('message.received', { content: 'hello' })

    // Assert side effects
  })
})

Architecture Overview

Understand how all 15 modules fit together.

Project Structure

Where everything lives in the codebase.