commonmemo 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. commonmemo/__init__.py +171 -0
  2. commonmemo/__main__.py +7 -0
  3. commonmemo/agent_tools.py +240 -0
  4. commonmemo/cli.py +158 -0
  5. commonmemo/context.py +284 -0
  6. commonmemo/dates.py +138 -0
  7. commonmemo/engines/__init__.py +44 -0
  8. commonmemo/engines/_ranking.py +44 -0
  9. commonmemo/engines/base.py +128 -0
  10. commonmemo/engines/graphiti.py +337 -0
  11. commonmemo/engines/local.py +316 -0
  12. commonmemo/engines/mem0.py +288 -0
  13. commonmemo/environment.py +348 -0
  14. commonmemo/events.py +543 -0
  15. commonmemo/extraction.py +221 -0
  16. commonmemo/facts.py +219 -0
  17. commonmemo/history.py +366 -0
  18. commonmemo/hookclient.py +114 -0
  19. commonmemo/hooks.py +292 -0
  20. commonmemo/hookserver.py +166 -0
  21. commonmemo/inspector.py +228 -0
  22. commonmemo/inspector_ui.py +487 -0
  23. commonmemo/integrations/__init__.py +2 -0
  24. commonmemo/integrations/commonadk.py +245 -0
  25. commonmemo/knowledge.py +1216 -0
  26. commonmemo/learning.py +333 -0
  27. commonmemo/mcp.py +285 -0
  28. commonmemo/memory.py +985 -0
  29. commonmemo/opencode_plugin.js +96 -0
  30. commonmemo/otel.py +119 -0
  31. commonmemo/procedures.py +330 -0
  32. commonmemo/projections.py +480 -0
  33. commonmemo/ranking.py +144 -0
  34. commonmemo/reins.py +342 -0
  35. commonmemo/resume.py +286 -0
  36. commonmemo/runs.py +49 -0
  37. commonmemo/said.py +251 -0
  38. commonmemo/session.py +541 -0
  39. commonmemo/sqldb.py +228 -0
  40. commonmemo/stores/__init__.py +29 -0
  41. commonmemo/stores/base.py +189 -0
  42. commonmemo/stores/memory.py +199 -0
  43. commonmemo/stores/sql.py +423 -0
  44. commonmemo/stores/sqlite.py +7 -0
  45. commonmemo/tool_memory.py +166 -0
  46. commonmemo/traces.py +160 -0
  47. commonmemo/views.py +218 -0
  48. commonmemo-0.0.1.dist-info/METADATA +840 -0
  49. commonmemo-0.0.1.dist-info/RECORD +52 -0
  50. commonmemo-0.0.1.dist-info/WHEEL +4 -0
  51. commonmemo-0.0.1.dist-info/entry_points.txt +2 -0
  52. commonmemo-0.0.1.dist-info/licenses/LICENSE +201 -0
