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