Skip to content

Commit d763a12

Browse files
committed
Implement pluggable memory system with SQLite driver and associated configurations
1 parent 51e91a2 commit d763a12

13 files changed

Lines changed: 1035 additions & 6 deletions

File tree

cagent-schema.json

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@
5151
"$ref": "#/definitions/RAGConfig"
5252
}
5353
},
54+
"memory": {
55+
"type": "object",
56+
"description": "Map of memory scope configurations for pluggable memory backends",
57+
"additionalProperties": {
58+
"$ref": "#/definitions/MemoryConfig"
59+
}
60+
},
5461
"metadata": {
5562
"$ref": "#/definitions/Metadata",
5663
"description": "Configuration metadata"
@@ -277,6 +284,13 @@
277284
"type": "string"
278285
}
279286
},
287+
"memory": {
288+
"type": "array",
289+
"description": "List of memory scopes to use for this agent",
290+
"items": {
291+
"type": "string"
292+
}
293+
},
280294
"hooks": {
281295
"$ref": "#/definitions/HooksConfig",
282296
"description": "Lifecycle hooks for executing shell commands at various points in the agent's execution"
@@ -1297,6 +1311,80 @@
12971311
"strategies"
12981312
],
12991313
"additionalProperties": false
1314+
},
1315+
"MemoryConfig": {
1316+
"type": "object",
1317+
"description": "Memory scope configuration for pluggable memory backends supporting long-term (RAG-style) and short-term (whiteboard) strategies",
1318+
"required": ["kind"],
1319+
"properties": {
1320+
"kind": {
1321+
"type": "string",
1322+
"description": "Memory backend type",
1323+
"enum": ["sqlite", "neo4j", "qdrant", "redis", "whiteboard"]
1324+
},
1325+
"strategy": {
1326+
"type": "string",
1327+
"description": "Memory strategy: long_term (persistent RAG-style) or short_term (ephemeral whiteboard)",
1328+
"enum": ["long_term", "short_term"]
1329+
},
1330+
"description": {
1331+
"type": "string",
1332+
"description": "Human-readable description of this memory scope"
1333+
},
1334+
"path": {
1335+
"type": "string",
1336+
"description": "File path for file-based backends (sqlite)"
1337+
},
1338+
"ttl": {
1339+
"type": "integer",
1340+
"description": "Time-to-live in seconds for ephemeral memory (whiteboard, redis)",
1341+
"minimum": 0
1342+
},
1343+
"mode": {
1344+
"type": "string",
1345+
"description": "Access mode for the memory",
1346+
"enum": ["read_write", "read_only", "append_only"]
1347+
},
1348+
"connection": {
1349+
"type": "object",
1350+
"description": "Connection details for remote backends",
1351+
"properties": {
1352+
"url": {
1353+
"type": "string",
1354+
"description": "Connection URL for the memory backend"
1355+
},
1356+
"database": {
1357+
"type": "string",
1358+
"description": "Database name (for backends supporting multiple databases)"
1359+
},
1360+
"collection": {
1361+
"type": "string",
1362+
"description": "Collection/table name (for vector stores)"
1363+
},
1364+
"auth": {
1365+
"type": "object",
1366+
"description": "Authentication credentials",
1367+
"properties": {
1368+
"username": {
1369+
"type": "string",
1370+
"description": "Username for basic authentication"
1371+
},
1372+
"password": {
1373+
"type": "string",
1374+
"description": "Password for basic authentication"
1375+
},
1376+
"token": {
1377+
"type": "string",
1378+
"description": "Token for token-based authentication"
1379+
}
1380+
},
1381+
"additionalProperties": false
1382+
}
1383+
},
1384+
"additionalProperties": false
1385+
}
1386+
},
1387+
"additionalProperties": false
13001388
}
13011389
}
13021390
}

