Skip to content
359 changes: 41 additions & 318 deletions README.md

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions benches/context_assembly_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct BenchVectorStore;
impl engram::core::VectorStore for BenchVectorStore {
async fn insert(&self, _session_id: &str, _text: &str, _embedding: Vec<f32>, _message_id: &str) -> Result<(), engram::core::StoreError> { Ok(()) }
async fn search(&self, _session_id: &str, _query_embedding: &[f32], _top_k: usize) -> Result<Vec<engram::core::SearchResult>, engram::core::StoreError> {
Ok(vec![engram::core::SearchResult { text: "Relevant memory".to_string(), score: 0.9 }; 10])
Ok((0..10).map(|i| engram::core::SearchResult { memory_id: format!("m{i}"), text: "Relevant memory".to_string(), score: 0.9 }).collect())
}
async fn delete_session(&self, _session_id: &str) -> Result<(), engram::core::StoreError> { Ok(()) }
}
Expand All @@ -50,12 +50,20 @@ impl engram::core::CoreMemoryStore for BenchCoreMemoryStore {
}

fn make_assembler(messages: Vec<Message>) -> ContextAssembler {
use engram::adaptive::scoring::InMemoryMemoryScoreStore;
use engram::adaptive::retrieval_context::InMemoryRetrievalContextStore;
ContextAssembler::new(
Arc::new(BenchShortTermMemory { messages }),
Arc::new(BenchVectorStore),
Arc::new(BenchEmbeddingProvider),
Arc::new(OpenAITokenCounter::new().unwrap()),
Arc::new(BenchCoreMemoryStore),
Arc::new(InMemoryMemoryScoreStore::default()),
Arc::new(InMemoryRetrievalContextStore::default()),
None,
0.0,
1,
Arc::new(engram::metrics::AppMetrics::new().unwrap()),
)
}

Expand Down Expand Up @@ -83,7 +91,7 @@ fn bench_context_assembly(c: &mut Criterion) {
let assembler = make_assembler(make_messages(*size));
group.bench_with_input(BenchmarkId::new(*name, size), size, |b, _| {
b.iter(|| {
rt.block_on(assembler.assemble_context("session-1", 2048, 0.7, 10)).unwrap();
rt.block_on(assembler.assemble_context("session-1", 2048, 0.7, 10)).unwrap().0;
})
});
}
Expand Down
20 changes: 19 additions & 1 deletion benches/e2e_throughput.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ impl BenchmarkHarness {
let lancedb_dir = tempfile::tempdir()?;

let short_term_memory: Arc<dyn ShortTermMemory> = Arc::new(InMemoryStore::default());
let vector_store: Arc<dyn VectorStore> = Arc::new(LanceDBStore::connect(lancedb_dir.path()).await?);
let vector_store: Arc<dyn VectorStore> = Arc::new(LanceDBStore::connect(lancedb_dir.path(), 1536).await?);
let embedding_provider: Arc<dyn EmbeddingProvider> = Arc::new(MockEmbeddingProvider);
let token_counter: Arc<dyn TokenCounter> = Arc::new(OpenAITokenCounter::new()?);
let core_memory_store: Arc<dyn CoreMemoryStore> = Arc::new(InMemoryCoreMemoryStore::default());
Expand All @@ -172,6 +172,12 @@ impl BenchmarkHarness {
embedding_provider.clone(),
token_counter.clone(),
core_memory_store.clone(),
Arc::new(engram::adaptive::scoring::InMemoryMemoryScoreStore::default()),
Arc::new(engram::adaptive::retrieval_context::InMemoryRetrievalContextStore::default()),
None,
0.0,
1,
metrics.clone(),
));
let (embedding_job_sender, receiver) = embedding_job_channel(config.channel_size);

