Memory Management Strategies

Building on Examples 01–03, this example demonstrates memory management for long-running agentic loops. As conversations grow with many LLM turns, agents can manage history via different strategies: sliding window (drop old turns), summarization (compress via a dedicated small LLM), or keep-all. The example also shows a dedicated summarizer LLM on a separate GPU partition to avoid contention with the main reasoning model.

Prerequisites: Read 03_hitl_approval.py first.

What you’ll learn:

  • How to configure memory strategies for different agents

  • SLIDING_WINDOW — drop old turns, keep last N; lowest cost

  • SUMMARIZE — keep last N turns + a summary of older ones (via separate LLM)

  • FULL — keep everything (suitable for short-lived agents only)

  • How to set up a dedicated summarizer LLM on a separate node

  • How to balance context preservation with token usage and latency

Architecture:

  • Node 0, GPUs [0,1]: Main inference service (large reasoning LLM, tp_size=2)

  • Node 1, GPU [0]: Summarizer service (small LLM, tp_size=1)

Main Code

Below is the complete example:

Listing 31 04_memory.py: Memory strategies and dedicated summarizer
  1"""04 — Memory Management Strategies + Dedicated Summarizer LLM.
  2
  3**Prerequisites:** Read ``03_hitl_approval.py`` first.
  4
  5**What's new:**
  6
  7* **MemoryConfig** and **MemoryStrategy** — control how agents manage
  8  conversation history as the agentic loop grows
  9* **Three strategies side-by-side:**
 10
 11  - ``planner_agent`` → ``SLIDING_WINDOW`` — drops old turn-pairs beyond
 12    ``max_kept_turns``, replacing them with a synthetic note
 13  - ``runner_agent``  → ``SUMMARIZE`` — calls a second LLM to compress
 14    old turns into a summary instead of dropping them
 15  - ``analyzer_agent`` / ``reporter_agent`` → ``FULL`` (default) — keep
 16    everything (fine for short-lived agents)
 17
 18* **Dedicated summarizer LLM** — a separate, smaller model on its own
 19  GPU partition, used only for summarization.  The main model stays
 20  uncontended for reasoning and tool-calling.
 21
 22Architecture::
 23
 24    Node 0  GPUs [0,1]  → Main InferenceWorker  (large model, tp_size=2)
 25    Node 1  GPU  [0]    → Summarizer Worker     (small model, tp_size=1)
 26
 27Usage::
 28
 29    # Terminal 1 (needs 2 nodes with GPUs):
 30    dragon 04_memory.py
 31
 32    # Terminal 2 (HITL approval client):
 33    python -m dragon.ai.agent.hitl --tcp HOST:PORT
 34"""
 35
 36import asyncio
 37from typing import Any
 38
 39import dragon
 40import multiprocessing as mp
 41
 42from dragon.ai.agent.core import create_sub_agent
 43from dragon.ai.agent.config import (
 44    AgentConfig,
 45    MemoryConfig,          # ← NEW
 46    MemoryStrategy,        # ← NEW
 47    OrchestratorConfig,
 48    Pipeline,
 49    PipelineNode,
 50)
 51from dragon import Policy, System, Batch
 52from dragon.ai.agent.tools import ToolRegistry
 53from dragon.ai.agent.orchestrator import DAGOrchestrator
 54from dragon.native.event import Event
 55from dragon.native.machine import Node
 56from dragon.native.process import Process
 57from dragon.native.queue import Queue
 58
 59from dragon.ai.inference import (
 60    BatchingConfig,
 61    HardwareConfig,
 62    InferenceConfig,
 63    ModelConfig,
 64    Inference,
 65)
 66
 67from tools import (
 68    propose_experiment,
 69    launch_experiment,
 70    check_progress,
 71    collect_results,
 72    analyze_convergence,
 73    format_results_table,
 74)
 75from tools.runner import cleanup_experiment_state
 76
 77
 78# ===========================================================================
 79# User-configurable constants
 80# ===========================================================================
 81
 82MODEL_NAME = "/path/to/your/model"
 83HF_TOKEN = ""
 84
 85# ← NEW: Smaller instruct model used exclusively for summarization
 86SUMMARIZER_MODEL_NAME = "/path/to/your/summarizer/model"
 87
 88
 89# ===========================================================================
 90# Inference Pipeline Configuration — Main (reasoning + tool-calling)
 91# ===========================================================================
 92
 93INFERENCE_CONFIG = InferenceConfig(
 94    model=ModelConfig(
 95        model_name=MODEL_NAME,
 96        hf_token=HF_TOKEN,
 97        tp_size=2,
 98        max_tokens=8192,
 99        max_model_len=32768,
100    ),
101    hardware=HardwareConfig(
102        num_nodes=1,
103        num_gpus=2,
104        num_inf_workers_per_cpu=1,
105    ),
106    batching=BatchingConfig(
107        batch_wait_seconds=0.1,
108        max_batch_size=32,
109    ),
110)
111
112
113# ===========================================================================
114# ← NEW: Inference Pipeline Configuration — Summarizer
115#
116# Placed on the SECOND node (node_offset=1) so the main model's GPUs on
117# node 0 are not contended.  A small instruct model fits on a single GPU.
118# ===========================================================================
119
120SUMMARIZER_CONFIG = InferenceConfig(
121    model=ModelConfig(
122        model_name=SUMMARIZER_MODEL_NAME,
123        hf_token=HF_TOKEN,
124        tp_size=1,
125        max_tokens=2048,
126        max_model_len=8192,
127    ),
128    hardware=HardwareConfig(
129        num_nodes=1,
130        num_gpus=1,
131        node_offset=1,               # ← start from node 1 (second node)
132        num_inf_workers_per_cpu=1,
133    ),
134    batching=BatchingConfig(
135        batch_wait_seconds=0.05,
136        max_batch_size=8,
137    ),
138)
139
140
141# ===========================================================================
142# Tool registries
143# ===========================================================================
144
145planner_registry = ToolRegistry()
146planner_registry.register(propose_experiment)
147
148runner_registry = ToolRegistry()
149runner_registry.register(launch_experiment)
150runner_registry.register(check_progress)
151runner_registry.register(collect_results)
152
153analyzer_registry = ToolRegistry()
154analyzer_registry.register(analyze_convergence)
155
156reporter_registry = ToolRegistry()
157reporter_registry.register(format_results_table)
158
159
160# ===========================================================================
161# DAG pipeline
162# ===========================================================================
163
164pipeline = Pipeline(nodes=[
165    PipelineNode(
166        agent_id="planner_agent",
167        task_description=(
168            "You are a scientific experiment planner.  The user wants to study "
169            "Monte Carlo convergence for estimating π.\n\n"
170            "Propose an experiment plan by calling propose_experiment with:\n"
171            "  - description, sample_sizes, convergence_target, methodology\n\n"
172            "A human operator will review your plan.  If they provide "
173            "feedback, revise and resubmit.  Once approved, report verbatim."
174        ),
175        depends_on=[],
176    ),
177    PipelineNode(
178        agent_id="runner_agent",
179        task_description=(
180            "You manage parallel Monte Carlo simulations on an HPC cluster.\n\n"
181            "Tools:\n"
182            "  1. launch_experiment(sample_sizes, seeds) — launches ALL "
183            "simulations in parallel.  Operator must approve.\n"
184            "  2. check_progress() — shows done/running/pending status.\n"
185            "  3. collect_results() — call ONLY when all_done=true.\n\n"
186            "STRICT workflow:\n"
187            "  1. Call launch_experiment.\n"
188            "  2. Call check_progress until all_done=true.\n"
189            "  3. Call collect_results — REQUIRED before final answer.\n"
190            "  4. Report collect_results output verbatim."
191        ),
192        depends_on=["planner_agent"],
193    ),
194    PipelineNode(
195        agent_id="analyzer_agent",
196        task_description=(
197            "Call analyze_convergence with a list of dicts (keys: "
198            "'n_samples', 'absolute_error').  Report all metrics verbatim."
199        ),
200        depends_on=["runner_agent"],
201    ),
202    PipelineNode(
203        agent_id="reporter_agent",
204        task_description=(
205            "Write a structured report.\n\n"
206            "STRICT workflow:\n"
207            "  1. Call format_results_table to get a Markdown table.\n"
208            "  2. COPY-PASTE the actual table into your final answer.\n"
209            "  3. Include sections: Plan, Results Table, Parallel Execution,\n"
210            "     Convergence Analysis, Quality Assessment, Recommendations.\n\n"
211            "Never invent data — use only data from upstream agents."
212        ),
213        depends_on=["planner_agent", "runner_agent", "analyzer_agent"],
214    ),
215])
216
217
218# ===========================================================================
219# Helpers
220# ===========================================================================
221
222def _make_agent_kwargs(agent_id, name, role, registry, inference_queue,
223                       approval_filter=None,
224                       max_tool_call_iterations=20,
225                       memory=None,
226                       summarizer_inference_queue=None):
227    """Build kwargs for create_sub_agent().
228
229    NEW in this example:
230      ``memory`` — MemoryConfig controlling conversation history pruning
231      ``summarizer_inference_queue`` — separate queue for the summarizer LLM
232    """
233    return {
234        "config": AgentConfig(
235            agent_id=agent_id, name=name, role=role,
236            inference_queue=inference_queue,
237            summarizer_inference_queue=summarizer_inference_queue,
238            approval_filter=approval_filter,
239            max_tool_call_iterations=max_tool_call_iterations,
240            memory=memory,
241        ),
242        "tool_registry": registry,
243        "shutdown_event": Event(),
244        "reply_queue": Queue(),
245    }
246
247
248def _start_agents(specs, policies):
249    procs = []
250    for spec, policy in zip(specs, policies):
251        p = Process(target=create_sub_agent, kwargs=spec, policy=policy)
252        p.start()
253        procs.append(p)
254    queues = {}
255    for spec in specs:
256        aid = spec["config"].agent_id
257        queues[aid] = spec["reply_queue"].get()
258        print(f"[startup] Agent '{aid}' ready.", flush=True)
259    return procs, queues
260
261
262# ===========================================================================
263# Main
264# ===========================================================================
265
266async def main():
267    # -------------------------------------------------------------------
268    # 1. Main inference pipeline
269    # -------------------------------------------------------------------
270    input_queue = Queue()
271
272    print("[startup] Initializing main inference pipeline...", flush=True)
273
274    inference_pipeline = None
275    try:
276        inference_pipeline = Inference(INFERENCE_CONFIG, input_queue)
277        inference_pipeline.initialize()
278    except Exception as exc:
279        import traceback
280        print(f"\n[FATAL] Main inference pipeline failed to initialize: {exc}", flush=True)
281        traceback.print_exc()
282        if inference_pipeline is not None:
283            inference_pipeline.destroy()
284        return
285    print("[startup] Main inference pipeline ready.\n", flush=True)
286
287    # -------------------------------------------------------------------
288    # ← NEW: 2. Summarizer inference pipeline (separate model, separate GPU)
289    # -------------------------------------------------------------------
290    summarizer_input_queue = Queue()
291
292    print("[startup] Initializing summarizer inference pipeline...", flush=True)
293    print(f"[startup] Summarizer model: {SUMMARIZER_MODEL_NAME}", flush=True)
294
295    summarizer_pipeline = None
296    try:
297        summarizer_pipeline = Inference(SUMMARIZER_CONFIG, summarizer_input_queue)
298        summarizer_pipeline.initialize()
299    except Exception as exc:
300        import traceback
301        print(f"\n[FATAL] Summarizer pipeline failed to initialize: {exc}", flush=True)
302        traceback.print_exc()
303        inference_pipeline.destroy()
304        if summarizer_pipeline is not None:
305            summarizer_pipeline.destroy()
306        return
307    print("[startup] Summarizer inference pipeline ready.\n", flush=True)
308
309    my_alloc = System()
310    node_list = my_alloc.nodes
311    compute_host = (
312        Node(node_list[1]).hostname if len(node_list) > 1
313        else Node(node_list[0]).hostname
314    )
315    compute_policy = Policy(
316        placement=Policy.Placement.HOST_NAME,
317        host_name=compute_host,
318    )
319
320    procs, agent_specs = [], []
321    try:
322        agent_specs = [
323            _make_agent_kwargs(
324                "planner_agent", "Experiment Planner",
325                "You are an experiment planner for Monte Carlo convergence "
326                "studies on an HPC cluster.  Propose plans via "
327                "propose_experiment.  Revise on feedback.",
328                planner_registry, input_queue,
329                approval_filter=lambda n, a: n == "propose_experiment",
330                max_tool_call_iterations=60,
331                # ← NEW: SLIDING_WINDOW — drop old turns beyond max_kept_turns
332                memory=MemoryConfig(
333                    strategy=MemoryStrategy.SLIDING_WINDOW,
334                    max_kept_turns=3,
335                ),
336            ),
337            _make_agent_kwargs(
338                "runner_agent", "Parallel Simulation Runner",
339                "You manage parallel Monte Carlo simulations.\n"
340                "You MUST call all three tools in order: "
341                "launch_experiment → check_progress → collect_results.\n"
342                "NEVER give a final answer without calling collect_results first.",
343                runner_registry, input_queue,
344                approval_filter=lambda n, a: n == "launch_experiment",
345                max_tool_call_iterations=60,
346                # ← NEW: SUMMARIZE — compress old turns via LLM
347                memory=MemoryConfig(
348                    strategy=MemoryStrategy.SUMMARIZE,
349                    max_kept_turns=1,
350                    summarize_after_turns=2,
351                ),
352                # ← NEW: use dedicated summarizer LLM
353                summarizer_inference_queue=summarizer_input_queue,
354            ),
355            _make_agent_kwargs(
356                "analyzer_agent", "Convergence Analyzer",
357                "Analyse convergence.  Call analyze_convergence with "
358                "n_samples + absolute_error dicts.  Report verbatim.",
359                analyzer_registry, input_queue,
360                # No memory config → FULL (default) — keep everything
361            ),
362            _make_agent_kwargs(
363                "reporter_agent", "Report Writer",
364                "Write a structured report with Markdown tables.  "
365                "Include parallel execution details.  "
366                "Always include tool output verbatim.",
367                reporter_registry, input_queue,
368                # No memory config → FULL (default)
369            ),
370        ]
371        policies = [None, compute_policy, None, None]
372
373        procs, queues = _start_agents(agent_specs, policies)
374        for spec in agent_specs:
375            spec["config"].input_queue = queues[spec["config"].agent_id]
376
377        orchestrator = DAGOrchestrator(
378            config=OrchestratorConfig(
379                agents=[s["config"] for s in agent_specs],
380                poll_interval=0.5,
381                poll_timeout=14400.0,
382            ),
383            pipeline=pipeline,
384        )
385
386        user_input = (
387            "Design and run a Monte Carlo convergence experiment for π.  "
388            "I want to review the plan first, then approve the parallel "
389            "launch.  Monitor progress and report which nodes ran what."
390        )
391
392        batch = Batch()
393        try:
394            print("=" * 60, flush=True)
395            print("Dragon AI — 04 Memory Strategies + Summarizer LLM", flush=True)
396            print("=" * 60, flush=True)
397            print(f"Request: {user_input}\n", flush=True)
398
399            # Print memory strategy summary
400            print("Memory strategies:", flush=True)
401            print("  planner_agent  → SLIDING_WINDOW (max_kept_turns=3)", flush=True)
402            print("  runner_agent   → SUMMARIZE (dedicated summarizer LLM)", flush=True)
403            print("  analyzer_agent → FULL (default)", flush=True)
404            print("  reporter_agent → FULL (default)", flush=True)
405            print(flush=True)
406
407            if orchestrator.hitl_address:
408                h, p = orchestrator.hitl_address
409                print(
410                    f"HITL approval client — start in another terminal:\n"
411                    f"  python -m dragon.ai.agent.hitl --tcp {h}:{p}\n"
412                    f"  Options:\n"
413                    f"    --jsonl PATH        Custom JSONL path (default: auto-generated)\n"
414                    f"    --report PATH       Custom .txt report path\n"
415                    f"    --no-color          Disable ANSI colours\n",
416                    flush=True,
417                )
418
419            result = orchestrator.run(
420                user_input=user_input,
421                batch=batch,
422            )
423
424            print("\n" + "=" * 60, flush=True)
425            print("FINAL RESULT", flush=True)
426            print("=" * 60, flush=True)
427            print(result, flush=True)
428
429        except Exception as exc:
430            import traceback
431            print(f"\n[error] Pipeline failed: {exc}", flush=True)
432            traceback.print_exc()
433        finally:
434            orchestrator.destroy()
435            batch.join()
436
437    except Exception as exc:
438        import traceback
439        print(f"\n[error] Fatal: {exc}", flush=True)
440        traceback.print_exc()
441    finally:
442        cleanup_experiment_state()
443        for spec in agent_specs:
444            try:
445                spec["shutdown_event"].set()
446            except Exception:
447                pass
448        for p in procs:
449            try:
450                p.join()
451            except Exception:
452                pass
453        print("\n[teardown] All agents stopped.", flush=True)
454        try:
455            inference_pipeline.destroy()
456        except Exception:
457            pass
458        print("[teardown] Main inference pipeline stopped.", flush=True)
459        try:
460            summarizer_pipeline.destroy()
461        except Exception:
462            pass
463        print("[teardown] Summarizer inference pipeline stopped.", flush=True)
464
465
466if __name__ == "__main__":
467    mp.set_start_method("dragon")
468    asyncio.run(main())

