1use crate::kg_write::CachePolicy;
13use crate::service::helpers::{collect_palace_stats, list_palaces_blocking, open_palaces_blocking};
14use crate::service::{load_user_config, palace_info_from, DreamStatusPayload};
15use crate::AppState;
16use serde::Deserialize;
17use serde_json::{json, Value};
18use trusty_common::memory_core::dream::PersistedDreamStats;
19use trusty_common::memory_core::palace::{PalaceId, RoomType};
20use trusty_common::memory_core::retrieval::{
21 recall_across_palaces_with_default_embedder, recall_with_default_embedder,
22};
23use trusty_common::memory_core::store::kg::Triple;
24use trusty_common::memory_core::PalaceRegistry;
25use trusty_common::{ChatMessage, ToolDef};
26
27#[derive(Deserialize)]
30pub struct ChatBody {
31 #[serde(default)]
32 pub palace_id: Option<String>,
33 pub message: String,
34 #[serde(default)]
35 pub history: Vec<ChatMessage>,
36 #[serde(default)]
38 pub session_id: Option<String>,
39}
40
41pub(crate) const MAX_TOOL_ROUNDS: usize = 10;
47
48pub(crate) fn all_tools() -> Vec<ToolDef> {
57 vec![
58 ToolDef {
59 name: "list_palaces".into(),
60 description: "List all memory palaces on this machine with their metadata (id, name, description, counts).".into(),
61 parameters: json!({ "type": "object", "properties": {}, "required": [] }),
62 },
63 ToolDef {
64 name: "get_palace".into(),
65 description: "Get details for a specific palace by id.".into(),
66 parameters: json!({
67 "type": "object",
68 "properties": { "palace_id": { "type": "string", "description": "Palace id (kebab-case)" } },
69 "required": ["palace_id"],
70 }),
71 },
72 ToolDef {
73 name: "recall_memories".into(),
74 description: "Semantic search for memories in a palace. Returns the top-k most relevant drawers ranked by similarity to the query.".into(),
75 parameters: json!({
76 "type": "object",
77 "properties": {
78 "palace_id": { "type": "string" },
79 "query": { "type": "string", "description": "Free-text query" },
80 "top_k": { "type": "integer", "minimum": 1, "maximum": 50, "default": 5 }
81 },
82 "required": ["palace_id", "query"],
83 }),
84 },
85 ToolDef {
86 name: "list_drawers".into(),
87 description: "List all drawers (memories) in a palace, most recent first.".into(),
88 parameters: json!({
89 "type": "object",
90 "properties": { "palace_id": { "type": "string" } },
91 "required": ["palace_id"],
92 }),
93 },
94 ToolDef {
95 name: "kg_query".into(),
96 description: "Query the temporal knowledge graph for all currently-active triples whose subject matches.".into(),
97 parameters: json!({
98 "type": "object",
99 "properties": {
100 "palace_id": { "type": "string" },
101 "subject": { "type": "string" }
102 },
103 "required": ["palace_id", "subject"],
104 }),
105 },
106 ToolDef {
107 name: "get_config".into(),
108 description: "Get the trusty-memory daemon's configuration (provider, model, data root). API keys are masked.".into(),
109 parameters: json!({ "type": "object", "properties": {}, "required": [] }),
110 },
111 ToolDef {
112 name: "get_status".into(),
113 description: "Get daemon health: version, palace count, totals for drawers/vectors/triples.".into(),
114 parameters: json!({ "type": "object", "properties": {}, "required": [] }),
115 },
116 ToolDef {
117 name: "get_dream_status".into(),
118 description: "Get aggregated dreamer activity across all palaces (merged/pruned/compacted counts, last run timestamp).".into(),
119 parameters: json!({ "type": "object", "properties": {}, "required": [] }),
120 },
121 ToolDef {
122 name: "get_palace_dream_status".into(),
123 description: "Get dreamer activity stats for a specific palace.".into(),
124 parameters: json!({
125 "type": "object",
126 "properties": { "palace_id": { "type": "string" } },
127 "required": ["palace_id"],
128 }),
129 },
130 ToolDef {
131 name: "create_memory".into(),
132 description: "Store a new memory (drawer) in a palace. The content is embedded and inserted into the vector index plus the drawer table.".into(),
133 parameters: json!({
134 "type": "object",
135 "properties": {
136 "palace_id": { "type": "string" },
137 "content": { "type": "string", "description": "Verbatim memory text" },
138 "room": { "type": "string", "description": "Room name (Frontend/Backend/Testing/Planning/Documentation/Research/Configuration/Meetings/General or a custom name); defaults to General." },
139 "tags": { "type": "array", "items": { "type": "string" } },
140 "importance": { "type": "number", "minimum": 0.0, "maximum": 1.0, "default": 0.5 }
141 },
142 "required": ["palace_id", "content"],
143 }),
144 },
145 ToolDef {
146 name: "kg_assert".into(),
147 description: "Assert a knowledge-graph triple. Any prior active triple with the same (subject, predicate) is closed out (valid_to set to now) before the new one is inserted.".into(),
148 parameters: json!({
149 "type": "object",
150 "properties": {
151 "palace_id": { "type": "string" },
152 "subject": { "type": "string" },
153 "predicate": { "type": "string" },
154 "object": { "type": "string" },
155 "confidence": { "type": "number", "minimum": 0.0, "maximum": 1.0, "default": 1.0 }
156 },
157 "required": ["palace_id", "subject", "predicate", "object"],
158 }),
159 },
160 ToolDef {
161 name: "memory_recall_all".into(),
162 description: "Semantic search across ALL palaces simultaneously. Returns the top-k most relevant drawers ranked by similarity, regardless of which palace they belong to. Each result includes a `palace_id` field identifying its source.".into(),
163 parameters: json!({
164 "type": "object",
165 "properties": {
166 "q": { "type": "string", "description": "Free-text query" },
167 "top_k": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 },
168 "deep": { "type": "boolean", "default": false }
169 },
170 "required": ["q"],
171 }),
172 },
173 ]
174}
175
176pub(crate) async fn execute_tool(name: &str, args: &str, state: &AppState) -> Value {
187 let parsed: Value = serde_json::from_str(args).unwrap_or(json!({}));
188 match name {
189 "list_palaces" => execute_list_palaces(state).await,
190 "get_palace" => match parsed.get("palace_id").and_then(|v| v.as_str()) {
191 Some(id) => execute_get_palace(state, id).await,
192 None => json!({ "error": "missing required argument: palace_id" }),
193 },
194 "recall_memories" => {
195 let pid = parsed.get("palace_id").and_then(|v| v.as_str());
196 let q = parsed.get("query").and_then(|v| v.as_str());
197 let top_k = parsed.get("top_k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
198 match (pid, q) {
199 (Some(p), Some(q)) => execute_recall(state, p, q, top_k).await,
200 _ => json!({ "error": "missing required argument(s): palace_id, query" }),
201 }
202 }
203 "list_drawers" => match parsed.get("palace_id").and_then(|v| v.as_str()) {
204 Some(id) => execute_list_drawers(state, id).await,
205 None => json!({ "error": "missing required argument: palace_id" }),
206 },
207 "kg_query" => {
208 let pid = parsed.get("palace_id").and_then(|v| v.as_str());
209 let subj = parsed.get("subject").and_then(|v| v.as_str());
210 match (pid, subj) {
211 (Some(p), Some(s)) => execute_kg_query(state, p, s).await,
212 _ => json!({ "error": "missing required argument(s): palace_id, subject" }),
213 }
214 }
215 "get_config" => execute_get_config(state),
216 "get_status" => execute_get_status(state).await,
217 "get_dream_status" => execute_get_dream_status(state).await,
218 "get_palace_dream_status" => match parsed.get("palace_id").and_then(|v| v.as_str()) {
219 Some(id) => execute_get_palace_dream_status(state, id).await,
220 None => json!({ "error": "missing required argument: palace_id" }),
221 },
222 "create_memory" => {
223 let pid = parsed.get("palace_id").and_then(|v| v.as_str());
224 let content = parsed.get("content").and_then(|v| v.as_str());
225 let room = parsed.get("room").and_then(|v| v.as_str());
226 let tags: Vec<String> = parsed
227 .get("tags")
228 .and_then(|v| v.as_array())
229 .map(|arr| {
230 arr.iter()
231 .filter_map(|t| t.as_str().map(|s| s.to_string()))
232 .collect()
233 })
234 .unwrap_or_default();
235 let importance = parsed
236 .get("importance")
237 .and_then(|v| v.as_f64())
238 .map(|f| f as f32)
239 .unwrap_or(0.5);
240 match (pid, content) {
241 (Some(p), Some(c)) => {
242 execute_create_memory(state, p, c, room, tags, importance).await
243 }
244 _ => json!({ "error": "missing required argument(s): palace_id, content" }),
245 }
246 }
247 "kg_assert" => {
248 let pid = parsed.get("palace_id").and_then(|v| v.as_str());
249 let subj = parsed.get("subject").and_then(|v| v.as_str());
250 let pred = parsed.get("predicate").and_then(|v| v.as_str());
251 let obj = parsed.get("object").and_then(|v| v.as_str());
252 let conf = parsed
253 .get("confidence")
254 .and_then(|v| v.as_f64())
255 .map(|f| f as f32)
256 .unwrap_or(1.0);
257 match (pid, subj, pred, obj) {
258 (Some(p), Some(s), Some(pr), Some(o)) => {
259 execute_kg_assert(state, p, s, pr, o, conf).await
260 }
261 _ => json!({
262 "error": "missing required argument(s): palace_id, subject, predicate, object"
263 }),
264 }
265 }
266 "memory_recall_all" => {
267 let q = parsed.get("q").and_then(|v| v.as_str());
268 let top_k = parsed.get("top_k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
269 let deep = parsed
270 .get("deep")
271 .and_then(|v| v.as_bool())
272 .unwrap_or(false);
273 match q {
274 Some(q) => execute_recall_all(state, q, top_k, deep).await,
275 None => json!({ "error": "missing required argument: q" }),
276 }
277 }
278 _ => json!({ "error": format!("unknown tool: {name}") }),
279 }
280}
281
282async fn execute_list_palaces(state: &AppState) -> Value {
292 let palaces = match list_palaces_blocking(state).await {
293 Ok(v) => v,
294 Err(e) => return json!({ "error": format!("{e:#}") }),
295 };
296 let out: Vec<Value> = palaces
297 .into_iter()
298 .map(|p| {
299 let handle = state.registry.peek(&p.id);
301 let info = palace_info_from(&p, handle.as_ref());
302 serde_json::to_value(info).unwrap_or(json!({}))
303 })
304 .collect();
305 json!(out)
306}
307
308async fn execute_get_palace(state: &AppState, id: &str) -> Value {
309 let palaces = match PalaceRegistry::list_palaces(&state.data_root) {
310 Ok(v) => v,
311 Err(e) => return json!({ "error": format!("list palaces: {e:#}") }),
312 };
313 match palaces.into_iter().find(|p| p.id.0 == id) {
314 Some(p) => {
315 let handle = state.registry.open_palace(&state.data_root, &p.id).ok();
316 serde_json::to_value(palace_info_from(&p, handle.as_ref())).unwrap_or(json!({}))
317 }
318 None => json!({ "error": format!("palace not found: {id}") }),
319 }
320}
321
322async fn execute_recall(state: &AppState, palace_id: &str, query: &str, top_k: usize) -> Value {
323 let handle = match state
324 .registry
325 .open_palace(&state.data_root, &PalaceId::new(palace_id))
326 {
327 Ok(h) => h,
328 Err(e) => return json!({ "error": format!("open palace {palace_id}: {e:#}") }),
329 };
330 match recall_with_default_embedder(&handle, query, top_k).await {
331 Ok(hits) => json!(hits
332 .into_iter()
333 .map(|r| json!({
334 "drawer_id": r.drawer.id.to_string(),
335 "content": r.drawer.content(),
336 "importance": r.drawer.importance,
337 "tags": r.drawer.tags,
338 "score": r.score,
339 "layer": r.layer,
340 }))
341 .collect::<Vec<_>>()),
342 Err(e) => json!({ "error": format!("recall: {e:#}") }),
343 }
344}
345
346pub(crate) async fn execute_recall_all(
357 state: &AppState,
358 query: &str,
359 top_k: usize,
360 deep: bool,
361) -> Value {
362 let palaces = match list_palaces_blocking(state).await {
363 Ok(v) => v,
364 Err(e) => return json!({ "error": format!("{e:#}") }),
365 };
366 let handles = open_palaces_blocking(state, &palaces, "execute_recall_all").await;
369 if handles.is_empty() {
370 return json!([]);
371 }
372 match recall_across_palaces_with_default_embedder(&handles, query, top_k, deep).await {
373 Ok(results) => json!(results
374 .into_iter()
375 .map(|r| json!({
376 "palace_id": r.palace_id,
377 "drawer_id": r.result.drawer.id.to_string(),
378 "content": r.result.drawer.content(),
379 "importance": r.result.drawer.importance,
380 "tags": r.result.drawer.tags,
381 "score": r.result.score,
382 "layer": r.result.layer,
383 }))
384 .collect::<Vec<_>>()),
385 Err(e) => json!({ "error": format!("recall_across_palaces: {e:#}") }),
386 }
387}
388
389async fn execute_list_drawers(state: &AppState, palace_id: &str) -> Value {
390 let handle = match state
391 .registry
392 .open_palace(&state.data_root, &PalaceId::new(palace_id))
393 {
394 Ok(h) => h,
395 Err(e) => return json!({ "error": format!("open palace {palace_id}: {e:#}") }),
396 };
397 let drawers = handle.list_drawers(None, None, 200);
398 serde_json::to_value(drawers).unwrap_or(json!([]))
399}
400
401async fn execute_kg_query(state: &AppState, palace_id: &str, subject: &str) -> Value {
402 let handle = match state
403 .registry
404 .open_palace(&state.data_root, &PalaceId::new(palace_id))
405 {
406 Ok(h) => h,
407 Err(e) => return json!({ "error": format!("open palace {palace_id}: {e:#}") }),
408 };
409 match handle.kg.query_active(subject).await {
410 Ok(triples) => serde_json::to_value(triples).unwrap_or(json!([])),
411 Err(e) => json!({ "error": format!("kg query: {e:#}") }),
412 }
413}
414
415fn execute_get_config(state: &AppState) -> Value {
416 let cfg = load_user_config().unwrap_or_default();
417 json!({
418 "openrouter_configured": !cfg.openrouter_api_key.is_empty(),
419 "openrouter_model": cfg.openrouter_model,
420 "local_model": {
421 "enabled": cfg.local_model.enabled,
422 "base_url": cfg.local_model.base_url,
423 "model": cfg.local_model.model,
424 },
425 "data_root": state.data_root.display().to_string(),
426 })
427}
428
429async fn execute_get_status(state: &AppState) -> Value {
440 let palaces = list_palaces_blocking(state).await.unwrap_or_default();
441 let stats = collect_palace_stats(state, palaces.iter().map(|p| &p.id));
443 json!({
444 "version": state.version,
445 "palace_count": palaces.len(),
446 "default_palace": state.default_palace,
447 "data_root": state.data_root.display().to_string(),
448 "total_drawers": stats.total_drawers,
449 "total_vectors": stats.total_vectors,
450 "total_kg_triples": stats.total_kg_triples,
451 "cached_palace_count": stats.cached_palace_count,
452 })
453}
454
455pub(crate) async fn execute_get_dream_status(state: &AppState) -> Value {
456 let palaces = PalaceRegistry::list_palaces(&state.data_root).unwrap_or_default();
457 let mut out = DreamStatusPayload::default();
458 let mut latest: Option<chrono::DateTime<chrono::Utc>> = None;
459 for p in palaces {
460 let data_dir = state.data_root.join(p.id.as_str());
461 let snap = match PersistedDreamStats::load(&data_dir) {
462 Ok(Some(s)) => s,
463 _ => continue,
464 };
465 out.merged = out.merged.saturating_add(snap.stats.merged);
466 out.pruned = out.pruned.saturating_add(snap.stats.pruned);
467 out.compacted = out.compacted.saturating_add(snap.stats.compacted);
468 out.closets_updated = out
469 .closets_updated
470 .saturating_add(snap.stats.closets_updated);
471 out.duration_ms = out.duration_ms.saturating_add(snap.stats.duration_ms);
472 latest = match latest {
473 Some(t) if t >= snap.last_run_at => Some(t),
474 _ => Some(snap.last_run_at),
475 };
476 }
477 out.last_run_at = latest;
478 serde_json::to_value(out).unwrap_or(json!({}))
479}
480
481async fn execute_get_palace_dream_status(state: &AppState, palace_id: &str) -> Value {
482 let data_dir = state.data_root.join(palace_id);
483 if !data_dir.exists() {
484 return json!({ "error": format!("palace not found: {palace_id}") });
485 }
486 match PersistedDreamStats::load(&data_dir) {
487 Ok(Some(s)) => serde_json::to_value(DreamStatusPayload::from(s)).unwrap_or(json!({})),
488 Ok(None) => serde_json::to_value(DreamStatusPayload::default()).unwrap_or(json!({})),
489 Err(e) => json!({ "error": format!("read dream stats: {e:#}") }),
490 }
491}
492
493async fn execute_create_memory(
494 state: &AppState,
495 palace_id: &str,
496 content: &str,
497 room: Option<&str>,
498 tags: Vec<String>,
499 importance: f32,
500) -> Value {
501 let handle = match state
502 .registry
503 .open_palace(&state.data_root, &PalaceId::new(palace_id))
504 {
505 Ok(h) => h,
506 Err(e) => return json!({ "error": format!("open palace {palace_id}: {e:#}") }),
507 };
508 let room = room.map(RoomType::parse).unwrap_or(RoomType::General);
509 match handle
510 .remember(content.to_string(), room, tags, importance)
511 .await
512 {
513 Ok(id) => json!({ "drawer_id": id.to_string(), "status": "stored" }),
514 Err(e) => json!({ "error": format!("remember: {e:#}") }),
515 }
516}
517
518async fn execute_kg_assert(
536 state: &AppState,
537 palace_id: &str,
538 subject: &str,
539 predicate: &str,
540 object: &str,
541 confidence: f32,
542) -> Value {
543 let handle = match state
544 .registry
545 .open_palace(&state.data_root, &PalaceId::new(palace_id))
546 {
547 Ok(h) => h,
548 Err(e) => return json!({ "error": format!("open palace {palace_id}: {e:#}") }),
549 };
550 let triple = Triple {
551 subject: subject.to_string(),
552 predicate: predicate.to_string(),
553 object: object.to_string(),
554 valid_from: chrono::Utc::now(),
555 valid_to: None,
556 confidence,
557 provenance: Some("chat:assistant".to_string()),
558 };
559 match crate::kg_write::assert_triple(state, &handle, triple, CachePolicy::Inline).await {
562 Ok(_) => json!({ "status": "asserted" }),
563 Err(e) => json!({ "error": format!("{e:#}") }),
564 }
565}