Building Memory Extensions

August 30, 2026 · View on GitHub

简体中文 | English | Documentation Center

This page is for plugin authors adding memory semantics, data planes, or scheduling policies to dsh-mnemon. Users still install only dsh-mnemon. Source workspaces define responsibilities; the compatibility boundary is the root package's public subpath exports.

Choose an extension point first

Extension pointOwnsMust not own
LayerOne memory semantic and its executor, such as episodic or procedural memoryGlobal authority or mutation of another Layer
AdapterIdentity, locality, scope, and capabilities of an external data planeA path around Layer and Kernel validation
StrategyBounded step proposals from a request and read-only descriptorsDatabases, network clients, or secrets
GuardDenial before Strategy planning; it may only narrow authorityPermission broader than the Host, configuration, or another Guard allows
MemorySourceQuery-independent snapshot of one Layer's revision, Wake projection, and Host-only recall stateRecall queries, mutations, Strategy selection, node trees, or another Layer's authority
SurfaceTranslation from DSH tools, commands, RPC, or UI into operation requestsA second routing or authorization system

Use these public entry points. Do not import from dsh-mnemon/src/* or repository-internal packages/* paths.

SubpathPurpose
dsh-mnemon/contractsPure JSON/wire types
dsh-mnemon/kernelCatalog, Topology, Kernel, Plan, Receipt, and Guards
dsh-mnemon/extension-sdkExtension definition, registration, and Cordis lifecycle integration
dsh-mnemon/strategy-sdkStrategy manifests, permission wrapping, and replay
dsh-mnemon/provider-sdkGeneric Adapter Factory Registry and current Memory Space Provider interfaces
dsh-mnemon/layers/*The three built-in Layer descriptors
dsh-mnemon/strategy-default-three-tierDefault topology and compatibility Strategy

Minimal Layer extension

This Layer illustrates the execution boundary. A real implementation must bound time, cancellation, output, and secret exposure inside its executor and return JSON values only.

import { defineMemoryExtension } from 'dsh-mnemon/extension-sdk'

export const episodicExtension = defineMemoryExtension({
  descriptor: {
    id: 'example-episodic',
    version: '1.0.0',
    label: 'Example Episodic Memory',
    description: 'A bounded event-memory Layer.',
  },
  layers: [{
    descriptor: {
      id: 'episodic',
      label: 'Episodic Memory',
      description: 'Recalls bounded event evidence.',
      role: 'episodic-memory',
      order: 400,
      capabilities: ['recall', 'write', 'project'],
    },
    async execute(step, context) {
      if (context.signal?.aborted) throw new Error('operation cancelled')
      return { layer: step.layerId, capability: step.capability, items: [] }
    },
  }],
  sources: [{
    layerId: 'episodic',
    mode: 'routed',
    snapshot: () => ({
      revision: 'episodes-v1',
      wake: 'Recent episodic evidence is available.',
      state: { stream: 'recent' },
    }),
  }],
})

When the extension first appears, the live Catalog advances its generation. Topology follows with a new generation and adds the Layer as a disabled candidate. Settings cards come from the memory-system descriptor, so a new ID needs no frontend enum change. An ordinary user decides only whether to enable it:

mnemon:
  memoryTopology:
    layers:
      episodic:
        enabled: true

Once enabled, the Host's fixed compatibility policy and the extension declaration determine executable capabilities. Fine-grained participation remains a Kernel/SDK control-plane constraint, not an ordinary setting. Disabling or unloading an extension never deletes its data. Unloading stops new operations from selecting the component and invalidates existing Plans through Catalog/Topology generation changes.

Expose a Layer as a MemorySource

A Layer declaring project needs a matching extension.sources contribution before its projection channel can be automatic. snapshot() receives only pinned Catalog, Topology, and Guard generations plus the operation scope. It returns one stable revision, one wake string, and optional JSON-safe state. The state is digest-bound Host authority and never enters the System Prompt.

Choose only between two modes:

  • eager injects wake exactly. Reserve it for small context needed on virtually every model step, such as Runtime Memory.
  • routed treats wake as one compact cover. It is whitespace-normalized, limited to 500 characters, JSON-quoted as untrusted routing data, and competes within a 4 KiB routed-cover budget. A cover omitted by that budget remains fully authorized through Host-only state.

The total Wake is limited to 64 KiB. There is no node tree, Zoom operation, or model-facing View ID. Recall takes only a query and optional Memory Space IDs; the Host derives the executing Agent turn's pinned Source state, validates the requested subset, and queries providers directly. Children retain the parent's dispatch-time View and runtime, then pin their own turns; extensions must not use the parent's latest View as execution authority. Runtime-graph construction fails closed when an enabled automatic Layer has no MemorySource. Live registration and unloading apply the same readiness check transactionally across attached graphs. Co-locating a Layer and its Source gives the simplest atomic lifecycle.

Let Cordis own the lifecycle

A normal DSH extension should depend on the Host's mnemonMemory service so registration follows the plugin fiber:

import type { Context } from '@deepseek-ai/cordis'
import type { MemoryBoot } from 'dsh-mnemon/extension-sdk'
import { episodicExtension } from './episodic.js'

declare module '@deepseek-ai/cordis' {
  interface Context {
    mnemonMemory: MemoryBoot
  }
}

export const inject = ['mnemonMemory']

export function apply(ctx: Context): void {
  ctx.effect(
    () => ctx.mnemonMemory.register(episodicExtension),
    'example-episodic.register()',
  )
}

An aggregate bundle that is guaranteed to execute before dsh-mnemon mounts may instead call registerMemoryExtension() and retain its disposer. Cordis isolate() supplies scope and ownership, not a security sandbox. Extension code still runs in the Host process and must come from a trusted package.

Strategy plugins and replay

A Strategy receives only the request and read-only Catalog/Topology snapshots and returns proposed steps. defineMemoryStrategyPlugin() further limits its Layers, Adapters, Capabilities, and maximum step count. The Kernel then revalidates topology, participation, bindings, budgets, and Guards.

import {
  MEMORY_STRATEGY_PLUGIN_API_VERSION,
  defineMemoryStrategy,
  defineMemoryStrategyPlugin,
} from 'dsh-mnemon/strategy-sdk'

export const focusedRecall = defineMemoryStrategyPlugin({
  manifest: {
    apiVersion: MEMORY_STRATEGY_PLUGIN_API_VERSION,
    kind: 'MemoryStrategyPlugin',
    metadata: {
      id: 'focused-recall',
      version: '1.0.0',
      label: 'Focused Recall',
      description: 'Routes recall only to Memory Spaces.',
    },
    permissions: {
      layerIds: ['memory-spaces'],
      adapterIds: [],
      capabilities: ['recall'],
      maxSteps: 1,
    },
  },
  strategy: defineMemoryStrategy({
    descriptor: {
      id: 'focused-recall',
      version: '1.0.0',
      label: 'Focused Recall',
      description: 'Routes recall only to Memory Spaces.',
      hooks: ['retrieval-planning'],
      deterministic: true,
    },
    propose(request) {
      return {
        strategyId: 'focused-recall',
        strategyVersion: '1.0.0',
        reason: 'Use the durable evidence layer.',
        steps: [{ layerId: 'memory-spaces', capability: request.capability }],
      }
    },
  }),
})

Place focusedRecall.strategy in an extension's strategies before selecting focused-recall as memoryTopology.strategyId. A zero-step, over-permission, or over-budget proposal fails closed. Use replayMemoryStrategy() with fixed requests and descriptors. Cover normal routing, disabled Layers, manual/automatic boundaries, missing Adapters, budget ceilings, and adversarial escape proposals.

Adapter versus Memory Space Provider

extension.adapters registers arbitrary Adapter descriptors and lets Topology bind an adapterId to a Layer. In v1alpha1, a generic Adapter contribution is a control-plane description; the corresponding Layer executor owns actual data-plane calls.

dsh-mnemon/provider-sdk also exposes MemoryAdapterFactoryRegistry and decouples construction of the nine built-in Memory Space Providers from MnemonService. The Memory Space settings cards, connection fields, credential redaction, and persistent registry are still defined by the built-in Provider Catalog. Registering a factory alone does not create a new Provider UI. A third party can integrate a new engine today as its own Layer/Adapter; fully dynamic Provider descriptor and configuration-schema registration is the next compatibility surface.

Safe pipeline for model-generated Strategies

This release supplies a generatable and verifiable artifact boundary, but it does not execute arbitrary code immediately after a model writes it. The recommended pipeline is:

generate source + manifest
  -> format/schema/type checks
  -> permission-manifest wrapping
  -> golden replay + escape tests
  -> shadow comparison with the active Strategy
  -> human approval or signed artifact
  -> small canary
  -> promote / automatic rollback

Evaluation should use redacted, reproducible operation descriptors and expected Plans. Never give Provider credentials or raw private memories to the generating model. Receipts need Strategy ID/version, Catalog/Topology/Guard generations, and failure status so iterations remain comparable and reversible.

Compatibility checklist

  • Use lowercase kebab-case IDs and never reuse a published ID for different semantics.
  • Keep descriptors and cross-package inputs JSON-safe; do not pass classes, closures, clients, databases, or secrets.
  • Pin one generation per operation. Re-plan after a stale-plan error; never replay old steps.
  • manual does not mean “the model explicitly called a tool.” Model, lifecycle, and system automation are automatic triggers.
  • Guards return allow/deny without data-plane side effects. A Guard-set change invalidates existing Plans.
  • Executors must honor AbortSignal and let the Kernel represent partial failure as a partial Receipt.
  • A Layer with automatic project participation must have a MemorySource; test publication, Wake budgets, Host-only state, and rejected/rolled-back live unload.
  • Test registration, live unload, duplicate IDs, over-permission Strategies, cancellation, and Receipt states.