Memory
September 5, 2026 · View on GitHub
Documentation index · CodexKit
Configure automatic capture, write guided or raw memory records, control retrieval, and inspect attribution.
Memory Layer
CodexKit now supports three memory layers:
- high-level automatic capture policies for apps that want the runtime to extract memory after successful turns
- a guided
MemoryWriterlayer that resolves defaults into concrete records - the raw
MemoryRecord/MemoryStoringAPIs for apps that want exact control
The SDK owns storage, retrieval, ranking, and optional prompt injection. Your app can choose how automatic or explicit memory authoring should be.
Instruction placement
Rendered memory stays inside the backend instruction string. Its placement is configurable:
let memory = AgentMemoryConfiguration(
store: memoryStore,
promptRenderer: RacingMemoryPromptRenderer(),
instructionPlacement: .beforeSkills,
automaticCapturePolicy: nil
)
The default, .afterSkills, preserves the original CodexKit behavior. A thread can override
the runtime default through AgentMemoryContext.instructionPlacement, and one request can
override both through MemorySelection.instructionPlacement. .beforePersonas
inserts memory after base instructions and before every persona or skill section.
.beforeSkills inserts it immediately before the first active skill, so domain learnings can
inform an analysis while the skill remains the later instruction. Placement never changes the
relative order of existing base, persona, and skill sections. In particular, personas continue
to replace base instructions, and an existing thread skill continues to precede a turn persona
override. Placement is precedence signalling within one instruction string, not a security or
validation boundary.
Automatic capture
High-level automatic capture looks like this:
let runtime = try AgentRuntime(configuration: .init(
authProvider: try ChatGPTAuthProvider(
method: .deviceCode,
deviceCodePresenter: deviceCodeCoordinator
),
secureStore: KeychainSessionSecureStore(
service: "CodexKit.ChatGPTSession",
account: "demo"
),
backend: CodexResponsesBackend(
configuration: .init(model: .gpt56Sol)
),
approvalPresenter: approvalPresenter,
stateStore: try SQLiteRuntimeStateStore(),
memory: .init(
store: try SQLiteMemoryStore(),
automaticCapturePolicy: .init(
source: .lastTurn,
options: .init(
defaults: .init(
namespace: "demo-assistant",
category: "preference"
),
maxMemories: 2
)
)
)
))
let thread = try await runtime.createThread(
title: "Health Coach",
memoryContext: .init(
namespace: "demo-assistant",
scopes: ["feature:health-coach"]
)
)
_ = try await runtime.send(
Request(text: "Be direct with me when I fall behind on steps."),
in: thread.id
)
Guided writing
Mid-level guided authoring looks like this:
let writer = try await runtime.memoryWriter(
defaults: .init(
namespace: "demo-assistant",
scope: "feature:health-coach",
category: "preference",
tags: ["steps", "tone"]
)
)
let record = try await writer.upsert(
MemoryDraft(
summary: "Health Coach should use direct accountability when the user is behind on steps.",
evidence: ["The user responds better to blunt reminders than soft encouragement."],
importance: 0.9,
dedupeKey: "health-coach-direct-accountability"
)
)
Explicit capture
If you want the SDK to capture memory for you, AgentRuntime can extract durable memory candidates from a thread or transcript and write them automatically:
let thread = try await runtime.createThread(
title: "Health Coach",
memoryContext: .init(
namespace: "demo-assistant",
scopes: ["feature:health-coach"]
)
)
let result = try await runtime.captureMemories(
from: .threadHistory(maxMessages: 6),
for: thread.id,
options: .init(
defaults: .init(
namespace: "demo-assistant",
scope: "feature:health-coach",
category: "preference"
),
maxMemories: 3
)
)
print(result.records.count)
Raw store access
If you want full control, the low-level store API is still there:
let memoryStore = try SQLiteMemoryStore()
try await memoryStore.upsert(
MemoryRecord(
namespace: "demo-assistant",
scope: "feature:health-coach",
category: "preference",
summary: "Health Coach should use direct accountability when the user is behind on steps.",
evidence: ["The user responds better to blunt coaching than soft encouragement."],
importance: 0.9,
tags: ["steps", "tone"]
),
dedupeKey: "health-coach-direct-accountability"
)
let runtime = try AgentRuntime(configuration: .init(
authProvider: try ChatGPTAuthProvider(
method: .deviceCode,
deviceCodePresenter: deviceCodeCoordinator
),
secureStore: KeychainSessionSecureStore(
service: "CodexKit.ChatGPTSession",
account: "main"
),
backend: CodexResponsesBackend(),
approvalPresenter: approvalInbox,
stateStore: try SQLiteRuntimeStateStore(),
memory: .init(store: memoryStore)
))
let thread = try await runtime.createThread(
title: "Press Chat",
memoryContext: AgentMemoryContext(
namespace: "demo-assistant",
scopes: ["feature:health-coach", "thread:daily-checkin"]
)
)
Per-turn retrieval
Per-turn memory can be narrowed, expanded, replaced, or disabled with MemorySelection:
let reply = try await runtime.send(
Request(
text: "How should the health coach respond when the user is behind on steps?",
memorySelection: MemorySelection(
mode: .append,
scopes: ["feature:travel-planner"],
tags: ["steps"],
text: "daily step adherence coaching",
instructionPlacement: .beforeSkills
)
),
in: thread.id
)
MemorySelection.text is the retrieval query override. Without it, CodexKit searches with
Request.text. Typed Request.context, request options, and image contents are deliberately
not converted into search text or exposed to the memory store. Applications whose visible
prompt is generic—such as a typed race-analysis request—should build a bounded, deterministic
query from the domain features that are safe and useful for retrieval.
Rendering and attribution
Memory renderers can also declare exactly which selected records contributed to their prompt.
Existing renderers remain source compatible, but their attribution is conservatively empty.
A custom renderer must implement renderWithMetadata to provide exact attribution:
struct RacingMemoryPromptRenderer: MemoryPromptRendering {
func render(result: MemoryQueryResult, budget: MemoryReadBudget) -> String {
renderWithMetadata(result: result, budget: budget).instructions
}
func renderWithMetadata(
result: MemoryQueryResult,
budget: MemoryReadBudget
) -> RenderedMemoryPrompt {
let included = Array(result.matches.prefix(max(0, budget.maxItems)))
let instructions = included
.map { "- \(\$0.record.summary)" }
.joined(separator: "\n")
return RenderedMemoryPrompt(
instructions: instructions,
includedRecordIDs: included.map(\.record.id)
)
}
}
Before memory reaches the backend, CodexKit validates the resolved query before calling even a
custom store, verifies returned records against its scope, text, ordering, cursor, and size
contract before rendering, requires renderer output to fit the effective
MemoryReadBudget.maxCharacters, bounds renderer metadata, and reserves worst-case identifier
encoding in the attribution snapshot. Invalid or oversized memory is omitted with a
memory-category warning, so a successful model turn cannot fail later merely because its
attribution record is unsafe to persist. Task cancellation is never converted into an ordinary
memory miss, and a completion emitted after cancellation cannot create attribution.
Persisted attribution and turn results
Every successful threaded runtime turn durably stores its MemoryApplicationSnapshot inside
the same turnCompleted history event. It includes the real thread and turn IDs, optional
host correlation ID, model and reasoning settings, active skill IDs, renderer identifier,
SHA-256 digest of the complete compiled instructions, query result, exact rendered memory
text, declared record IDs, and placement. Recover persisted attribution without relying on a
callback:
let request = Request(text: "Analyse this race")
.correlated(with: assessmentID)
let completed = try await runtime.sendWithSummary(
request,
in: thread.id,
response: RaceAssessment.self
)
let applied = completed.memoryApplicationSnapshot
assert(applied?.turnID == completed.summary.turnID)
let durable = try await runtime.fetchMemoryApplicationSnapshots(id: thread.id)
assert(durable.first == applied)
AgentTurnResult.memoryApplication is explicit about successful turns that did not apply memory:
.applied(snapshot)returns the exact query result and rendered instructions used for the turn..notApplied(.disabled)means the request explicitly disabled memory..notApplied(.noMatches)means retrieval succeeded but neither the result nor renderer produced instructions..notApplied(.unavailable)means the memory store could not complete the query; the model turn continued without memory.- Other reasons distinguish missing selection context, empty renderer output, rejected unsafe data, an unconfigured runtime, and attribution omitted by manual result construction.
The result also exposes clientRequestID at the top level, so callers can correlate a successful
turn even when memory was not applied. CodexKit resolves memory once before backend execution and
captures the outcome at accepted completion; returning attribution does not run another query.
For an ephemeral request, sendWithSummary returns the same first-class outcome without writing a
turn or attribution record:
let completed = try await runtime.sendWithSummary(
Request(
text: "Analyse this transient race payload",
executionMode: .ephemeral
).correlated(with: assessmentID),
in: thread.id,
response: RaceAssessment.self
)
if case let .applied(snapshot) = completed.memoryApplication {
learningPipeline.recordEvidence(from: snapshot)
}
sendWithSummary requires a backend completion-summary event. The existing send overloads retain
their value-only, message-completion behavior for source and behavioral compatibility with custom
backends. Manually initialized AgentTurnResult values default to .notApplied(.notReported).
MemoryObserving.handle(application:) is a non-blocking notification of that durable record;
it is not the source of truth and may be missed if the process exits. Query previews, empty
rendered memory, failed or cancelled turns, and runtime-rejected completions do not produce
application snapshots. For ephemeral sendWithSummary calls, the returned outcome is authoritative
for that call while the observer remains best effort. A completed runtime turn means the model
received the attributed memory and the runtime accepted its completion; an application with
additional domain validation should keep the snapshot pending until that validation succeeds.
The runtime validates every custom-backend event against the active thread and turn, using the correlation fields that event exposes, before it is published, executed, or persisted. An accepted completion is terminal: later duplicate events or backend failures cannot add a second attribution record or change the completed turn to failed.
The attribution convenience queries page through system history in bounded batches, reject stalled
or malformed cursors, cap aggregate decoded payloads, and stop at
AgentStoreLimits.maximumMemoryAttributionScanCount. They throw AgentStoreError.invalidInput
when those safety bounds prevent the requested result from being completed. Apps that intentionally
need older raw events can page fetchThreadHistory(id:query:) directly with their own cursor policy.
Successful compactions performed by an instruction-aware backend similarly store
MemoryCompactionApplicationSnapshot beside the contextCompacted marker and can be recovered with
fetchMemoryCompactionApplicationSnapshots(id:). Compaction uses the same resolved memory and
placement but excludes turn-only skill execution-policy wording. Automatic pre-turn compaction
only compacts prior history; it never folds the pending request into history and sends it again.
Local-only compaction and local fallback never claim memory attribution because they do not consume
the compiled instructions. Automatic capture remains a separate opt-in policy.
Durable snapshots contain complete matched memory records, which may include evidence and attributes. Treat runtime history as sensitive application data and use the existing history redaction/deletion controls where retention is not appropriate.
Inspection and diagnostics
For debugging and tooling, memory stores also support direct inspection:
let stored = try await memoryStore.record(
id: "some-memory-id",
namespace: "demo-assistant"
)
let records = try await memoryStore.list(
namespace: "demo-assistant",
scopes: ["feature:health-coach"],
includeArchived: true,
limit: 20
)
let diagnostics = try await memoryStore.diagnostics(namespace: "demo-assistant")
The demo app now includes a dedicated Memory tab that shows:
- high-level automatic capture after a normal turn
- mid-level automatic capture from transcript
- guided authoring with
MemoryWriter - raw record writes against the underlying store
- preview of the exact prompt block injected into a turn