Thanks for the additional context - this clarifies the architecture significantly. Let me address your questions:
1. Templates with Shared Archival Memory
Yes, agents created from templates can share the same archival memory if you attach the same archive to each. However, there’s a current constraint worth noting: letta_v1 agents support only one archive attachment per agent (UNIQUE constraint on the join table). So your Receptionist template approach works as long as each spawned Receptionist attaches to the same single archive.
For your queue + reset pattern: that should work well. Reset clears the message buffer but preserves archival memory, so the routing table persists across resets.
2. Viability for Semantic Matching
Your reasoning is sound. Semantic search handles exactly the fuzzy cases you described:
- “CompanyA” vs “Company A Inc.” vs “Company A Limited”
- Different email domains (client → external counsel)
- “On behalf of James Brown…” variations
SQL WHERE clauses would require extensive normalization and exception handling. Archival memory’s semantic search gives you entity resolution “for free” at query time.
3. Deleting Entries by Case ID
Yes, you can delete specific passages. The pattern:
# Search for passages matching the case
passages = client.agents.passages.list(
agent_id=receptionist_id,
query_text=f"case_id: {case_id}"
)
# Delete matching passages
for passage in passages:
client.agents.passages.delete(
agent_id=receptionist_id,
passage_id=passage.id
)
Alternatively, structure your archival entries with a consistent prefix like [CASE-123] to make text-based lookup reliable.
4. Scale at 125 Entries
125 active entries is trivially small for archival memory - you won’t hit any performance issues. Even with top_k=10 retrieval, the LLM can easily disambiguate from a handful of candidate cases. Your HITL confirmation step adds an extra safety net.
5. Tags Usage Example
Tags filter searches before semantic matching. For your case:
# When inserting a new case
client.archival_memory.insert(
agent_id=receptionist_id,
content="case_id: 456, company: Acme Corp, contact: john@acme.com...",
tags=["active", "q1-2025"]
)
# When searching (only active cases)
results = client.archival_memory.search(
agent_id=receptionist_id,
query="email from external counsel regarding Acme",
tags=["active"]
)
# When closing a case - delete and optionally re-insert with different tag
# Or just delete entirely since the Worker is done
Tags are particularly useful if you want to keep closed cases for audit/reference without polluting active routing searches.
6. Is Letta the Right Tool?
For long-running, stateful email triage with 2-week to 3-month lifecycles - yes, this is exactly Letta’s sweet spot. The combination of:
- Persistent memory across interactions
- Semantic search for fuzzy entity resolution
- Agent-per-case pattern (Workers)
- Shared routing state (Receptionist’s archival)
…fits your workflow well. The HITL confirmation and pre-filtering with DeepSeek are smart additions that reduce risk.
One suggestion: consider logging the Receptionist’s routing suggestions + human corrections. Over time this gives you data to tune the system or catch systematic mismatches.