Expand Down Expand Up @@ -200,6 +206,18 @@ impl BenchmarkHarness {
global_graph: Arc::new(tokio::sync::RwLock::new(
engram::knowledge::global::GlobalGraph::new(),
)),
consolidated: Arc::new(engram::consolidation::store::InMemoryConsolidatedStore::default()),
consolidation_tx: {
let (tx, mut rx) = engram::consolidation::scheduler::consolidation_job_channel(16);
tokio::spawn(async move { while rx.recv().await.is_some() {} });
tx
},
checkpoints: Arc::new(engram::history::checkpoint::InMemoryCheckpointStore::default()),
reconstructor: None,
score_store: Arc::new(engram::adaptive::scoring::InMemoryMemoryScoreStore::default()),
retrieval_ctx_store: Arc::new(engram::adaptive::retrieval_context::InMemoryRetrievalContextStore::default()),
retrieval_learning_rate: 0.1,
retrieval_set_credit_factor: 0.2,
});

let _worker_handles = spawn_embedding_workers(
Expand Down
11 changes: 9 additions & 2 deletions benches/real_store_latency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fn bench_real_store_latency(c: &mut Criterion) {
let tempdir = tempdir().unwrap();

let lancedb_store =
Arc::new(LanceDBStore::connect(tempdir.path()).await.unwrap());
Arc::new(LanceDBStore::connect(tempdir.path(), 1536).await.unwrap());

let token_counter =
Arc::new(OpenAITokenCounter::new().unwrap());
Expand All @@ -80,6 +80,12 @@ fn bench_real_store_latency(c: &mut Criterion) {
embedding_provider,
token_counter,
core_memory_store,
Arc::new(engram::adaptive::scoring::InMemoryMemoryScoreStore::default()),
Arc::new(engram::adaptive::retrieval_context::InMemoryRetrievalContextStore::default()),
None,
0.0,
1,
Arc::new(engram::metrics::AppMetrics::new().unwrap()),
));

(
Expand Down Expand Up @@ -136,7 +142,8 @@ fn bench_real_store_latency(c: &mut Criterion) {
assembler
.assemble_context(&sid, 8000, 0.7, 10)
.await
.unwrap();
.unwrap()
.0;
});
});
});
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.cluster.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ services:
SUMMARIZER: "mock"
CONSOLIDATION_THRESHOLD: "5"
CONSOLIDATION_TARGET_WINDOW: "2"
RETRIEVAL_LEARNING_RATE: "0.3"
RETRIEVAL_CANDIDATE_MULTIPLIER: "3"
RETRIEVAL_FEEDBACK_WEIGHT: "2.0"
RETRIEVAL_SET_CREDIT_FACTOR: "0.1"
RETRIEVAL_CONTEXT_TTL_SECS: "300"
RUST_LOG: "info,openraft=debug"
ports:
- "3000:3000"
Expand Down Expand Up @@ -62,6 +67,11 @@ services:
SUMMARIZER: "mock"
CONSOLIDATION_THRESHOLD: "5"
CONSOLIDATION_TARGET_WINDOW: "2"
RETRIEVAL_LEARNING_RATE: "0.3"
RETRIEVAL_CANDIDATE_MULTIPLIER: "3"
RETRIEVAL_FEEDBACK_WEIGHT: "2.0"
RETRIEVAL_SET_CREDIT_FACTOR: "0.1"
RETRIEVAL_CONTEXT_TTL_SECS: "300"
RUST_LOG: "info,openraft=debug"
ports:
- "3001:3000"
Expand Down Expand Up @@ -91,6 +101,11 @@ services:
SUMMARIZER: "mock"
CONSOLIDATION_THRESHOLD: "5"
CONSOLIDATION_TARGET_WINDOW: "2"
RETRIEVAL_LEARNING_RATE: "0.3"
RETRIEVAL_CANDIDATE_MULTIPLIER: "3"
RETRIEVAL_FEEDBACK_WEIGHT: "2.0"
RETRIEVAL_SET_CREDIT_FACTOR: "0.1"
RETRIEVAL_CONTEXT_TTL_SECS: "300"
RUST_LOG: "info,openraft=debug"
ports:
- "3002:3000"
Expand Down
73 changes: 72 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This document describes every REST endpoint exposed by Engram. All endpoints are
| GET | /knowledge/global/conflicts | list conflicting facts across sessions |
| GET | /sessions/{session_id}/summaries | list consolidated summaries for a session |
| POST | /sessions/{session_id}/consolidate | manually trigger consolidation (leader only) |
| POST | /sessions/{session_id}/feedback | report retrieval outcome to update learned scores |
| GET | /sessions/{session_id}/at | reconstruct session state at a past point (cluster) |
| GET | /sessions/{session_id}/history | list the session's committed timeline (cluster) |
| GET | /sessions/{session_id}/diff | structural diff between two points (cluster) |
Expand Down Expand Up @@ -128,10 +129,13 @@ Get the assembled context for a session.
- body:
```json
{
"context": "core memories:\n- user name is alex\n\nconversation:\nuser: hello\n..."
"context": "core memories:\n- user name is alex\n\nconversation:\nuser: hello\n...",
"query_id": "a1b2c3d4-5678-1234-9abc-def012345678"
}
```