Key Concepts

SLIDING_WINDOW:

Keep the most recent max_kept_turns conversation turns. Drop older turns and replace with a synthetic “earlier context” note. Lowest memory and token cost; suitable for short-term reasoning tasks.

SUMMARIZE:

Keep the last max_kept_turns turns in full detail. Older turns are compressed into a summary (generated by a dedicated small LLM) inserted before the kept turns. Good for long-running workflows where full history matters but you want to control latency.

FULL:

Keep all conversation turns from the start. Suitable only for short-lived agents (will run out of context window eventually). Good for understanding agent reasoning in logs.

Dedicated Summarizer LLM:

By running the summarizer on a separate GPU partition (different node, smaller model), the main reasoning agent stays uncontended. This improves throughput and latency of the critical reasoning path.

Installation

See Example 01 (same dependencies).

System Description

Tested on HPE Cray EX:

  • Node 0 (main inference): 2 Nvidia A100 GPUs

  • Node 1 (summarizer): 1 Nvidia A100 GPU

  • Total: 2 compute nodes required

How to Run

Step 1: Edit model paths

Open 04_memory.py and set:

  • MODEL_NAME (main reasoning model, e.g., 70B Llama)

  • SUMMARIZER_MODEL_NAME (small model, e.g., 7B Llama)

Step 2: Allocate nodes

salloc --nodes=2 --exclusive

Step 3: Run

dragon 04_memory.py

Example output:

$ dragon 04_memory.py
Node 0: Starting main inference service (70B model, tp_size=2)
Node 1: Starting summarizer service (7B model, tp_size=1)
Planner (SLIDING_WINDOW): Turn 1, kept_turns=1/5
Planner (SLIDING_WINDOW): Turn 2, kept_turns=2/5
...
Planner (SLIDING_WINDOW): Turn 6, kept_turns=5/5 (dropped turn 1)
Runner (SUMMARIZE): Turn 1, kept_turns=1/10
...
Runner (SUMMARIZE): Turn 11, kept_turns=10/10 + summary of turns 1-5
All agents completed

For HITL approval (in a second terminal):

python -m dragon.ai.agent.hitl_client --tcp HOST:PORT

Next Steps

  • 05 — MCP Tools (integrate remote Model Context Protocol servers)