examples/memory_demo.yaml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env cagent run
2+
version: "3"
3+
4+
metadata:
5+
author: Memory System Demo
6+
readme: |
7+
Demonstrates the pluggable memory system with both long-term and short-term memory.
8+
9+
Memory strategies:
10+
- **Long-term (SQLite)**: Persistent memory for user facts
11+
- **Short-term (Whiteboard)**: Ephemeral shared context for multi-agent collaboration
12+
13+
Future backends (config-ready, implementation pending):
14+
- Neo4j GraphRAG for knowledge graphs
15+
- Qdrant for vector-based semantic search
16+
- Redis for distributed whiteboard
17+
18+
memory:
19+
# Long-term persistent memory (current implementation)
20+
user_facts:
21+
kind: sqlite
22+
strategy: long_term
23+
path: ./memory/user_facts.db
24+
description: "Persistent memory about the user"
25+
26+
# Short-term shared whiteboard (implementation pending)
27+
# team_whiteboard:
28+
# kind: whiteboard
29+
# strategy: short_term
30+
# ttl: 3600 # 1 hour expiry
31+
# description: "Shared context for agent collaboration"
32+
33+
# GraphRAG with Neo4j (implementation pending)
34+
# knowledge_graph:
35+
# kind: neo4j
36+
# strategy: long_term
37+
# connection:
38+
# url: bolt://localhost:7687
39+
# database: cagent
40+
# auth:
41+
# username: neo4j
42+
# password: ${NEO4J_PASSWORD}
43+
# description: "Knowledge graph for semantic relationships"
44+
45+
agents:
46+
root:
47+
model: anthropic/claude-sonnet-4-5
48+
description: "Assistant with long-term memory"
49+
instruction: |
50+
You are a helpful assistant with memory capabilities.
51+
52+
Use the memory tool to remember things about the user.
53+
Before responding, always check memories to personalize your responses.
54+
memory:
55+
- user_facts
56+
toolsets:
57+
- type: think
58+
59+
# Multi-agent example with shared whiteboard (pending whiteboard implementation)
60+
# coordinator:
61+
# model: anthropic/claude-sonnet-4-5
62+
# description: "Coordinates team using shared whiteboard"
63+
# memory:
64+
# - team_whiteboard # Shared with sub-agents
65+
# - user_facts # Personal long-term memory
66+
# sub_agents:
67+
# - researcher
68+
# - writer
69+
#
70+
# researcher:
71+
# model: openai/gpt-4o
72+
# description: "Research specialist"
73+
# memory:
74+
# - team_whiteboard # Shared whiteboard
75+
#
76+
# writer:
77+
# model: anthropic/claude-sonnet-4-5
78+
# description: "Writing specialist"
79+
# memory:
80+
# - team_whiteboard # Shared whiteboard