commonmemo/__init__.py ADDED
@@ -0,0 +1,171 @@
1
+ """CommonMemo -- framework-neutral memory for agents and workflows.
2
+
3
+ Four tiers behind one `Memory` handle, plus structured kinds (tool memory,
4
+ world facts, feedback, negatives, resume points) kept beside them:
5
+
6
+ - **Log** -- everything that happened, in order, never edited (owned by CommonMemo)
7
+ - **Working** -- what the current conversation or run has in hand (owned by CommonMemo)
8
+ - **Recaps** -- short summaries of finished sessions (delegated to an engine)
9
+ - **UserProfile** -- durable facts about one user (delegated to an engine)
10
+
11
+ See docs/design.md for the full design.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ TYPE_CHECKING = False # not `from typing import TYPE_CHECKING`: importing typing costs 7 ms a hook (#92)
17
+
18
+ if TYPE_CHECKING:
19
+ from .context import MemoryContext, MemoryPolicy
20
+ from .engines import Engine, Fact, LocalEngine, Recap
21
+ from .events import (
22
+ Checkpoint,
23
+ ExternalEvent,
24
+ Feedback,
25
+ Forgotten,
26
+ MemoryEvent,
27
+ Message,
28
+ Negative,
29
+ ProcedureEntry,
30
+ ProcedureFollowed,
31
+ ToolInvoked,
32
+ UserFact,
33
+ WorldFact,
34
+ event_from_dict,
35
+ )
36
+ from .environment import EnvironmentResult
37
+ from .facts import FactKey
38
+ from .ranking import EmbeddingRanker, KeywordRanker
39
+ from .history import Diff, History, MemoryView
40
+ from .extraction import ExtractedFact, Extractor, ModelExtractor
41
+ from .learning import Learner, ModelLearner
42
+ from .memory import Memory
43
+ from .procedures import TaskProcedures
44
+ from .reins import Alert, AlertPolicy, Budget, RevertResult
45
+ from .resume import ResumePoint, StepInDoubt, StepInProgress
46
+ from .runs import Run, current_run
47
+ from .session import Session, current_session
48
+ from .stores import SQLiteStore, Store
49
+ from .tool_memory import ToolProfile
50
+
51
+
52
+ # Importing the package must stay cheap: a hook process imports `commonmemo.hookclient` and nothing
53
+ # else (#92), so the public names load on first use (PEP 562).
54
+ _EXPORTS = {
55
+ "MemoryContext": ".context",
56
+ "MemoryPolicy": ".context",
57
+ "Engine": ".engines",
58
+ "Fact": ".engines",
59
+ "LocalEngine": ".engines",
60
+ "Recap": ".engines",
61
+ "Checkpoint": ".events",
62
+ "ExternalEvent": ".events",
63
+ "Feedback": ".events",
64
+ "Forgotten": ".events",
65
+ "MemoryEvent": ".events",
66
+ "Message": ".events",
67
+ "Negative": ".events",
68
+ "ProcedureEntry": ".events",
69
+ "ProcedureFollowed": ".events",
70
+ "ToolInvoked": ".events",
71
+ "UserFact": ".events",
72
+ "WorldFact": ".events",
73
+ "event_from_dict": ".events",
74
+ "EnvironmentResult": ".environment",
75
+ "FactKey": ".facts",
76
+ "EmbeddingRanker": ".ranking",
77
+ "KeywordRanker": ".ranking",
78
+ "Diff": ".history",
79
+ "History": ".history",
80
+ "MemoryView": ".history",
81
+ "ExtractedFact": ".extraction",
82
+ "Extractor": ".extraction",
83
+ "ModelExtractor": ".extraction",
84
+ "Learner": ".learning",
85
+ "ModelLearner": ".learning",
86
+ "Memory": ".memory",
87
+ "TaskProcedures": ".procedures",
88
+ "Alert": ".reins",
89
+ "AlertPolicy": ".reins",
90
+ "Budget": ".reins",
91
+ "RevertResult": ".reins",
92
+ "ResumePoint": ".resume",
93
+ "StepInDoubt": ".resume",
94
+ "StepInProgress": ".resume",
95
+ "Run": ".runs",
96
+ "current_run": ".runs",
97
+ "Session": ".session",
98
+ "current_session": ".session",
99
+ "SQLiteStore": ".stores",
100
+ "Store": ".stores",
101
+ "ToolProfile": ".tool_memory",
102
+ }
103
+
104
+
105
+ def __getattr__(name: str):
106
+ module = _EXPORTS.get(name)
107
+ if module is None:
108
+ raise AttributeError(f"module 'commonmemo' has no attribute {name!r}")
109
+ from importlib import import_module
110
+
111
+ value = getattr(import_module(module, __name__), name)
112
+ globals()[name] = value
113
+ return value
114
+
115
+
116
+ def __dir__() -> list[str]:
117
+ return sorted(set(globals()) | set(_EXPORTS))
118
+
119
+
120
+ __version__ = "0.0.1"
121
+
122
+ __all__ = [
123
+ "Memory",
124
+ "Session",
125
+ "current_session",
126
+ "MemoryContext",
127
+ "MemoryPolicy",
128
+ "EnvironmentResult",
129
+ "FactKey",
130
+ "EmbeddingRanker",
131
+ "KeywordRanker",
132
+ "WorldFact",
133
+ "Feedback",
134
+ "Negative",
135
+ "Forgotten",
136
+ "Checkpoint",
137
+ "ProcedureEntry",
138
+ "ProcedureFollowed",
139
+ "TaskProcedures",
140
+ "ExtractedFact",
141
+ "Extractor",
142
+ "Learner",
143
+ "ModelExtractor",
144
+ "ModelLearner",
145
+ "ResumePoint",
146
+ "StepInDoubt",
147
+ "StepInProgress",
148
+ "ToolProfile",
149
+ "History",
150
+ "MemoryView",
151
+ "Diff",
152
+ "Run",
153
+ "Alert",
154
+ "AlertPolicy",
155
+ "Budget",
156
+ "RevertResult",
157
+ "current_run",
158
+ "MemoryEvent",
159
+ "Message",
160
+ "ToolInvoked",
161
+ "UserFact",
162
+ "ExternalEvent",
163
+ "event_from_dict",
164
+ "Engine",
165
+ "Recap",
166
+ "Fact",
167
+ "LocalEngine",
168
+ "Store",
169
+ "SQLiteStore",
170
+ "__version__",
171
+ ]
commonmemo/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """`python -m commonmemo ...` is the `commonmemo` command (see cli.py)."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,240 @@
1
+ """Memory tools for agents -- the *pull* half of hybrid memory.
2
+
3
+ `Memory.agent_tools(session)` returns plain, typed, documented functions
4
+ bound to one session, so any framework that turns Python functions into
5
+ tools can use them as they are (CommonADK requires exactly that shape).
6
+
7
+ Reads go further than the push: `memory_recall` searches every kind with no
8
+ relevance threshold. Writes are recorded with `source="agent"`, so what an
9
+ agent wrote can always be told apart from what the user or the application
10
+ stated -- and reviewed, or revoked.
11
+
12
+ Every name starts with `memory_`, so the tools cannot collide with an
13
+ agent's own.
14
+
15
+ The tools are **synchronous**: not every framework awaits an async tool
16
+ (CommonADK's Claude adapter calls tool functions directly). Engine search
17
+ is async, so `memory_recall` runs it on a worker thread with its own event
18
+ loop; an engine whose client is bound to the caller's loop (Graphiti's
19
+ async Neo4j driver can be) falls back to the exact local index rather than
20
+ failing the tool call.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import concurrent.futures
27
+ import json
28
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine
29
+
30
+ from .context import MemoryContext, world_line
31
+ from .events import Checkpoint, Negative, UserFact
32
+ from .ranking import rank
33
+
34
+ if TYPE_CHECKING:
35
+ from .session import Session
36
+
37
+ def run_sync(coro: Coroutine[Any, Any, Any]) -> Any:
38
+ """Run `coro` to completion from synchronous code, inside or outside a
39
+ running event loop."""
40
+ try:
41
+ asyncio.get_running_loop()
42
+ except RuntimeError:
43
+ return asyncio.run(coro)
44
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
45
+ return pool.submit(asyncio.run, coro).result()
46
+
47
+
48
+ def build_agent_tools(session: "Session", *, writes: bool = True) -> list[Callable[..., str]]:
49
+ """The memory tools for `session`. `writes=False` gives read-only
50
+ tools (recall only). The session then knows its agent can recall, so
51
+ its push says when it left things out."""
52
+ memory = session.memory
53
+ session.recall_tool = True
54
+
55
+ def memory_recall(query: str) -> str:
56
+ """Search long-term memory: past sessions, what was said in them,
57
+ facts about the user, known world facts, past feedback, lessons and
58
+ decisions. Use it when the request may
59
+ depend on something from an earlier conversation that is not in
60
+ front of you. Returns matching entries, best first, or a note that
61
+ nothing matched.
62
+
63
+ Args:
64
+ query: What to look for, in plain words.
65
+ """
66
+ try:
67
+ ctx = run_sync(session.context(query, policy=memory.policy_for_recall))
68
+ except Exception: # noqa: BLE001 -- see module docstring: degrade to the local index
69
+ ctx = run_sync(session.context(query, policy=memory.policy_for_recall, local_only=True))
70
+ bar = memory.policy_for_recall.min_recall_score
71
+
72
+ def related(items: list, text: Callable[[Any], str]) -> list:
73
+ # Best first, by the memory's ranker: by meaning with an embedder (#21).
74
+ return run_sync(rank(memory.ranker, query, items, text, threshold=bar))[0]
75
+
76
+ found = MemoryContext(
77
+ facts=related(ctx.facts, lambda f: f"{f.key} {f.value}"),
78
+ world=related([*ctx.world, *ctx.world_turn], memory._world_text),
79
+ lessons=related(ctx.lessons, lambda item: item.line()),
80
+ learned=related(ctx.learned, lambda x: x.text),
81
+ recaps=ctx.recaps, said=ctx.said, policy=memory.policy, agent=session.agent,
82
+ )
83
+ decisions = (related(memory.decisions(user_id=session.user_id)[:50],
84
+ lambda d: f"{d.goal} {d.chosen} {d.outcome}")[:5] if session.user_id is not None else [])
85
+ lines = []
86
+ lines += [f"[user] {f.key}: {f.value}" for f in found.facts]
87
+ lines += [f"[fact] {world_line(w)}" for w in found.world]
88
+ lines += [f"[feedback] {item.line()}" for item in found.lessons]
89
+ lines += [f"[lesson] {x.line()}" for x in found.learned]
90
+ lines += [f"[decision] {d.line()}" for d in decisions]
91
+ lines += [f"[past session] {ctx.recap_line(e)}" for e in found.recaps]
92
+ lines += [f"[said] {w.line()}" for w in found.said]
93
+ text = "\n".join(lines) if lines else "Nothing in memory matches that."
94
+ if writes and not session.closed: # what the agent pulled, as a trace (#50); read-only tools record nothing
95
+ session._trace("recall", found, text, extra=tuple(
96
+ {"kind": "decision", "id": d.event_id, "source": "learned", "events": [d.event_id]} for d in decisions
97
+ ), query=query)
98
+ return text
99
+
100
+ def memory_get_procedure(task: str) -> str:
101
+ """How a type of task is done here: its templates (the shape of a good
102
+ result), rules, step-by-step procedures, and accepted examples. Use it
103
+ before doing one of the tasks listed below, or when memory pointed you
104
+ at it.
105
+
106
+ Args:
107
+ task: The task's name, as listed.
108
+ """
109
+ known = memory.tasks(user_id=session.user_id)
110
+ found = memory.procedures(task, user_id=session.user_id) if task else None
111
+ if not found:
112
+ listing = "\n".join(f"- {t}: {w}" if w else f"- {t}" for t, w in known.items())
113
+ head = f"No procedure for {task!r}. " if task else ""
114
+ return head + ("Known tasks:\n" + listing if known else "No procedures are recorded yet.")
115
+ found.how = "pulled"
116
+ text = found.full_text()
117
+ if writes and not session.closed: # read-only tools record nothing
118
+ session._trace("pull", MemoryContext(procedure=found, policy=memory.policy, agent=session.agent), text)
119
+ return text
120
+
121
+ reads: list[Callable[..., str]] = [memory_recall]
122
+ tasks = memory.tasks(user_id=session.user_id)
123
+ if tasks:
124
+ # Only when there is something to get; the task names go where the
125
+ # model reads them, the tool's description.
126
+ head, _, args = (memory_get_procedure.__doc__ or "").partition("\n\n Args:")
127
+ listing = "\n".join(f" - {t}: {w}" if w else f" - {t}" for t, w in list(tasks.items())[:40])
128
+ memory_get_procedure.__doc__ = f"{head}\n\n Known tasks:\n{listing}\n\n Args:{args}"
129
+ reads.append(memory_get_procedure)
130
+
131
+ if not writes:
132
+ return reads
133
+
134
+ def memory_remember(key: str, value: str) -> str:
135
+ """Save a durable fact about the current user for future sessions,
136
+ e.g. key "preferred_contact", value "email". Use it only for things
137
+ the user stated or clearly confirmed. A new value for the same key
138
+ replaces the old one.
139
+
140
+ Args:
141
+ key: Short snake_case name of the fact.
142
+ value: The fact itself.
143
+ """
144
+ if session.user_id is None:
145
+ return "Not saved: this session has no user to remember things about."
146
+ session.record(UserFact(key=key, value=value, source="agent"))
147
+ return f"Saved: {key} = {value}"
148
+
149
+ def memory_note_fact(key: str, value: str, valid_for_seconds: int = 0) -> str:
150
+ """Note a fact about the world or this project (not about the user)
151
+ for future sessions, e.g. key "refund_window", value "30 days".
152
+ Facts you note are proposals: only this user sees them until an
153
+ operator approves them, because a world fact is shown to every user.
154
+ Set valid_for_seconds for facts that go stale (prices, statuses);
155
+ 0 means it does not expire.
156
+
157
+ Args:
158
+ key: Short snake_case name of the fact.
159
+ value: The fact itself.
160
+ valid_for_seconds: How long the fact stays true; 0 for no expiry.
161
+ """
162
+ event = session.learn(key, value, ttl=valid_for_seconds or None, source="agent")
163
+ if event.proposed:
164
+ why = f" Note: {event.note}." if event.note else ""
165
+ return f"Proposed fact (other users see it once an operator approves it): {key} = {value}.{why}"
166
+ return f"Saved fact: {key} = {value}"
167
+
168
+ if memory.fact_keys:
169
+ # List the registered keys where the model reads them: the tool's own
170
+ # description, so it reuses `refund_window` instead of inventing
171
+ # `refund_period`.
172
+ head, _, args = (memory_note_fact.__doc__ or "").partition("\n\n Args:")
173
+ known = "\n".join(f" - {line}" for line in memory.fact_keys.agent_lines())
174
+ memory_note_fact.__doc__ = f"{head}\n\n Use one of these keys when it fits:\n{known}\n\n Args:{args}"
175
+
176
+ def memory_avoid(statement: str, reason: str = "") -> str:
177
+ """Record something to never do again for this user (or, without a
178
+ user, for this agent), e.g. "suggest phone calls". Use it when the
179
+ user asks you to stop doing something.
180
+
181
+ Args:
182
+ statement: What to avoid, phrased as an action.
183
+ reason: Why, if the user said.
184
+ """
185
+ scope = f"user:{session.user_id}" if session.user_id else f"agent:{session.agent or 'default'}"
186
+ session.record(Negative(statement=statement, scope=scope, reason=reason or None, source="agent"))
187
+ return f"Will avoid: {statement}"
188
+
189
+ def memory_save_progress(note: str, data_json: str = "{}") -> str:
190
+ """Record progress on a long task so it can be resumed if this
191
+ session is interrupted: what is done and what comes next.
192
+
193
+ Args:
194
+ note: One line on where the work stands.
195
+ data_json: Optional JSON object with details needed to continue.
196
+ """
197
+ try:
198
+ data = json.loads(data_json) if data_json.strip() else {}
199
+ except ValueError:
200
+ data = {"raw": data_json}
201
+ if not isinstance(data, dict):
202
+ data = {"value": data}
203
+ session.record(Checkpoint(label=note, data=data))
204
+ return "Progress saved."
205
+
206
+ def memory_save_procedure(task: str, kind: str, name: str, body: str, use_when: str = "",
207
+ for_everyone: bool = False) -> str:
208
+ """Save how a type of task is done, for next time: a "template" (the
209
+ shape of a good result), a "rule" it must follow, a "procedure" (its
210
+ steps, one numbered line each), or an "exemplar" (a result the user
211
+ accepted). By default it is this user's own way; for_everyone=True
212
+ proposes it for every user, and an operator approves it first.
213
+ Exemplars always stay with this user.
214
+
215
+ Args:
216
+ task: Short snake_case name of the task, e.g. "weekly_report".
217
+ kind: "template", "rule", "procedure" or "exemplar".
218
+ name: Short name of this entry, e.g. "default".
219
+ body: The template, rule, steps or example itself.
220
+ use_when: One line saying when it applies.
221
+ for_everyone: Propose it for every user instead of keeping it for this one.
222
+ """
223
+ if kind not in ("template", "rule", "procedure", "exemplar"):
224
+ return f"Not saved: kind must be template, rule, procedure or exemplar, not {kind!r}."
225
+ if kind == "exemplar":
226
+ if session.user_id is None:
227
+ return "Not saved: an exemplar belongs to a user, and this session has none."
228
+ event = session.save_exemplar(task, body, name=name, use_when=use_when, source="agent")
229
+ else:
230
+ # Without a user there is no "own way": it can only be a proposal.
231
+ mine = not for_everyone and session.user_id is not None
232
+ event = session.save_procedure(task, kind, name, body, use_when=use_when, for_user=mine, source="agent")
233
+ if event.proposed:
234
+ return f"Proposed {kind} {name!r} for {task} (other users get it once an operator approves it)."
235
+ return f"Saved {kind} {name!r} for {task} (this user's own)."
236
+
237
+ return [*reads, memory_remember, memory_note_fact, memory_avoid, memory_save_progress, memory_save_procedure]
238
+
239
+
240
+ __all__ = ["build_agent_tools"]
commonmemo/cli.py ADDED
@@ -0,0 +1,158 @@
1
+ """The `commonmemo` command. Subcommands:
2
+
3
+ commonmemo mcp [options] # memory as an MCP server over stdio (mcp.py)
4
+ commonmemo hook --harness NAME [options] # one harness hook event, JSON on stdin (hooks.py)
5
+ commonmemo setup NAME [options] # print the hook config for a harness (hooks.py)
6
+ commonmemo show user ID | session ID # what memory holds, read-only (views.py)
7
+ commonmemo trace SESSION [--why KEY] # what each push gave the agent, and why (traces.py)
8
+ commonmemo inspect [--port N] # the same, in a local read-only web page (inspector.py)
9
+
10
+ They share the memory options below: a flag wins over its `COMMONMEMO_*`
11
+ environment variable, which wins over the default.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import sys
18
+ from typing import TYPE_CHECKING, Optional
19
+
20
+ if TYPE_CHECKING:
21
+ import argparse
22
+
23
+ from .context import MemoryPolicy
24
+ from .memory import Memory
25
+
26
+ DEFAULT_STORE = "sqlite:///.commonmemo/memory.db"
27
+
28
+
29
+ def env_flag(name: str) -> bool:
30
+ return os.environ.get(name, "").strip().lower() in ("1", "true", "yes", "on")
31
+
32
+
33
+ WINDOWS = "8"
34
+ """`MemoryPolicy.messages` and `recall_messages`, as the option default (a test keeps them equal)."""
35
+
36
+
37
+ def memory_options(p: argparse.ArgumentParser, *, agent: Optional[str]) -> argparse.ArgumentParser:
38
+ """Add the options that pick the memory, user, agent and session."""
39
+ env = os.environ.get
40
+ p.add_argument("--store", default=env("COMMONMEMO_STORE", DEFAULT_STORE))
41
+ p.add_argument("--engine", default=env("COMMONMEMO_ENGINE", "local"))
42
+ p.add_argument("--project", default=env("COMMONMEMO_PROJECT"))
43
+ p.add_argument("--user", default=env("COMMONMEMO_USER"))
44
+ p.add_argument("--agent", default=env("COMMONMEMO_AGENT", agent))
45
+ p.add_argument("--session", default=env("COMMONMEMO_SESSION"))
46
+ p.add_argument("--read-only", action="store_true", default=env_flag("COMMONMEMO_READ_ONLY"))
47
+ p.add_argument("--messages", type=int, default=int(env("COMMONMEMO_MESSAGES", WINDOWS)),
48
+ help="windows of the user's past messages to push with each turn (default 8; 0: none)")
49
+ p.add_argument("--recall-messages", type=int,
50
+ default=int(env("COMMONMEMO_RECALL_MESSAGES", WINDOWS)),
51
+ help="windows of past messages memory_recall returns (default 8; 0: none)")
52
+ p.add_argument("--message-dates", choices=("off", "stamp", "resolve"), default=env("COMMONMEMO_MESSAGE_DATES", "off"),
53
+ help="what windows say about when: off, stamp (dated messages), resolve (also relative dates)")
54
+ return p
55
+
56
+
57
+ def policy_from(args: argparse.Namespace) -> "MemoryPolicy":
58
+ """The memory policy the command line asks for."""
59
+ from .context import MemoryPolicy
60
+
61
+ return MemoryPolicy(messages=args.messages, recall_messages=args.recall_messages, message_dates=args.message_dates)
62
+
63
+
64
+ def main(argv: Optional[list[str]] = None) -> int:
65
+ args_ = sys.argv[1:] if argv is None else argv
66
+ if args_[:1] == ["hook"] and ("--resident" in args_ or env_flag("COMMONMEMO_RESIDENT")):
67
+ from .hookclient import main as client_main # imports almost nothing: that is the point (#92)
68
+
69
+ return client_main(args_[1:])
70
+ import argparse
71
+
72
+ parser = argparse.ArgumentParser(prog="commonmemo")
73
+ sub = parser.add_subparsers(dest="command", required=True)
74
+ sub.add_parser("mcp", help="serve memory to MCP clients over stdio", add_help=False)
75
+ sub.add_parser("hook", help="handle one harness hook event (JSON on stdin)", add_help=False)
76
+ sub.add_parser("setup", help="print the hook config for claude-code, codex or opencode", add_help=False)
77
+ sub.add_parser("show", help="show a user or a session, read-only", add_help=False)
78
+ sub.add_parser("trace", help="show a session's turn traces", add_help=False)
79
+ sub.add_parser("inspect", help="open a local read-only web inspector", add_help=False)
80
+ args, rest = parser.parse_known_args(argv)
81
+ if args.command == "mcp":
82
+ from .mcp import main as mcp_main
83
+
84
+ return mcp_main(rest)
85
+ if args.command in ("hook", "setup"):
86
+ from . import hooks
87
+
88
+ return hooks.main(rest) if args.command == "hook" else hooks.setup_main(rest)
89
+ if args.command == "inspect":
90
+ from .inspector import main as inspect_main
91
+
92
+ return inspect_main(rest)
93
+ return show_main(rest) if args.command == "show" else trace_main(rest)
94
+
95
+
96
+ def _open(args: argparse.Namespace) -> "Memory":
97
+ from .memory import Memory
98
+
99
+ return Memory(args.store, args.engine, project=args.project, policy=policy_from(args))
100
+
101
+
102
+ def show_main(argv: Optional[list[str]] = None) -> int:
103
+ from .agent_tools import run_sync
104
+ from .views import session_lines, session_view, user_lines, user_view
105
+
106
+ import argparse
107
+
108
+ p = argparse.ArgumentParser(prog="commonmemo show", description="Show what memory holds, read-only.")
109
+ p.add_argument("what", choices=("user", "session"))
110
+ p.add_argument("id")
111
+ args = memory_options(p, agent=None).parse_args(argv)
112
+ mem = _open(args)
113
+ try:
114
+ if args.what == "user":
115
+ lines = user_lines(user_view(mem, args.id))
116
+ else:
117
+ view = session_view(mem, args.id)
118
+ if view is None:
119
+ print(f"no session {args.id!r}", file=sys.stderr)
120
+ return 1
121
+ lines = session_lines(view)
122
+ print("\n".join(lines))
123
+ return 0
124
+ finally:
125
+ run_sync(mem.aclose())
126
+
127
+
128
+ def trace_main(argv: Optional[list[str]] = None) -> int:
129
+ from .agent_tools import run_sync
130
+ from .views import session_lines, session_view
131
+
132
+ import argparse
133
+
134
+ p = argparse.ArgumentParser(prog="commonmemo trace", description="What each push gave the agent, and why.")
135
+ p.add_argument("session")
136
+ p.add_argument("--why", metavar="KEY", help="trace one pushed item (a fact key, world fact key or id) to its sources")
137
+ args = memory_options(p, agent=None).parse_args(argv)
138
+ mem = _open(args)
139
+ try:
140
+ if args.why:
141
+ found = mem.why(args.why, session_id=args.session)
142
+ if found is None:
143
+ print(f"{args.why!r} was never pushed in session {args.session!r}", file=sys.stderr)
144
+ return 1
145
+ print("\n".join(found.lines()))
146
+ return 0
147
+ view = session_view(mem, args.session)
148
+ if view is None:
149
+ print(f"no session {args.session!r}", file=sys.stderr)
150
+ return 1
151
+ print("\n".join(session_lines(view, traces_only=True)))
152
+ return 0
153
+ finally:
154
+ run_sync(mem.aclose())
155
+
156
+
157
+ if __name__ == "__main__":
158
+ sys.exit(main())