Skip to main content

Generic multi-layer agent memory for LLM simulation loops (L0→L1→L2→L3)

Project description

istar

Generic multi-layer agent memory for LLM simulation loops.

Istar (singular) or Istari (plural) is the Quenya term for Wizards, which means "those who know", referring to five angelic beings known as Maiar sent to Middle-earth by the Valar to aid the Free Peoples against Sauron.

Istar borrows the L* tier hierarchy (L0 → L3) from TencentDB-Agent-Memory, which pioneered layered abstraction for agent memory.


LLM agents running over many turns need structured memory — not a raw transcript dump. istar provides a four-layer pipeline that compresses turn-by-turn data into increasingly abstract representations: raw records → discrete facts → strategic narratives → cross-run lessons.

The pipeline is domain-agnostic. You implement three small protocol classes that inject your domain's logic; istar handles the lifecycle, throttling, LLM calls, and persistence.


Architecture

┌──────────────────────────────────────────────────────────────┐
│  L3  Run Profile      Markdown file — persists across runs   │
│       ↑ one LLM call at run end                              │
├──────────────────────────────────────────────────────────────┤
│  L2  Episodes         ~150-token narrative — updated every   │
│                       25 turns by one LLM call               │
│       ↑ synthesised from L1 atoms + domain context           │
├──────────────────────────────────────────────────────────────┤
│  L1  Atoms            Timestamped facts — extracted every    │
│                       10 turns by deterministic rules        │
│       ↑ derived from L0 records, no LLM involved            │
├──────────────────────────────────────────────────────────────┤
│  L0  Turn Records     In-memory per-turn data                │
│                       (TurnRecord — normalised from your     │
│                        simulator's native response)          │
└──────────────────────────────────────────────────────────────┘

Only L2 and L3 involve LLM calls. L1 extraction is deterministic — no inference, no cost.


Installation

pip install istar

Quick start

Implement three protocol classes for your domain, then wire them in:

from istar import AgentMemory, TurnRecord, L1Atom
from istar.models import EpisodePromptContext, RetroPromptContext

class MyAtomExtractor:
    def extract(self, window: list[TurnRecord], existing: list[L1Atom]) -> list[L1Atom]:
        atoms = []
        for i in range(1, len(window)):
            prev, curr = window[i - 1], window[i]
            # detect inflection points in your domain
            if prev.reward > 0 and curr.reward < 0:
                atoms.append(L1Atom(
                    id=f"loss-start-{curr.turn}",
                    turn=curr.turn,
                    type="market_event",
                    content=f"T{curr.turn}: Turned unprofitable (reward {curr.reward:.1f}).",
                    priority=85,
                    tags=["loss_start"],
                ))
        return atoms

class MyEpisodeClassifier:
    def build_context(self, recent_turns: list[TurnRecord], prev_turn: int) -> EpisodePromptContext:
        avg = sum(r.reward for r in recent_turns) / len(recent_turns) if recent_turns else 0
        return EpisodePromptContext(
            phase_names=["GROWTH", "PLATEAU", "DECLINE", "RECOVERY"],
            domain_preamble="You are tracking strategic phases for a trading agent.",
            context_summary=f"Average reward over last {len(recent_turns)} turns: {avg:.2f}.",
            phase_guidance="Classify based on the reward trend, not individual events.",
        )

class MyRetroClassifier:
    def build_context(self, all_turns: list[TurnRecord], episodes) -> RetroPromptContext:
        total = sum(r.reward for r in all_turns)
        return RetroPromptContext(
            domain_preamble="You are writing a retrospective for a trading agent.",
            outcome_summary=f"Total reward: {total:.1f} over {len(all_turns)} turns.",
            outcome_metric_name="reward",
        )

# Construct memory for one agent
memory = AgentMemory(
    atom_extractor=MyAtomExtractor(),
    episode_classifier=MyEpisodeClassifier(),
    retro_classifier=MyRetroClassifier(),
)

# Every turn
memory.record_turn(TurnRecord(
    turn=1, reward=42.0, reward_unit="pts",
    events=["price_spike"], actions_taken=[{"type": "buy", "qty": 10}],
    extra={"price": 105.0, "volatility": 0.3},
))
memory.extract_atoms(turn=1)  # self-throttles to every 10 turns

# Every 25 turns (async)
episode = await memory.update_episode(router, turn=25)

# Inject into agent observation
block = memory.memory_block()
# → {"current_phase": "[GROWTH | T1-T25]\n...", "key_observations": [...]}

# End of run (async)
await memory.summarise_run(router, profile_id="my-agent", profiles_dir=Path("profiles/"))

Domain injection protocols

Three protocol classes are the only domain-specific code you write:

AtomExtractor

Scans a window of TurnRecord objects and emits L1Atom objects for detected inflections. Called every 10 turns. No LLM. Should be fast and deterministic.

class AtomExtractor(Protocol):
    def extract(self, window: list[TurnRecord], existing_atoms: list[L1Atom]) -> list[L1Atom]: ...

Atom priorities (0–100) control eviction when the in-memory ring fills (default cap: 20 atoms). Higher priority atoms survive longer. Use ~80–90 for critical events, ~40–60 for routine ones.

EpisodeClassifier

Builds the prompt context for L2 episode synthesis. Called every 25 turns, just before the LLM call.

class EpisodeClassifier(Protocol):
    def build_context(self, recent_turns: list[TurnRecord], prev_episode_end_turn: int) -> EpisodePromptContext: ...

EpisodePromptContext carries: phase_names (your taxonomy), domain_preamble (who the LLM is), context_summary (domain metrics), phase_guidance (classification hints).

RetroClassifier

Builds the prompt context for the L3 end-of-run retrospective. Called once per run.

class RetroClassifier(Protocol):
    def build_context(self, all_turns: list[TurnRecord], episodes: list[L2Episode]) -> RetroPromptContext: ...

Data models

@dataclass
class TurnRecord:
    turn: int
    reward: float           # scalar outcome this turn
    reward_unit: str        # "EUR", "points", etc. — used in prompts
    events: list[str]       # named events that fired
    actions_taken: list[dict]
    extra: dict             # domain-specific fields; only your AtomExtractor reads these

@dataclass
class L1Atom:
    id: str                 # stable identifier — MD5 of (turn, tag)
    turn: int
    type: str               # "market_event" | "coordination_event" | "portfolio_event"
    content: str            # human-readable — injected into prompts
    priority: int           # 0–100; drives eviction order
    tags: list[str]         # machine-readable labels

@dataclass
class L2Episode:
    name: str               # phase name from your EpisodeClassifier
    turn_start: int
    turn_end: int
    narrative: str          # LLM-written description
    heat: int               # times injected into agent observations via memory_block()

AgentMemory reference

Method When to call LLM?
record_turn(record) Every turn No
extract_atoms(turn) Every turn — self-throttles to every 10 No
update_episode(router, turn) Every turn — self-throttles to every 25 Yes, 1 call
memory_block() Every turn, to build agent observation No
summarise_run(router, profile_id, profiles_dir) End of run Yes, 1 call

Constants (override by subclassing):

Constant Default Meaning
EXTRACT_EVERY 10 Atom extraction cadence (turns)
EPISODE_EVERY 25 Episode synthesis cadence (turns)
MAX_ATOMS 20 In-memory atom ring buffer size
MAX_EPISODES 5 Max episodes kept in memory

L3 cross-run learning

At the end of each run, summarise_run() writes a Markdown retrospective to profiles_dir/{profile_id}.md. To inject it into the next run's system prompt:

from istar.memory import load_prior_run_notes

notes = load_prior_run_notes("my-agent", profiles_dir=Path("profiles/"))
system_prompt = base_persona + (f"\n\n{notes}" if notes else "")

Reference implementation

The examples/energy_tycoon/ directory contains a complete, runnable implementation against a competitive multi-agent energy market simulator:

File What it shows
examples/energy_tycoon/adapter.py All three protocols — EnergyAtomExtractor, EnergyEpisodeClassifier, EnergyRetroClassifier — plus make_turn_record translating the simulator API to TurnRecord
examples/energy_tycoon/memory.py AgentMemory subclass that pre-wires the adapters, overrides EXTRACT_EVERY = 3, and adapts the call signatures to the simulator's native API

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

istar_memory-0.1.0.tar.gz (34.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

istar_memory-0.1.0-py3-none-any.whl (13.1 kB view details)

Uploaded Python 3

File details

Details for the file istar_memory-0.1.0.tar.gz.

File metadata

  • Download URL: istar_memory-0.1.0.tar.gz
  • Upload date:
  • Size: 34.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.9

File hashes

Hashes for istar_memory-0.1.0.tar.gz
Algorithm Hash digest
SHA256 87ca45e69a6e2086c3355e999f8a704b1c6a8d795fd567950cafcc4dc46adfc5
MD5 a7408fd7ec55caeb5117fa0758103753
BLAKE2b-256 2fc2ab8d2aeb1664e1ccba8b1fb3d62ec161d140a2e21a5cde1b0bf7d9d80293

See more details on using hashes here.

File details

Details for the file istar_memory-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: istar_memory-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.9

File hashes

Hashes for istar_memory-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b80dcf03d92b59f55d1d3ae1d26391cf16254341220805d5bcf61614adf3bfb4
MD5 47a76bd5e0cb42369681a71adb2c6770
BLAKE2b-256 a3f111e438ef941baca2c6d8bae0d0207af437ea67a40f66a4c54124bafd51f7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page