pkg/config/latest/types.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Config struct {
1818
Providers map[string]ProviderConfig `json:"providers,omitempty"`
1919
Models map[string]ModelConfig `json:"models,omitempty"`
2020
RAG map[string]RAGConfig `json:"rag,omitempty"`
21+
Memory map[string]MemoryConfig `json:"memory,omitempty"`
2122
Metadata Metadata `json:"metadata,omitempty"`
2223
Permissions *PermissionsConfig `json:"permissions,omitempty"`
2324
}
@@ -119,6 +120,7 @@ type AgentConfig struct {
119120
SubAgents []string `json:"sub_agents,omitempty"`
120121
Handoffs []string `json:"handoffs,omitempty"`
121122
RAG []string `json:"rag,omitempty"`
123+
Memory []string `json:"memory,omitempty"`
122124
AddDate bool `json:"add_date,omitempty"`
123125
AddEnvironmentInfo bool `json:"add_environment_info,omitempty"`
124126
CodeModeTools bool `json:"code_mode_tools,omitempty"`
@@ -1003,3 +1005,45 @@ func (h *HookDefinition) validate(prefix string, index int) error {
10031005

10041006
return nil
10051007
}
1008+
1009+
// MemoryConfig represents a named memory scope configuration.
1010+
// Memory scopes define how agents store and retrieve information.
1011+
type MemoryConfig struct {
1012+
// Kind specifies the memory backend type: sqlite, whiteboard, neo4j, qdrant, redis.
1013+
Kind string `json:"kind"`
1014+
// Strategy specifies the memory strategy: "long_term" (persistent RAG-style) or "short_term" (ephemeral whiteboard).
1015+
// Default is "long_term" for sqlite/neo4j/qdrant, "short_term" for whiteboard/redis.
1016+
Strategy string `json:"strategy,omitempty"`
1017+
// Description is an optional human-readable description of this memory scope.
1018+
Description string `json:"description,omitempty"`
1019+
// Connection holds connection details for remote backends.
1020+
Connection *MemoryConnectionConfig `json:"connection,omitempty"`
1021+
// Path is the file path for file-based backends like sqlite.
1022+
Path string `json:"path,omitempty"`
1023+
// TTL is the time-to-live in seconds for ephemeral memory (e.g., whiteboard). 0 means no expiry.
1024+
TTL int `json:"ttl,omitempty"`
1025+
// Mode specifies the access mode: "read_write" (default), "read_only", or "append_only" (event-log style).
1026+
Mode string `json:"mode,omitempty"`
1027+
}
1028+
1029+
// MemoryConnectionConfig holds connection details for remote memory backends.
1030+
type MemoryConnectionConfig struct {
1031+
// URL is the connection URL for the memory backend.
1032+
URL string `json:"url"`
1033+
// Database is the database name (for backends that support multiple databases).
1034+
Database string `json:"database,omitempty"`
1035+
// Collection is the collection/table name (for vector stores).
1036+
Collection string `json:"collection,omitempty"`
1037+
// Auth holds authentication credentials.
1038+
Auth *MemoryAuthConfig `json:"auth,omitempty"`
1039+
}
1040+
1041+
// MemoryAuthConfig holds authentication credentials for memory backends.
1042+
type MemoryAuthConfig struct {
1043+
// Username for basic authentication.
1044+
Username string `json:"username,omitempty"`
1045+
// Password for basic authentication.
1046+
Password string `json:"password,omitempty"`
1047+
// Token for token-based authentication.
1048+
Token string `json:"token,omitempty"`
1049+
}

pkg/config/latest/validate.go

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package latest
22

33
import (
44
"errors"
5+
"fmt"
56
"strings"
67
)
78

@@ -16,8 +17,7 @@ func (t *Config) UnmarshalYAML(unmarshal func(any) error) error {
1617
}
1718

1819
func (t *Config) validate() error {
19-
for i := range t.Agents {
20-
agent := t.Agents[i]
20+
for agentName, agent := range t.Agents {
2121
for j := range agent.Toolsets {
2222
if err := agent.Toolsets[j].validate(); err != nil {
2323
return err
@@ -28,6 +28,18 @@ func (t *Config) validate() error {
2828
return err
2929
}
3030
}
31+
// Validate agent memory references exist in top-level memory map
32+
for _, memRef := range agent.Memory {
33+
if _, exists := t.Memory[memRef]; !exists {
34+
return fmt.Errorf("agent %q: references undefined memory %q", agentName, memRef)
35+
}
36+
}
37+
}
38+
39+
for name, mem := range t.Memory {
40+
if err := mem.validate(name); err != nil {
41+
return err
42+
}
3143
}
3244

3345
return nil
@@ -123,3 +135,89 @@ func (t *Toolset) validate() error {
123135

124136
return nil
125137
}
138+
139+
func (m *MemoryConfig) validate(name string) error {
140+
if m.Kind == "" {
141+
return fmt.Errorf("memory %q: kind is required", name)
142+
}
143+
144+
validKinds := map[string]bool{
145+
"sqlite": true,
146+
"neo4j": true,
147+
"qdrant": true,
148+
"redis": true,
149+
"whiteboard": true,
150+
}
151+
if !validKinds[m.Kind] {
152+
return fmt.Errorf("memory %q: invalid kind %q, must be one of: sqlite, neo4j, qdrant, redis, whiteboard", name, m.Kind)
153+
}
154+
155+
// Validate strategy if provided
156+
if m.Strategy != "" {
157+
validStrategies := map[string]bool{
158+
"long_term": true, // Persistent RAG-style memory (sqlite, neo4j, qdrant)
159+
"short_term": true, // Ephemeral whiteboard-style memory (whiteboard, redis)
160+
}
161+
if !validStrategies[m.Strategy] {
162+
return fmt.Errorf("memory %q: invalid strategy %q, must be one of: long_term, short_term", name, m.Strategy)
163+
}
164+
165+
// Validate strategy matches kind semantics
166+
longTermKinds := map[string]bool{"sqlite": true, "neo4j": true, "qdrant": true}
167+
shortTermKinds := map[string]bool{"whiteboard": true, "redis": true}
168+
169+
if m.Strategy == "long_term" && shortTermKinds[m.Kind] {
170+
return fmt.Errorf("memory %q: kind %q is not suitable for long_term strategy (use sqlite, neo4j, or qdrant)", name, m.Kind)
171+
}
172+
if m.Strategy == "short_term" && longTermKinds[m.Kind] && m.Kind != "redis" {
173+
// Note: redis can be used for both strategies; sqlite/neo4j/qdrant are long-term only
174+
if m.Kind != "redis" {
175+
return fmt.Errorf("memory %q: kind %q is not suitable for short_term strategy (use whiteboard or redis)", name, m.Kind)
176+
}
177+
}
178+
}
179+
180+
// Validate mode if provided
181+
if m.Mode != "" {
182+
validModes := map[string]bool{
183+
"read_write": true, // Default: full read/write access
184+
"read_only": true, // Read-only access (useful for shared knowledge bases)
185+
"append_only": true, // Append-only (event-log style, no updates/deletes)
186+
}
187+
if !validModes[m.Mode] {
188+
return fmt.Errorf("memory %q: invalid mode %q, must be one of: read_write, read_only, append_only", name, m.Mode)
189+
}
190+
}
191+
192+
// Validate auth completeness if present (check Connection is not nil first)
193+
if m.Connection != nil && m.Connection.Auth != nil {
194+
auth := m.Connection.Auth
195+
hasUserPass := auth.Username != "" || auth.Password != ""
196+
hasToken := auth.Token != ""
197+
198+
if hasUserPass && hasToken {
199+
return fmt.Errorf("memory %q: auth must use either username/password or token, not both", name)
200+
}
201+
if hasUserPass && (auth.Username == "" || auth.Password == "") {
202+
return fmt.Errorf("memory %q: auth requires both username and password when using user/password auth", name)
203+
}
204+
}
205+
206+
// For sqlite, path is required
207+
if m.Kind == "sqlite" && m.Path == "" {
208+
return fmt.Errorf("memory %q: sqlite requires a path", name)
209+
}
210+
211+
// For remote backends, connection URL is typically required
212+
remoteKinds := map[string]bool{"neo4j": true, "qdrant": true, "redis": true}
213+
if remoteKinds[m.Kind] && (m.Connection == nil || m.Connection.URL == "") {
214+
return fmt.Errorf("memory %q: %s requires connection.url", name, m.Kind)
215+
}
216+
217+
// TTL validation: only meaningful for short-term/ephemeral memory
218+
if m.TTL > 0 && m.Kind != "whiteboard" && m.Kind != "redis" {
219+
return fmt.Errorf("memory %q: ttl is only supported for whiteboard and redis kinds", name)
220+
}
221+
222+
return nil
223+
}

0 commit comments

Comments
 (0)