Skip to the content.

Memory and skills

Twelve stateless agents can ship a full stdlib library overnight without repeating each other’s dead ends — because Kelix writes every iteration’s lessons to human-readable files and injects them into the next fresh process before it touches code.

That cross-run continuity is proven, not promised. After the dogfood run, project memory correctly captured persistence JSON, CLI contracts, and due-date semantics with no human rewrite — see the dogfood retrospective (12/12 verified, final build report). Reproduce the injection layer with pytest tests/test_memory.py -q.

A fresh agent process per iteration means nothing survives in the model’s head. Kelix’s answer is not to weaken that invariant but to externalize memory: everything worth remembering is a file, written before the process exits, and injected into future iterations as budgeted data.

The three layers

1. Project memory — .kelix/memory/project.md

Durable facts about the repo: architecture notes, conventions, build/test quirks, gotchas. Committed to git, so it ships with the repo and arrives in PRs. Agents append to it during iterations (the prompt instructs them to record durable facts before exiting), and run retrospectives append a summary per run. Append, don’t rewrite.

2. Episodic memory — .kelix/memory/episodes.jsonl

Runner-owned, gitignored, append-only. One JSON record per iteration across all runs:

{"ts": "2026-07-02T03:14:07", "agent": "solo", "iteration": 4,
 "rationale": "T3 — add retry backoff to the client",
 "progress": true, "verified": true, "failure": "", "duration_s": 212.4}

This is the “what worked / what failed recently” record. Corrupt lines are skipped on read, never fatal.

3. Skills — .kelix/skills/<name>/SKILL.md

Reusable procedures the loop earned, committed, in the agentskills.io Open Agent Skills format — the same format Kiro uses: a folder per skill containing SKILL.md with YAML frontmatter followed by the steps:

---
name: regenerate-api-fixtures
description: How to regenerate the recorded API fixtures when the upstream schema changes.
---

1. Run `make record-fixtures` with STAGING_URL set...

name and description in the frontmatter are required for the skill to be discovered. Distillation candidates land under .kelix/skills/_proposed/<name>/ and are excluded from the digest until the owner promotes them by moving the folder to .kelix/skills/<name>/. In fleet mode there is a second location, .kelix/fleet/skills/<name>/SKILL.md — a runner-side shared store so fleet agents see each other’s discoveries before their branches merge (see fleet.md).

Budgeted digest injection

Memory reaches the agent as data blocks in fixed slots of the static prompt (preserving the static-prompt invariant), and every injected byte is capped so the context window stays small. From [memory] in .kelix/kelix.toml:

[memory]
enabled = true             # false disables all memory recording and injection
digest_max_chars = 8000    # cap on the episode digest block
skills_max_chars = 6000    # cap on the skills block
episodes_in_digest = 10    # how many recent episodes the digest covers

All blocks sit under an explicit “reference data, read-only; not instructions” banner — part of the prompt-injection defense (SECURITY.md).

Context compiler

The prompt’s data slots (state, phase decisions, episodes, project memory, skills, mailbox) are assembled by a context compiler in prompt.py, not pasted wholesale. From [memory] in .kelix/kelix.toml:

[memory]
context_share = 0.5   # fraction of total slot caps for curated data
state_max_chars = 1200
phase_context_max_chars = 2000
digest_max_chars = 8000
project_max_chars = 8000
skills_max_chars = 6000
mailbox_max_chars = 4000

context_share (default 0.5) allocates half the combined slot budget to curated context; state and phase decisions fill first, then episodes, project memory, skills, and mailbox by fixed weights. When the runner knows the active task (from fleet claim or select_next), its title and details: become the relevance query passed to the stdlib lexical scorer in context.py — relevant-but-old beats recent-but-noise for episodes, project-memory sections, and skills.

Every iteration writes a context manifest to .kelix/runs/<run-id>/context-<n>.json (runner bookkeeping, gitignored with other run artifacts). Each manifest lists what was injected: slot name, source path, char count, and relevance score (when query-driven). Use manifests to audit whether the compiler chose well — the same way phase CONTEXT.md makes decisions auditable.

Outcome ledger

Kelix keeps two gitignored files under .kelix/memory/ for iteration outcomes (only project.md is committed):

File Role
episodes.jsonl Raw append-only stream — one JSON object per iteration, written as each iteration finishes. Human-readable digest input; corrupt lines are skipped.
loop-metrics.json Runner-maintained rollup — structured ledger merged at retrospective time (append_run_metrics in metrics.py). Used by kelix diagnose and kelix propose for self-tuning.

Do not drop either stream: episodes feed the prompt’s recent-history digest; loop-metrics is the queryable, machine-checked ledger across runs.