`query_id` is a node-local identifier for this retrieval. Pass it to `POST /sessions/{id}/feedback` to report whether the retrieved memories were useful. It expires after `RETRIEVAL_CONTEXT_TTL_SECS` (default 300 seconds); feedback on an expired or unknown `query_id` is a no-op.

**error responses:**
- 400: invalid query parameters
- 404: session not found
Expand Down Expand Up @@ -816,6 +820,73 @@ curl -X POST http://localhost:3000/sessions/{session_id}/consolidate

---

## adaptive retrieval endpoints

Stage 6 adds a feedback loop between context retrieval and memory scoring. Each call to `GET /sessions/{id}/context` returns a `query_id`. Callers report what happened after that retrieval, and Engram updates per-memory learned scores through Raft consensus so all nodes stay consistent.

---

## POST /sessions/{session_id}/feedback

Reports the outcome of a previous retrieval. The server resolves the `query_id` to the memories that were returned, computes updated scores via a bounded EMA, and replicates each score change as an `ApplyFeedback` Raft command.

Feedback on an unknown or expired `query_id` is a clean no-op. It never returns an error for this case; the response indicates whether scores were actually updated.

**path parameters:**
- `session_id` (string): session identifier

**request body:**
```json
{
"query_id": "a1b2c3d4-5678-1234-9abc-def012345678",
"outcome": "positive",
"memory_feedback": [
{ "memory_id": "m1", "signal": "positive" },
{ "memory_id": "m2", "signal": "negative", "confidence": 0.8 }
]
}
```

`query_id` and `outcome` are required. `outcome` must be `"positive"` or `"negative"`.

`memory_feedback` is optional. When provided, each listed memory receives a full-magnitude score update (`reward = ±1`). When omitted, all memories in the retrieved set receive a weaker set-credit update (magnitude `RETRIEVAL_SET_CREDIT_FACTOR`). `confidence` is accepted but ignored in the current scoring rule; it is forward-compatible.

**success response:**
- status: 200
- body:
```json
{
"applied": true,
"scores_updated": 2
}
```

`applied` is `false` and `scores_updated` is `0` when the `query_id` is unknown or expired.

**error responses:**
- 400: invalid request body or unknown `outcome` value
- 307: redirect to leader (cluster mode, follower received the request)
- 500: Raft replication failed

**example:**
```sh
# explicit per-memory feedback
curl -X POST http://localhost:3000/sessions/{session_id}/feedback \
-H 'content-type: application/json' \
-d '{
"query_id": "a1b2c3d4-5678-1234-9abc-def012345678",
"outcome": "positive",
"memory_feedback": [{"memory_id": "m1", "signal": "positive"}]
}'

# outcome-only (set-credit) feedback
curl -X POST http://localhost:3000/sessions/{session_id}/feedback \
-H 'content-type: application/json' \
-d '{"query_id": "a1b2c3d4-5678-1234-9abc-def012345678", "outcome": "negative"}'
```

---

## temporal (cognitive version control) endpoints

Stage 5 adds time-travel reads and named checkpoints. Reconstruction replays this node's own Raft log against its nearest retained snapshot into a throwaway in-memory state machine. The log records committed *results*, so replay is deterministic and read-only. Reads need no consensus and run node-locally; only creating a checkpoint is a cluster mutation and routes through Raft.
Expand Down
Loading
Loading