-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 5 Integration
This page aggregates all Phase 5 documentation for the Integration phase.
Hook handler connection, query CLI, and admin commands.
Create a client library that hook handlers can use to call IngestEvent RPC, with event type mapping from hook events to memory events.
- HOOK-02: Hook handlers call daemon's IngestEvent RPC
- HOOK-03: Event types map 1:1 from hook events
File: crates/memory-client/Cargo.toml
[package]
name = "memory-client"
version = "0.1.0"
edition = "2021"
description = "Client library for Agent Memory daemon"
[dependencies]
memory-service = { path = "../memory-service" }
memory-types = { path = "../memory-types" }
tonic = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }File: crates/memory-client/src/lib.rs
//! Client library for Agent Memory daemon.
//!
//! Per HOOK-02: Hook handlers call daemon's IngestEvent RPC.
pub mod client;
pub mod error;
pub mod hook_mapping;
pub use client::MemoryClient;
pub use error::ClientError;
pub use hook_mapping::{HookEvent, HookEventType, map_hook_event};File: crates/memory-client/src/client.rs
use memory_service::proto::{
memory_service_client::MemoryServiceClient,
IngestEventRequest,
};
use memory_types::Event;
use tonic::transport::Channel;
use crate::error::ClientError;
pub struct MemoryClient {
inner: MemoryServiceClient<Channel>,
}
impl MemoryClient {
pub async fn connect(endpoint: &str) -> Result<Self, ClientError> {
let inner = MemoryServiceClient::connect(endpoint.to_string())
.await
.map_err(ClientError::Connection)?;
Ok(Self { inner })
}
pub async fn ingest(&mut self, event: Event) -> Result<String, ClientError> {
// Convert event to proto and call RPC
}
}File: crates/memory-client/src/hook_mapping.rs
use memory_types::{Event, EventType, EventRole};
#[derive(Debug, Clone)]
pub enum HookEventType {
SessionStart,
UserPromptSubmit,
AssistantResponse,
ToolUse,
Stop,
}
#[derive(Debug, Clone)]
pub struct HookEvent {
pub session_id: String,
pub event_type: HookEventType,
pub content: String,
pub timestamp: Option<i64>,
pub tool_name: Option<String>,
}
pub fn map_hook_event(hook: HookEvent) -> Event {
let event_type = match hook.event_type {
HookEventType::SessionStart => EventType::SessionStart,
HookEventType::UserPromptSubmit => EventType::UserMessage,
HookEventType::AssistantResponse => EventType::AssistantMessage,
HookEventType::ToolUse => EventType::ToolUse,
HookEventType::Stop => EventType::SessionEnd,
};
let role = match hook.event_type {
HookEventType::UserPromptSubmit => EventRole::User,
HookEventType::AssistantResponse => EventRole::Assistant,
HookEventType::ToolUse => EventRole::System,
_ => EventRole::System,
};
Event::new(hook.session_id, event_type, role, hook.content)
.with_timestamp(hook.timestamp.map(|ts|
chrono::DateTime::from_timestamp_millis(ts).unwrap_or_default()
).unwrap_or_else(chrono::Utc::now))
}File: crates/memory-client/src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ClientError {
#[error("Connection failed: {0}")]
Connection(#[from] tonic::transport::Error),
#[error("RPC failed: {0}")]
Rpc(#[from] tonic::Status),
#[error("Serialization failed: {0}")]
Serialization(String),
}Add memory-client to workspace members.
Test client connection (mock server), event mapping.
- memory-client crate compiles
- MemoryClient can connect to daemon endpoint
- ingest() method sends events via gRPC
- HookEvent maps to Event with correct types
- Tests pass for event mapping
- Phase 4 complete (IngestEvent RPC exists)
Plan created: 2026-01-30
- New crate at
crates/memory-client/ - Added to workspace members and dependencies
- Dependencies: memory-service, memory-types, tonic, tokio, thiserror, tracing, chrono, ulid
-
MemoryClient::connect(endpoint)- Connect to daemon -
MemoryClient::connect_default()- Connect to default endpoint -
MemoryClient::ingest(event)- Ingest single event via gRPC -
MemoryClient::ingest_batch(events)- Ingest multiple events - Type conversion from domain Event to proto Event
-
HookEventTypeenum with variants: SessionStart, UserPromptSubmit, AssistantResponse, ToolUse, ToolResult, Stop, SubagentStart, SubagentStop -
HookEventstruct with builder pattern methods -
map_hook_event(hook)function maps to domain Event
- Connection errors (tonic transport)
- RPC errors (tonic status)
- Serialization errors
- Invalid endpoint errors
| File | Purpose |
|---|---|
crates/memory-client/Cargo.toml |
Crate manifest |
crates/memory-client/src/lib.rs |
Module exports |
crates/memory-client/src/client.rs |
MemoryClient implementation |
crates/memory-client/src/error.rs |
Error types |
crates/memory-client/src/hook_mapping.rs |
Hook event mapping |
-
cargo build --workspacecompiles -
cargo test --workspacepasses (107 tests) - 11 new tests for memory-client
- HOOK-02: Hook handlers can call daemon's IngestEvent RPC via MemoryClient
- HOOK-03: Event types map 1:1 from hook events via map_hook_event()
Completed: 2026-01-30
Add query commands to the CLI for manual TOC navigation and testing.
- CLI-02: Query CLI for manual TOC navigation and testing
File: crates/memory-daemon/src/cli.rs
Add Query variant to Commands enum with nested subcommands:
#[derive(Subcommand, Debug)]
pub enum Commands {
// existing...
/// Query the memory system
Query {
#[command(subcommand)]
command: QueryCommands,
},
}
#[derive(Subcommand, Debug)]
pub enum QueryCommands {
/// List root TOC nodes (year level)
Root,
/// Get a specific TOC node
Node {
/// Node ID to retrieve
node_id: String,
},
/// Browse children of a node
Browse {
/// Parent node ID
parent_id: String,
/// Maximum results
#[arg(short, long, default_value = "20")]
limit: u32,
/// Continuation token for pagination
#[arg(short, long)]
token: Option<String>,
},
/// Get events in time range
Events {
/// Start time (Unix ms or ISO)
#[arg(long)]
from: i64,
/// End time (Unix ms or ISO)
#[arg(long)]
to: i64,
/// Maximum results
#[arg(short, long, default_value = "50")]
limit: u32,
},
/// Expand a grip to show context
Expand {
/// Grip ID to expand
grip_id: String,
},
}File: crates/memory-daemon/src/commands/query.rs
use memory_client::MemoryClient;
use anyhow::Result;
pub async fn handle_query(command: QueryCommands, endpoint: &str) -> Result<()> {
let mut client = MemoryClient::connect(endpoint).await?;
match command {
QueryCommands::Root => {
let nodes = client.get_toc_root().await?;
for node in nodes {
println!("{}: {} ({})", node.node_id, node.title, node.level);
}
}
QueryCommands::Node { node_id } => {
let node = client.get_node(&node_id).await?;
print_node(&node);
}
QueryCommands::Browse { parent_id, limit, token } => {
let response = client.browse_toc(&parent_id, limit, token).await?;
for child in response.children {
println!("{}: {}", child.node_id, child.title);
}
if let Some(token) = response.continuation_token {
println!("\nMore results available. Use --token {}", token);
}
}
QueryCommands::Events { from, to, limit } => {
let events = client.get_events(from, to, limit).await?;
for event in events {
println!("[{}] {}: {}", event.timestamp, event.role, &event.text[..80.min(event.text.len())]);
}
}
QueryCommands::Expand { grip_id } => {
let context = client.expand_grip(&grip_id).await?;
println!("=== Before ===");
for e in context.events_before { println!("{}", e.text); }
println!("\n=== Excerpt ===\n{}", context.excerpt);
println!("\n=== After ===");
for e in context.events_after { println!("{}", e.text); }
}
}
Ok(())
}
fn print_node(node: &TocNode) {
println!("ID: {}", node.node_id);
println!("Title: {}", node.title);
println!("Level: {:?}", node.level);
println!("Summary: {}", node.summary.as_deref().unwrap_or("-"));
println!("\nBullets:");
for bullet in &node.bullets {
println!(" • {}", bullet.text);
}
println!("\nChildren: {}", node.child_node_ids.len());
}File: crates/memory-client/src/client.rs
Add methods for query RPCs:
impl MemoryClient {
pub async fn get_toc_root(&mut self) -> Result<Vec<TocNode>, ClientError>;
pub async fn get_node(&mut self, node_id: &str) -> Result<TocNode, ClientError>;
pub async fn browse_toc(&mut self, parent_id: &str, limit: u32, token: Option<String>) -> Result<BrowseResponse, ClientError>;
pub async fn get_events(&mut self, from: i64, to: i64, limit: u32) -> Result<Vec<Event>, ClientError>;
pub async fn expand_grip(&mut self, grip_id: &str) -> Result<GripContext, ClientError>;
}Update main.rs to handle Query command.
Test CLI parsing for query commands.
-
memory-daemon query rootlists year nodes -
memory-daemon query node <id>shows node details -
memory-daemon query browse <id>paginates children -
memory-daemon query events --from --toretrieves events -
memory-daemon query expand <grip_id>shows context - Helpful error messages on connection failure
- Plan 05-01 complete (MemoryClient)
Plan created: 2026-01-30
Updated proto/memory.proto with:
-
TocLevelenum (Year, Month, Week, Day, Segment) -
TocBulletmessage (text, grip_ids) -
TocNodemessage (full node structure) -
Gripmessage (excerpt with event pointers) -
GetTocRoot,GetNode,BrowseTocRPCs -
GetEvents,ExpandGripRPCs
Created crates/memory-service/src/query.rs:
-
get_toc_root()- Returns year-level nodes -
get_node()- Fetches node by ID -
browse_toc()- Paginated child navigation -
get_events()- Time range event retrieval -
expand_grip()- Context around grip excerpt - Type conversion functions (domain ↔ proto)
- 8 unit tests
Updated MemoryServiceImpl in ingest.rs to implement all query RPCs.
Extended memory-client/src/client.rs:
-
get_toc_root()- Get year nodes -
get_node()- Get specific node -
browse_toc()- Browse children with pagination -
get_events()- Get events in time range -
expand_grip()- Expand grip context
Updated memory-daemon/src/cli.rs:
-
QueryCommandsenum with Root, Node, Browse, Events, Expand subcommands - Endpoint flag for specifying daemon address
Created handle_query() in commands.rs:
- Formatted output for all query types
- Pagination support with continuation tokens
- Error handling for connection failures
| File | Purpose |
|---|---|
proto/memory.proto |
TOC navigation and query messages |
memory-service/src/query.rs |
Query RPC implementations |
memory-service/src/ingest.rs |
Service trait implementation |
memory-client/src/client.rs |
Client query methods |
memory-daemon/src/cli.rs |
Query subcommand definitions |
memory-daemon/src/commands.rs |
Query command handler |
# List root nodes
memory-daemon query root
# Get specific node
memory-daemon query node <node_id>
# Browse children with pagination
memory-daemon query browse <parent_id> --limit 20 --token <token>
# Get events in time range
memory-daemon query events --from <ms> --to <ms> --limit 50
# Expand grip context
memory-daemon query expand <grip_id> --before 3 --after 3-
cargo build --workspacecompiles -
cargo test --workspacepasses (117 tests) - 8 new query tests in memory-service
- 2 new CLI tests in memory-daemon
- CLI-02: Query CLI for manual TOC navigation and testing
Completed: 2026-01-30
Add administrative commands for TOC rebuilding, database compaction, and status reporting.
- CLI-03: Admin commands: rebuild-toc, compact, status
File: crates/memory-daemon/src/cli.rs
Add Admin variant to Commands enum:
#[derive(Subcommand, Debug)]
pub enum Commands {
// existing...
/// Administrative commands
Admin {
#[command(subcommand)]
command: AdminCommands,
},
}
#[derive(Subcommand, Debug)]
pub enum AdminCommands {
/// Rebuild TOC from raw events
RebuildToc {
/// Start from this date (YYYY-MM-DD)
#[arg(long)]
from_date: Option<String>,
/// Dry run - show what would be done
#[arg(long)]
dry_run: bool,
},
/// Trigger RocksDB compaction
Compact {
/// Compact only specific column family
#[arg(long)]
cf: Option<String>,
},
/// Show database statistics
Stats,
}File: proto/memory.proto
// Admin messages
message RebuildTocRequest {
optional int64 from_timestamp_ms = 1;
bool dry_run = 2;
}
message RebuildTocResponse {
int32 segments_created = 1;
int32 nodes_updated = 2;
string message = 3;
}
message CompactRequest {
optional string column_family = 1;
}
message CompactResponse {
string message = 1;
}
message StatsRequest {}
message StatsResponse {
int64 event_count = 1;
int64 toc_node_count = 2;
int64 grip_count = 3;
int64 disk_usage_bytes = 4;
map<string, int64> cf_sizes = 5;
}
service MemoryService {
// existing RPCs...
// Admin RPCs
rpc RebuildToc(RebuildTocRequest) returns (RebuildTocResponse);
rpc Compact(CompactRequest) returns (CompactResponse);
rpc GetStats(StatsRequest) returns (StatsResponse);
}File: crates/memory-service/src/admin.rs
use crate::proto::{RebuildTocRequest, RebuildTocResponse, CompactRequest, CompactResponse, StatsRequest, StatsResponse};
use memory_storage::Storage;
use std::sync::Arc;
pub async fn rebuild_toc(
storage: Arc<Storage>,
request: RebuildTocRequest,
) -> Result<RebuildTocResponse, tonic::Status> {
// 1. Query events from storage starting from from_timestamp
// 2. Re-run segmentation
// 3. Re-generate TOC nodes
// 4. Return counts
}
pub async fn compact(
storage: Arc<Storage>,
request: CompactRequest,
) -> Result<CompactResponse, tonic::Status> {
// Call storage.compact() or storage.compact_cf(cf_name)
}
pub async fn get_stats(
storage: Arc<Storage>,
) -> Result<StatsResponse, tonic::Status> {
// Gather counts from each CF
// Get disk usage via std::fs
}File: crates/memory-storage/src/db.rs
impl Storage {
pub fn compact(&self) -> Result<(), StorageError> {
self.db.compact_range::<&[u8], &[u8]>(None, None);
Ok(())
}
pub fn compact_cf(&self, cf_name: &str) -> Result<(), StorageError> {
let cf = self.db.cf_handle(cf_name).ok_or(...)?;
self.db.compact_range_cf::<&[u8], &[u8]>(&cf, None, None);
Ok(())
}
pub fn get_stats(&self) -> Result<StorageStats, StorageError> {
// Count entries in each CF
// Get disk usage
}
}File: crates/memory-daemon/src/commands/admin.rs
pub async fn handle_admin(command: AdminCommands, endpoint: &str) -> Result<()> {
let mut client = MemoryClient::connect(endpoint).await?;
match command {
AdminCommands::RebuildToc { from_date, dry_run } => {
let from_ts = from_date.map(|d| parse_date(&d));
let result = client.rebuild_toc(from_ts, dry_run).await?;
println!("Segments created: {}", result.segments_created);
println!("Nodes updated: {}", result.nodes_updated);
}
AdminCommands::Compact { cf } => {
let result = client.compact(cf).await?;
println!("{}", result.message);
}
AdminCommands::Stats => {
let stats = client.get_stats().await?;
println!("Events: {}", stats.event_count);
println!("TOC Nodes: {}", stats.toc_node_count);
println!("Grips: {}", stats.grip_count);
println!("Disk Usage: {} MB", stats.disk_usage_bytes / 1024 / 1024);
}
}
Ok(())
}impl MemoryClient {
pub async fn rebuild_toc(&mut self, from_ts: Option<i64>, dry_run: bool) -> Result<RebuildTocResponse, ClientError>;
pub async fn compact(&mut self, cf: Option<String>) -> Result<CompactResponse, ClientError>;
pub async fn get_stats(&mut self) -> Result<StatsResponse, ClientError>;
}Test CLI parsing, storage stats method.
-
memory-daemon admin rebuild-tocrebuilds TOC from events -
memory-daemon admin compacttriggers compaction -
memory-daemon admin statsshows database statistics - --dry-run flag shows plan without executing
- Error handling for connection failures
- Plan 05-01 complete (MemoryClient)
- Plan 05-02 complete (query module pattern)
Plan created: 2026-01-30
Updated memory-daemon/src/cli.rs:
-
AdminCommandsenum with Stats, Compact, RebuildToc subcommands - Database path override option
- Dry-run flag for rebuild-toc
Added to memory-storage/src/db.rs:
-
compact()- Full compaction on all column families -
compact_cf(cf_name)- Compact specific column family -
get_stats()- Returns StorageStats with counts and disk usage -
StorageStatsstruct exported from crate
Created handle_admin() in commands.rs:
- Stats: Shows event/node/grip counts and disk usage
- Compact: Triggers RocksDB compaction (all or specific CF)
- RebuildToc: Placeholder for TOC rebuild with dry-run support
| File | Purpose |
|---|---|
memory-daemon/src/cli.rs |
Admin subcommand definitions |
memory-daemon/src/commands.rs |
Admin command handler |
memory-storage/src/db.rs |
compact(), get_stats() methods |
memory-storage/src/lib.rs |
StorageStats export |
# Show database statistics
memory-daemon admin stats
# Trigger full compaction
memory-daemon admin compact
# Compact specific column family
memory-daemon admin compact --cf events
# Rebuild TOC (dry run)
memory-daemon admin rebuild-toc --dry-run
# Rebuild TOC from specific date
memory-daemon admin rebuild-toc --from-date 2026-01-01-
cargo build --workspacecompiles -
cargo test --workspacepasses (116 tests) - CLI help displays correctly
- CLI-03: Admin commands: rebuild-toc, compact, status
- RebuildToc is a placeholder - full implementation would require integrating with memory-toc segmentation and summarization
- Stats opens storage directly (not via gRPC) for local admin operations
- Shellexpand used for tilde expansion in paths
Completed: 2026-01-30
Phase 5 connects the memory system to external hook handlers and provides CLI tools for querying and administration.
- HOOK-02: Hook handlers call daemon's IngestEvent RPC
- HOOK-03: Event types map 1:1 from hook events (SessionStart, UserPromptSubmit, PostToolUse, Stop, etc.)
- CLI-02: Query CLI for manual TOC navigation and testing
- CLI-03: Admin commands: rebuild-toc, compact, status
gRPC Client Library
Need to expose a client API that hook handlers can use:
// memory-client crate
pub struct MemoryClient {
inner: memory_service::MemoryServiceClient<Channel>,
}
impl MemoryClient {
pub async fn connect(endpoint: &str) -> Result<Self, Error>;
pub async fn ingest(&self, event: Event) -> Result<IngestResponse, Error>;
}Event Type Mapping
Hook events from code_agent_context_hooks:
-
SessionStart→EventType::SessionStart -
UserPromptSubmit→EventType::UserMessage -
PostToolUse→EventType::ToolUse -
Stop→EventType::SessionEnd
Mapping function:
pub fn map_hook_event(hook_event: HookEvent) -> memory_types::Event {
// Convert hook event fields to memory event
}Commands to Add
memory-daemon query root # List year nodes
memory-daemon query node <node_id> # Get specific node
memory-daemon query browse <parent_id> [--limit N] # Browse children
memory-daemon query events --from TS --to TS [--limit N] # Get events
memory-daemon query expand <grip_id> # Expand grip contextImplementation
Add Query subcommand to existing CLI with nested subcommands.
Commands to Add
memory-daemon admin rebuild-toc [--from-date DATE] # Rebuild TOC from events
memory-daemon admin compact # Trigger RocksDB compaction
memory-daemon admin stats # Show database statisticsImplementation
-
rebuild-toc: Re-run segmentation and summarization from raw events -
compact: Calldb.compact_range()on RocksDB -
stats: Show event count, TOC node count, grip count, disk usage
-
tonicfor gRPC client - Existing
memory-serviceproto definitions -
tokiofor async runtime
crates/
├── memory-client/ # New crate for client API
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs
│ └── hook_mapping.rs
├── memory-daemon/
│ └── src/
│ ├── cli.rs # Update with new commands
│ ├── commands/
│ │ ├── query.rs # New: query commands
│ │ └── admin.rs # New: admin commands
└── memory-service/
└── src/
└── admin.rs # Admin RPC implementations
| Plan | Focus | Files |
|---|---|---|
| 05-01 | Client library + hook mapping | memory-client crate |
| 05-02 | Query CLI commands | memory-daemon query subcommand |
| 05-03 | Admin CLI commands | memory-daemon admin subcommand |
Research completed: 2026-01-30