personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""CLI command for chatting with the journal agent."""
5
6from __future__ import annotations
7
8import argparse
9import sys
10import threading
11
12from think.callosum import CallosumConnection
13from think.cortex_client import cortex_request, read_agent_events
14from think.utils import setup_cli
15
16
17def main() -> None:
18 """Entry point for ``sol chat``."""
19 parser = argparse.ArgumentParser(
20 prog="sol chat",
21 description="Chat with your journal",
22 )
23 parser.add_argument("message", nargs="*", help="Chat message")
24 parser.add_argument("--facet", help="Facet context")
25 parser.add_argument("--provider", help="AI provider override")
26 parser.add_argument(
27 "--talent", default="unified", help="Talent agent name (default: unified)"
28 )
29 args = setup_cli(parser)
30
31 from think.awareness import ensure_sol_directory
32
33 ensure_sol_directory()
34
35 if not args.message:
36 parser.print_help()
37 return
38
39 message = " ".join(args.message).strip()
40
41 config: dict[str, str] = {}
42 if args.facet:
43 config["facet"] = args.facet
44
45 agent_id = cortex_request(
46 prompt=message,
47 name=args.talent,
48 provider=args.provider,
49 config=config if config else None,
50 )
51 if agent_id is None:
52 print(
53 "Error: failed to connect to cortex (is the stack running?)",
54 file=sys.stderr,
55 )
56 sys.exit(1)
57
58 result: dict[str, str] = {}
59 done = threading.Event()
60 listener = CallosumConnection()
61
62 def on_event(msg: dict) -> None:
63 if msg.get("tract") != "cortex":
64 return
65 if msg.get("agent_id") != agent_id:
66 return
67
68 event_type = msg.get("event")
69 if event_type == "start":
70 if args.verbose:
71 print(
72 f"Agent started (model={msg.get('model')}, provider={msg.get('provider')})",
73 file=sys.stderr,
74 )
75 elif event_type == "thinking":
76 if args.verbose:
77 print(
78 f"Thinking: {msg.get('summary', '')[:200]}",
79 file=sys.stderr,
80 )
81 elif event_type == "tool_start":
82 if args.verbose:
83 print(f"Tool: {msg.get('tool', 'unknown')}", file=sys.stderr)
84 elif event_type == "tool_end":
85 if args.verbose:
86 print(f"Tool done: {msg.get('tool', '')}", file=sys.stderr)
87 elif event_type == "finish":
88 result["text"] = msg.get("result", "")
89 done.set()
90 elif event_type == "error":
91 result["error"] = msg.get("error", "Unknown error")
92 done.set()
93
94 listener.start(callback=on_event)
95
96 if not args.verbose:
97 print("Thinking...", end="", file=sys.stderr, flush=True)
98
99 try:
100 done.wait(timeout=600)
101 except KeyboardInterrupt:
102 print("\nInterrupted.", file=sys.stderr)
103 listener.stop()
104 sys.exit(1)
105
106 listener.stop()
107
108 if not args.verbose:
109 print("\r \r", end="", file=sys.stderr, flush=True)
110
111 if "error" in result:
112 print(f"Error: {result['error']}", file=sys.stderr)
113 sys.exit(1)
114
115 if "text" in result and result["text"].strip():
116 print(result["text"])
117 return
118
119 if "text" in result:
120 print("Error: agent returned an empty result.", file=sys.stderr)
121 sys.exit(1)
122
123 try:
124 events = read_agent_events(agent_id)
125 for event in reversed(events):
126 event_type = event.get("event")
127 if event_type == "finish":
128 text = event.get("result", "")
129 if str(text).strip():
130 print(text)
131 return
132 print("Error: agent returned an empty result.", file=sys.stderr)
133 sys.exit(1)
134 if event_type == "error":
135 print(
136 f"Error: {event.get('error', 'Unknown error')}",
137 file=sys.stderr,
138 )
139 sys.exit(1)
140 except FileNotFoundError:
141 pass
142
143 print("Error: request timed out.", file=sys.stderr)
144 sys.exit(1)