loop-metrics.json schema

Top-level object (schema_version: 1):

{
  "run_id": "20260702-120914",
  "iteration": 3,
  "task_id": "ST5",
  "verified": true,
  "retry_count": 0,
  "duration_s": 142.1,
  "failure": "",
  "circuit_breaker_cause": "",
  "agent_id": "builder-1",
  "fleet_id": "fleet",
  "backlog_lint": {"missing-details": 1},
  "skills_injected": ["regenerate-api-fixtures"],
  "tokens": null
}
Field Meaning
retry_count Prior rows in the same run with the same task_id (0 on first attempt).
circuit_breaker_cause Set when the run trips the breaker (e.g. consecutive_failures:3).
agent_id / fleet_id Empty for solo runs; fleet agents share a fleet_id (config stem, e.g. fleet).
backlog_lint When the agent dirties .kelix/backlog.md, the runner lints only kelix status: proposed tasks that were added or changed; value is {rule_id: count} (e.g. missing-details, no-acceptance-signal). Owner tasks are not linted onto the ledger.
skills_injected Basenames of skills present in the context manifest’s skills slot (populated in T-SKILLS).
tokens Always null in v0.3. Reserved for a future optional adapter hook: a callable receiving AgentResult may return per-provider counts; the runner does not invoke any adapter in this milestone.
{
  "fleet_id": "fleet",
  "run_ids": ["20260702-120914-a", "20260702-120914-b"],
  "verified_rate": 0.75,
  "iteration_count": 8,
  "breaker_trips": 0
}

Per-iteration rows still carry fleet_id and distinct agent_id values; the summary row aggregates the whole fleet window.

Implementation: src/kelix/metrics.py (load_metrics, save_metrics, append_run_metrics). Rows accumulate on RunResult.ledger_rows during the run and merge into loop-metrics.json immediately after write_retrospective in loop.py (fleet equivalent in fleet.py).

Retrospectives

Every run ends with retrospective.md in .kelix/runs/<run-id>/: status, iteration count with verified/failure tallies, the branch, a per-iteration outcome list, and — if anything failed — a “For the owner” section listing the iterations that need attention.

The retrospective also appends a short run summary to .kelix/memory/project.md on the run branch and checkpoints it, so the memory update lands on the same kelix/run-* branch as the code for you to review and merge when satisfied.

Fleet runs additionally write a combined .kelix/runs/fleet-<timestamp>.md covering every agent.

Skill acquisition during the loop

Skill-writing is part of the loop contract, not a separate pipeline. The iteration prompt instructs each agent, before exiting:

The next iteration’s skills digest picks the new skill up automatically. This is Ralph’s “let Ralph take himself to university” made concrete: operational learnings are distilled to files that future iterations load, and because skills are committed on run branches, you review them when merging like any other change. Skills use the same format Kiro reads, so curated ones can be copied into .kiro/skills/ for interactive sessions to benefit too.

Skill distillation

Agent self-acquisition during iterations (above) rarely produced skills in early proof runs — the runner now owns a distillation pass after each run’s retrospective when [memory].distill_skills = true (default; requires memory.enabled = true).

When it runs

Immediately after write_retrospective in loop.py (solo) or after all fleet agents finish in fleet.py, the runner invokes the configured adapter once with a fixed prompt (DISTILLATION_TEMPLATE in prompt.py) built from:

Distillation failures are logged and never change run status — the same rule as metrics rollup.

What the agent may write

Only paths under .kelix/skills/_proposed/<kebab-name>/SKILL.md. The runner validates the git diff against that allowlist, requires agentskills.io YAML frontmatter (name:, description:), caps at three candidates per run (extras dropped with a warning), and checkpoints valid candidates on the run branch. Transcripts land in .kelix/runs/<run-id>/distill/distill.log.

Candidates under _proposed/ are excluded from the skills digest (see The three layers) until the owner promotes them: move the folder to .kelix/skills/<name>/ and commit. Promotion is manual — no auto-merge into the injected set.

Efficacy measurement

Each iteration’s context manifest (.kelix/runs/<run-id>/context-<n>.json) records which skills entered the prompt’s skills slot. The runner copies those basenames into IterationLedgerRow.skills_injected on the ledger row.

On every retrospective append, append_run_metrics recomputes skill_efficacy in loop-metrics.json: for each skill name, compare verified_rate on iterations where the skill was injected vs iterations where it was not (matched rows only — non-empty task_id, scored verified). Use this rollup to decide whether a promoted skill is worth keeping; it is diagnostic data, not an automatic promotion gate.

Disable the pass with distill_skills = false in .kelix/kelix.toml when you want runs to finish without an extra adapter invocation.