How the AI Agent System Integrates Team Memory and Tools in the Macro Codebase
The Macro AI agent system combines dynamic tool registration, PostgreSQL-persisted team memory, and structured system prompt assembly to enable context-aware, tool-enabled automation workflows.
The macro-inc/macro repository implements a Rust-based AI agent that retrieves personalized team memory and invokes tools through a unified prompting flow. This article examines the three core components—Agent Loop, Memory Service, and PG Memory Repository—and how they coordinate to deliver contextual responses.
Agent Loop: Session Creation and Dynamic Tool Registration
The AgentLoop in crates/agent/src/agent_loop.rs orchestrates per-request execution. When a request arrives, AgentLoop::session (or test_session for tests) constructs a Session that encapsulates the LLM, tools, and streaming infrastructure.
let session = AgentLoop::new(recorder)
.session(toolset, Arc::new(tool_context), &system_prompt, usage_ctx)
.await;
During session initialization (lines 73-120), the loop performs three critical operations:
- Builds a searchable catalog via
toolset.searchable_catalog()containing static and MCP (Macro-Customer-Portal) tools - Creates dynamic tool adapters using
DynToolSetAdapter::from_toolsetand registers each with a liveToolServerthroughhandle.add_dynamic_tool(adapter).await - Bundles request context with the user ID and cancellation token via
RequestContext::new(usage_ctx.user.clone())
Dynamic tool loading continues throughout the session lifecycle. The register_loaded closure (lines 128-176) receives discovered SearchableTools and hot-loads them into the running ToolServer.
Memory Service: Fetching and Generating Team Memory
The MemoryServiceImpl in crates/memory/src/domain/service.rs provides the <user_memory> block injected into system prompts. Every agent task calls get_or_generate_memory before execution:
match memory_service.get_or_generate_memory(owner.clone()).await {
Ok(memory) => memory,
Err(e) => {
tracing::warn!(error=?e, %owner, "failed to fetch user memory; running without it");
None
}
}
This invocation appears in agent_task.rs lines 59-81. The service checks PgMemoryRepo::get_latest_memory for freshness against the MAX_AGE constant (24 hours). Stale or missing memory triggers an asynchronous generation pipeline:
- Research phase: Calls the LLM with
GENERATE_MEMORY_PROMPTto synthesize context from documents, projects, and emails - Validation phase: Sends the draft to a judge LLM using
JUDGE_PROMPTfor quality scoring - Persistence phase: Stores accepted memory via
PgMemoryRepo::save_memory
The core orchestration lives in lines 10-25; the generation flow occupies lines 61-78.
PG Memory Repository: Persistence Layer
The PgMemoryRepo in crates/memory/src/outbound/pg_memory_repo.rs (lines 19-55) provides SQLx-backed operations against the memory table:
get_latest_memory(user)— Retrieves the most recent memory record with timestampsave_memory(memory, user)— Persists validated memory content- Record age validation supports the
MAX_AGEfreshness check
This repository enables the memory service to treat team context as durable, queryable state rather than ephemeral prompt injection.
System Prompt Assembly: Combining Memory and Tools
The agent constructs its system prompt through explicit string concatenation in agent_task.rs lines 24-30:
let system_prompt = match user_memory {
Some(memory) => format!("{}\\n{}\\n<user_memory>\\n{}\\n</user_memory>\\n{}",
tools.prompt, SCHEDULED_AGENT_PROMPT, memory, agent_task.prompt),
None => format!("{}\\n{}", tools.prompt, agent_task.prompt),
};
The four components are:
| Component | Source | Purpose |
|---|---|---|
tools.prompt |
ai_tools::all_tools() |
Describes all available tools to the LLM |
SCHEDULED_AGENT_PROMPT |
Constant | Signals automation-triggered execution context |
<user_memory> |
MemoryServiceImpl |
Injects personalized team context (optional) |
agent_task.prompt |
User input | High-level instruction for the specific task |
This assembled prompt is passed to AgentLoop::session and transmitted as part of the LLM request payload.
Streaming Execution and Tool Invocation
Inside the session, user messages convert to Rig-compatible format and stream to the model:
let rig_messages = agent::to_rig_messages(&[user_msg]);
let mut stream = session.send_message(rig_messages).await?;
Lines 47-52 in agent_task.rs demonstrate this pattern. The stream consumes with idle_timeout, accumulating parts into a StreamAccumulator. Final assistant text filters from these parts and optionally routes to notification services.
When the LLM emits tool calls, the ToolServer intercepts and executes them. The server maintains a live registry blending:
- Pre-registered static tools loaded at startup
- Dynamically loaded tools from the
register_loadedclosure
Complete Integration Flow
The Macro AI agent system follows this coordinated sequence:
AgentLoop::sessioncreatesSessionandToolServerwith dynamic tool adaptersMemoryServiceImplsupplies fresh<user_memory>viaPgMemoryRepoor generates new context- System prompt assembles
tools.prompt+SCHEDULED_AGENT_PROMPT+user_memory+agent_task.prompt - Session streams LLM response; tool calls route through live
ToolServerregistry - Assistant output persists to chat database with optional notification dispatch
Summary
- Agent Loop (
crates/agent/src/agent_loop.rs) manages per-request sessions with dynamic tool loading and streaming LLM completion - Memory Service (
crates/memory/src/domain/service.rs) fetches or generates 24-hour-fresh team memory through research and judge LLM validation - PG Memory Repository (
crates/memory/src/outbound/pg_memory_repo.rs) provides durable storage for memory documents - System Prompt Assembly concatenates tool descriptions, automation context, optional user memory, and task instructions into the LLM prompt
- Tool Execution routes through a live
ToolServerregistry supporting both static and dynamically discovered tools
Frequently Asked Questions
How does the Macro agent decide when to generate new team memory?
The MemoryServiceImpl compares the latest stored memory timestamp against the MAX_AGE constant (24 hours). Records older than this threshold or completely missing entries trigger the asynchronous generation pipeline involving research and judge LLM calls.
What happens if team memory fetching fails?
The agentTask gracefully degrades. As shown in agent_task.rs lines 59-81, fetch failures log a warning with tracing::warn! and proceed with None memory—the system prompt omits the <user_memory> block rather than failing the entire request.
Can tools be added after session initialization?
Yes. The register_loaded closure in AgentLoop::session_with (lines 128-176) enables dynamic tool discovery and hot-loading throughout the session lifecycle, extending the ToolServer registry beyond startup-time static tools.
Where does the tool description prompt originate?
The tools.prompt component comes from ai_tools::all_tools() in crates/ai_tools/src/subagent.rs, providing a static catalog of available tools that is injected into every system prompt construction.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →