Skip to main content
Conversations & Sessions

Memory

Let agents carry knowledge across turns and sessions with native memory, or take over generation and consumption yourself.

Memory lets an agent keep the knowledge it gains in one session and reuse it later. A project's build command, test layout, and code conventions no longer have to be rediscovered on every run. Qoder Agent SDK memory has two halves — generation and consumption — which can be configured independently:
HalfWhat it doesWhen it runs
GenerationWrites what the agent learned into memory filesAfter a turn completes, in a background memory agent
ConsumptionLoads memory files into the agent's contextAt session initialization, and on explicit refresh
Language availability: TypeScript only.The memory option and the flushMemory() / refreshMemory() runtime methods exist in the TypeScript SDK. The Python SDK has no equivalent option, so a Python application cannot configure SDK memory, choose scopes, or receive generation and consumption results. Everything on this page applies to TypeScript.

Enable native memory

mode: 'native' delegates all behavior to Qoder CLI defaults. This is the recommended starting point: the runtime decides what is worth remembering, where to store it, and when to load it.
import { query } from '@qoder-ai/qoder-agent-sdk';

for await (const message of query({
  prompt: 'Add a health check endpoint and run the tests',
  options: {
    memory: { mode: 'native' },
  },
})) {
  console.log(message);
}
In native mode the only configurable parts are the two scope switches and the result callbacks. Everything else — the storage location, the generation prompt, the loading budget — stays owned by the runtime and improves with each release.

Choose memory scopes

Native memory has two scopes. Both are enabled by default.
ScopeRoot idHolds
UseruserKnowledge that follows the operator across every project
ProjectprojectKnowledge specific to the current working directory
Disable either one per query:
// Project knowledge only — do not read or write user-level memory
options: {
  memory: { mode: 'native', userScope: false },
}
Native mode requires at least one enabled scope. Disabling both throws:
native mode requires at least one enabled scope; use memory: {} to disable SDK memory

Disable memory

An empty object is the explicit "off" switch, equivalent to omitting memory entirely. No memory configuration is sent to the runtime.
options: {
  memory: {},
}

Override generation and consumption

mode: 'custom' applies your overrides and inherits everything you omit from the runtime defaults. Use it when the application owns where memory lives or what counts as worth remembering.

Control what gets written

options: {
  memory: {
    mode: 'custom',
    generation: {
      roots: [
        { id: 'team', path: '/srv/knowledge/team', access: 'read' },
        { id: 'service', path: '/srv/knowledge/checkout', indexFile: 'INDEX.md' },
      ],
      prompt: 'Record only deployment steps and failure signatures.',
    },
  },
}
Each root is a directory the memory agent may use:
FieldTypeDefaultDescription
idstringStable identifier used in prompts, initialization, and results
pathstringDirectory on the machine running Qoder CLI
access'read' | 'read-write''read-write'Whether the memory agent may write to this root
indexFilestringRelative index path inside the root. Omit when every file is content

Decide per turn whether to write

Generation normally starts on every completed turn. shouldGenerate lets the application veto an attempt — useful to skip trivial turns, respect a per-tenant budget, or avoid writing during a read-only review.
options: {
  memory: {
    mode: 'custom',
    generation: {
      turnComplete: {
        shouldGenerate: async (input, { signal }) => {
          if (input.prompt.length < 40) {
            return { run: false, reason: 'prompt too short to be worth remembering' };
          }
          return { run: true };
        },
        timeoutMs: 5_000,
        onGateError: 'skip',
      },
    },
  },
}
The callback receives the completed turn's prompt and response, plus sessionId, cwd, and a one-based turnIndex. Returning run: false emits a skipped result instead of starting the background agent.
FieldTypeDefaultDescription
enabledbooleantrueWhether turn-complete generation runs at all
shouldGeneratecallbackProduct-side gate. Omit to use runtime guards only
timeoutMsnumber10000Maximum time allowed for the gate
onGateError'skip' | 'report_failed''skip'How a callback error or timeout is reported

Control what gets loaded

Passing explicit files replaces native auto-memory for this query. Static instructions still load.
options: {
  memory: {
    mode: 'custom',
    consumption: {
      files: [
        { id: 'conventions', path: '/srv/knowledge/CONVENTIONS.md', required: true },
        { id: 'runbook', path: '/srv/knowledge/RUNBOOK.md' },
      ],
      maxTokens: 4_000,
      overflow: 'truncate',
      failureMode: 'fail_query',
    },
  },
}
Files are injected in array order, and id becomes the injected section name.
FieldTypeDefaultDescription
enabledbooleantrueWhether memory is loaded at all
filesMemoryConsumptionFile[]Explicit files. Replaces native auto-memory
maxTokensnumberShared budget across all explicit files
overflow'truncate' | 'fail_query''truncate'Behavior when content exceeds maxTokens
failureMode'best_effort' | 'fail_query''best_effort'Read-failure behavior. Only required files can fail the query

Observe what memory did

Memory runs in the background, so surface its outcome rather than assuming success. Two paths are available. Register callbacks, which work in both native and custom mode:
options: {
  memory: {
    mode: 'native',
    generation: {
      onResult: (result) => {
        console.log(`[memory] ${result.status} in ${result.durationMs}ms`);
        for (const file of result.writtenFiles) {
          console.log(`  wrote ${file.rootId}:${file.path}`);
        }
      },
    },
    consumption: {
      onResult: (result) => {
        for (const file of result.files) {
          console.log(`  loaded ${file.id}: ${file.status}`);
        }
      },
    },
  },
}
Or read the same payloads off the message stream, as system messages with subtype memory_generation and memory_consumption:
for await (const message of q) {
  if (message.type === 'system' && message.subtype === 'memory_generation') {
    console.log(message.result.status);
  }
}
Generation reports one of five outcomes:
StatusMeaning
savedEvery written file succeeded
partialAt least one file succeeded and one failed — inspect failedFiles
no_changeThe attempt ran and found nothing new to record
skippedThe attempt did not run, for example because a gate returned run: false
failedThe attempt ran and produced no successful write
Consumption reports success, partial, or failed, with a per-file status of loaded, missing, failed, or truncated. A missing file is not an error on its own — memory files may legitimately not exist yet on a first run.

Control memory at runtime

Two methods on the query handle the timing problems that background generation creates.
const q = query({ prompt: userMessages(), options: { memory: { mode: 'native' } } });

// Wait for pending turn-complete generation before shutting down or asserting on files
await q.flushMemory();

// Reload configured memory files after an external process changed them
await q.refreshMemory();
flushMemory() matters most in CI and in tests: without it, a process that exits right after the final result message can terminate a generation attempt mid-write.

Verify the effective configuration

Memory options are negotiated with the runtime, so read back what actually took effect instead of trusting the request:
const init = await q.initializationResult();
console.log(init.memory);
// {
//   enabled: true,
//   requester: 'sdk',
//   mode: 'native',
//   generationEnabled: true,
//   turnCompleteEnabled: true,
//   consumptionEnabled: true,
//   roots: [{ id: 'user', access: 'read-write' }, { id: 'project', access: 'read-write' }]
// }
When memory is omitted or set to {}, init.memory is undefined.

Next steps