brain-core-mcp 0.2.2__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.
- brain_core/__init__.py +13 -0
- brain_core/agent.py +423 -0
- brain_core/cli/__init__.py +1 -0
- brain_core/cli/doctor.py +66 -0
- brain_core/cli/export.py +120 -0
- brain_core/cli/init.py +80 -0
- brain_core/cli/main.py +297 -0
- brain_core/config.py +173 -0
- brain_core/cron.py +403 -0
- brain_core/decisions/__init__.py +37 -0
- brain_core/decisions/classifier.py +235 -0
- brain_core/distill.py +269 -0
- brain_core/installer.py +214 -0
- brain_core/mcp/__init__.py +8 -0
- brain_core/mcp/server.py +123 -0
- brain_core/mcp/tools.py +343 -0
- brain_core/memory/__init__.py +26 -0
- brain_core/memory/episodic.py +105 -0
- brain_core/memory/hsam.py +105 -0
- brain_core/memory/structured.py +207 -0
- brain_core/py.typed +0 -0
- brain_core/recall.py +169 -0
- brain_core/reflection/__init__.py +38 -0
- brain_core/reflection/session.py +233 -0
- brain_core/remember.py +129 -0
- brain_core/session.py +269 -0
- brain_core/status.py +126 -0
- brain_core/storage/__init__.py +11 -0
- brain_core/storage/db.py +97 -0
- brain_core/storage/migrations/001_initial.sql +105 -0
- brain_core/storage/migrations/002_relations.sql +15 -0
- brain_core/storage/migrations/__init__.py +5 -0
- brain_core/storage/migrator.py +103 -0
- brain_core/stores/__init__.py +26 -0
- brain_core/stores/base.py +56 -0
- brain_core/stores/fakes.py +39 -0
- brain_core/stores/graph.py +110 -0
- brain_core/stores/hindsight.py +167 -0
- brain_core/stores/sqlite.py +68 -0
- brain_core/stores/vault.py +428 -0
- brain_core/templates/__init__.py +0 -0
- brain_core/templates/agent/.claude/hooks/block-dangerous-bash.sh +77 -0
- brain_core/templates/agent/.claude/rules/01-identity.md +26 -0
- brain_core/templates/agent/.claude/rules/02-doctrine.md +35 -0
- brain_core/templates/agent/.claude/rules/03-authority.md +40 -0
- brain_core/templates/agent/.claude/rules/04-memory.md +45 -0
- brain_core/templates/agent/.claude/rules/05-system.md +24 -0
- brain_core/templates/agent/.claude/settings.json +10 -0
- brain_core/templates/agent/.claude/skills/brain-check/SKILL.md +21 -0
- brain_core/templates/agent/.claude/skills/distill/SKILL.md +55 -0
- brain_core/templates/agent/.claude/skills/session-end/SKILL.md +26 -0
- brain_core/templates/agent/.mcp.json +6 -0
- brain_core/templates/agent/CLAUDE.md +15 -0
- brain_core/templates/agent/_gitignore +10 -0
- brain_core/templates/agent/brain.yaml +23 -0
- brain_core/templates/agent/data/.gitkeep +0 -0
- brain_core/templates/agent/vault/corrections.md +10 -0
- brain_core/templates/agent/vault/daily_brief.md +3 -0
- brain_core/templates/agent/vault/decisions_log.md +14 -0
- brain_core/templates/agent/vault/executive_state.md +24 -0
- brain_core/templates/agent/vault/facts.md +11 -0
- brain_core/templates/agent/vault/goals/mission_queue.md +12 -0
- brain_core/templates/agent/vault/inbox_staged.md +10 -0
- brain_core/templates/agent/vault/knowledge/_index.md +2 -0
- brain_core/templates/agent/vault/knowledge/_log.md +3 -0
- brain_core/templates/agent/vault/last_standalone_session.md +11 -0
- brain_core/templates/agent/vault/patterns.md +12 -0
- brain_core/templates/agent/vault/procedures/README.md +3 -0
- brain_core/templates/agent/vault/raw/.gitkeep +0 -0
- brain_core/templates/agent/vault/session_critique.md +17 -0
- brain_core/templates/agent/vault/sessions/.gitkeep +0 -0
- brain_core/templates/agent/vault/user_profile.md +13 -0
- brain_core_mcp-0.2.2.dist-info/METADATA +239 -0
- brain_core_mcp-0.2.2.dist-info/RECORD +77 -0
- brain_core_mcp-0.2.2.dist-info/WHEEL +4 -0
- brain_core_mcp-0.2.2.dist-info/entry_points.txt +2 -0
- brain_core_mcp-0.2.2.dist-info/licenses/LICENSE +21 -0
brain_core/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""brain-core — cognitive primitives for AI agents.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
Agent — composed entry point over memory/decisions/reflection
|
|
5
|
+
BrainConfig — brain.yaml schema (pydantic v2)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from brain_core.agent import Agent
|
|
9
|
+
from brain_core.config import BrainConfig
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.2"
|
|
12
|
+
|
|
13
|
+
__all__ = ["Agent", "BrainConfig", "__version__"]
|
brain_core/agent.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Agent class — high-level API composing all brain-core capabilities.
|
|
2
|
+
|
|
3
|
+
Consumers touch this one object. Each method is a thin, typed pass-through
|
|
4
|
+
to a capability module; capability checks are enforced at the Agent
|
|
5
|
+
boundary so misconfigured agents fail fast with actionable errors.
|
|
6
|
+
|
|
7
|
+
Lifecycle::
|
|
8
|
+
|
|
9
|
+
agent = Agent.from_config("./brain.yaml")
|
|
10
|
+
agent.start_session("sess-1")
|
|
11
|
+
agent.remember("Ada prefers dark mode", kind="preference")
|
|
12
|
+
agent.decide(action="deploy", reversible=True, risk="medium")
|
|
13
|
+
ctx = agent.gather_context() # uses current session
|
|
14
|
+
summary = agent.template_summary() # zero-LLM fallback
|
|
15
|
+
agent.save_reflection(what_went_well=summary)
|
|
16
|
+
agent.end_session()
|
|
17
|
+
agent.close()
|
|
18
|
+
|
|
19
|
+
Or as a context manager::
|
|
20
|
+
|
|
21
|
+
with Agent.from_config("./brain.yaml") as agent:
|
|
22
|
+
...
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from datetime import date as Date
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from types import TracebackType
|
|
30
|
+
from typing import Any, Self, cast, get_args
|
|
31
|
+
|
|
32
|
+
from brain_core.config import BrainConfig
|
|
33
|
+
from brain_core.decisions import classifier
|
|
34
|
+
from brain_core.decisions.classifier import Confidence, Decision, Risk, Status
|
|
35
|
+
from brain_core.distill import brain_distill
|
|
36
|
+
from brain_core.memory import episodic, hsam, structured
|
|
37
|
+
from brain_core.memory.episodic import Event
|
|
38
|
+
from brain_core.memory.hsam import HSAMDay
|
|
39
|
+
from brain_core.memory.structured import Fact, FactKind
|
|
40
|
+
from brain_core.recall import brain_recall
|
|
41
|
+
from brain_core.reflection import session as reflection_session
|
|
42
|
+
from brain_core.reflection.session import SessionContext, SessionReflection
|
|
43
|
+
from brain_core.remember import brain_remember
|
|
44
|
+
from brain_core.session import session_end, session_start
|
|
45
|
+
from brain_core.status import brain_status
|
|
46
|
+
from brain_core.storage.db import Database
|
|
47
|
+
from brain_core.storage.migrator import migrate
|
|
48
|
+
from brain_core.stores import GraphStore, HindsightStore, SqliteStore, Store, VaultStore
|
|
49
|
+
from brain_core.stores.base import Kind
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Agent:
|
|
53
|
+
"""Composed entry point over memory + decisions + reflection capabilities."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self, config: BrainConfig, db: Database, data_dir: Path, config_path: Path | None = None
|
|
57
|
+
) -> None:
|
|
58
|
+
self._config = config
|
|
59
|
+
self._db = db
|
|
60
|
+
self._data_dir = data_dir
|
|
61
|
+
self.config_path = config_path
|
|
62
|
+
self._current_session: str | None = None
|
|
63
|
+
self._stores: list[Store] | None = None
|
|
64
|
+
|
|
65
|
+
# ── construction ─────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_config(cls, config_path: Path | str) -> Self:
|
|
69
|
+
"""Load ``brain.yaml``, open SQLite, apply migrations, return Agent.
|
|
70
|
+
|
|
71
|
+
Workspace paths in the YAML are resolved relative to the config file
|
|
72
|
+
itself (not the caller's cwd). The ``data_dir`` is created on demand.
|
|
73
|
+
"""
|
|
74
|
+
cfg_path = Path(config_path).resolve()
|
|
75
|
+
config = BrainConfig.from_yaml(cfg_path)
|
|
76
|
+
|
|
77
|
+
data_dir = _resolve_dir(cfg_path.parent, config.workspace.data_dir)
|
|
78
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
|
|
80
|
+
db = Database.connect(data_dir / "brain.db")
|
|
81
|
+
migrate(db)
|
|
82
|
+
return cls(config=config, db=db, data_dir=data_dir, config_path=cfg_path)
|
|
83
|
+
|
|
84
|
+
# ── introspection ────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def name(self) -> str:
|
|
88
|
+
return self._config.name
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def config(self) -> BrainConfig:
|
|
92
|
+
return self._config
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def db(self) -> Database:
|
|
96
|
+
"""Escape hatch for raw SQL. Prefer the typed methods."""
|
|
97
|
+
return self._db
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def data_dir(self) -> Path:
|
|
101
|
+
return self._data_dir
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def current_session(self) -> str | None:
|
|
105
|
+
return self._current_session
|
|
106
|
+
|
|
107
|
+
def resolve(self, p: Path | str) -> Path:
|
|
108
|
+
"""A path from brain.yaml: ``~`` expanded, relative ones anchored at the yaml's folder."""
|
|
109
|
+
base = self.config_path.parent if self.config_path else Path.cwd()
|
|
110
|
+
return _resolve_dir(base, Path(p).expanduser())
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def stores(self) -> list[Store]:
|
|
114
|
+
"""Every configured store (v0.2). SQLite first, then vault, Hindsight, graph as set."""
|
|
115
|
+
if self._stores is None:
|
|
116
|
+
s = self._config.stores
|
|
117
|
+
self._stores = [SqliteStore(self._db)]
|
|
118
|
+
if s.vault:
|
|
119
|
+
self._stores.append(VaultStore(self.resolve(s.vault), owner=self.name.lower()))
|
|
120
|
+
if s.hindsight:
|
|
121
|
+
self._stores.append(HindsightStore(s.hindsight.url, s.hindsight.bank, self.name))
|
|
122
|
+
self._stores.append(GraphStore(self._db)) # relations table in brain.db, always on
|
|
123
|
+
return self._stores
|
|
124
|
+
|
|
125
|
+
def brain_recall(
|
|
126
|
+
self, query: str, scope: str = "all", fmt: str = "concise", limit: int = 10
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""One query across every store, deduplicated and ranked. See :mod:`brain_core.recall`."""
|
|
129
|
+
self._require_capability("memory")
|
|
130
|
+
return brain_recall(self.stores, query, scope=scope, fmt=fmt, limit=limit)
|
|
131
|
+
|
|
132
|
+
# ── session lifecycle ────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
def start_session(self, session_id: str) -> None:
|
|
135
|
+
"""Set the active session id. Used as the default for log_event and
|
|
136
|
+
reflection calls until :meth:`end_session`."""
|
|
137
|
+
if not session_id.strip():
|
|
138
|
+
raise ValueError("session_id must not be empty")
|
|
139
|
+
self._current_session = session_id
|
|
140
|
+
|
|
141
|
+
def end_session(self) -> None:
|
|
142
|
+
self._current_session = None
|
|
143
|
+
|
|
144
|
+
# ── memory ───────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
def remember(
|
|
147
|
+
self,
|
|
148
|
+
content: str,
|
|
149
|
+
kind: FactKind = "fact",
|
|
150
|
+
tags: list[str] | None = None,
|
|
151
|
+
metadata: dict[str, Any] | None = None,
|
|
152
|
+
) -> Fact:
|
|
153
|
+
self._require_capability("memory")
|
|
154
|
+
return structured.remember(self._db, content, kind=kind, tags=tags, metadata=metadata)
|
|
155
|
+
|
|
156
|
+
def recall(
|
|
157
|
+
self,
|
|
158
|
+
query: str | None = None,
|
|
159
|
+
kind: FactKind | None = None,
|
|
160
|
+
limit: int = 10,
|
|
161
|
+
include_superseded: bool = False,
|
|
162
|
+
) -> list[Fact]:
|
|
163
|
+
self._require_capability("memory")
|
|
164
|
+
return structured.recall(
|
|
165
|
+
self._db,
|
|
166
|
+
query=query,
|
|
167
|
+
kind=kind,
|
|
168
|
+
limit=limit,
|
|
169
|
+
include_superseded=include_superseded,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def supersede(
|
|
173
|
+
self,
|
|
174
|
+
old_fact_id: int,
|
|
175
|
+
new_content: str,
|
|
176
|
+
kind: FactKind = "fact",
|
|
177
|
+
tags: list[str] | None = None,
|
|
178
|
+
metadata: dict[str, Any] | None = None,
|
|
179
|
+
) -> Fact:
|
|
180
|
+
self._require_capability("memory")
|
|
181
|
+
return structured.supersede(
|
|
182
|
+
self._db,
|
|
183
|
+
old_fact_id,
|
|
184
|
+
new_content,
|
|
185
|
+
kind=kind,
|
|
186
|
+
tags=tags,
|
|
187
|
+
metadata=metadata,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def log_event(
|
|
191
|
+
self,
|
|
192
|
+
event_type: str,
|
|
193
|
+
payload: dict[str, Any] | None = None,
|
|
194
|
+
session_id: str | None = None,
|
|
195
|
+
) -> Event:
|
|
196
|
+
self._require_capability("memory")
|
|
197
|
+
sid = session_id if session_id is not None else self._current_session
|
|
198
|
+
return episodic.log_event(self._db, event_type, payload=payload, session_id=sid)
|
|
199
|
+
|
|
200
|
+
def recent_events(
|
|
201
|
+
self,
|
|
202
|
+
session_id: str | None = None,
|
|
203
|
+
event_type: str | None = None,
|
|
204
|
+
limit: int = 50,
|
|
205
|
+
) -> list[Event]:
|
|
206
|
+
self._require_capability("memory")
|
|
207
|
+
return episodic.recent_events(
|
|
208
|
+
self._db,
|
|
209
|
+
session_id=session_id,
|
|
210
|
+
event_type=event_type,
|
|
211
|
+
limit=limit,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def append_today(self, text: str, today: Date | None = None) -> HSAMDay:
|
|
215
|
+
"""Append text to today's HSAM archive.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
RuntimeError: if the memory capability is disabled, or HSAM is
|
|
219
|
+
disabled in ``brain.yaml``.
|
|
220
|
+
"""
|
|
221
|
+
self._require_capability("memory")
|
|
222
|
+
if not self._config.memory.hsam.enabled:
|
|
223
|
+
raise RuntimeError("HSAM is disabled in brain.yaml (memory.hsam.enabled)")
|
|
224
|
+
return hsam.append_today(
|
|
225
|
+
self._db,
|
|
226
|
+
workspace=self._data_dir,
|
|
227
|
+
text=text,
|
|
228
|
+
subdir=self._config.memory.hsam.subdir,
|
|
229
|
+
today=today,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def brain_remember(
|
|
233
|
+
self,
|
|
234
|
+
text: str,
|
|
235
|
+
kind: str,
|
|
236
|
+
*,
|
|
237
|
+
domain: str = "",
|
|
238
|
+
source: str = "",
|
|
239
|
+
mission: str | None = None,
|
|
240
|
+
when: Date | None = None,
|
|
241
|
+
force: bool = False,
|
|
242
|
+
**extra: str,
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
"""Write-through to every store with per-layer status. See :mod:`brain_core.remember`."""
|
|
245
|
+
self._require_capability("memory")
|
|
246
|
+
if kind not in get_args(Kind):
|
|
247
|
+
raise ValueError(f"kind must be one of {'|'.join(get_args(Kind))}")
|
|
248
|
+
out = brain_remember(
|
|
249
|
+
self.stores,
|
|
250
|
+
text,
|
|
251
|
+
cast(Kind, kind),
|
|
252
|
+
domain=domain,
|
|
253
|
+
source=source,
|
|
254
|
+
mission=mission,
|
|
255
|
+
when=when,
|
|
256
|
+
force=force,
|
|
257
|
+
**cast(dict[str, Any], extra),
|
|
258
|
+
)
|
|
259
|
+
# the session journal: brain_session('end') reports and summarises these
|
|
260
|
+
episodic.log_event(
|
|
261
|
+
self._db,
|
|
262
|
+
"brain_remember",
|
|
263
|
+
payload={
|
|
264
|
+
"kind": kind,
|
|
265
|
+
"id": out.get("id", ""),
|
|
266
|
+
"status": out["status"],
|
|
267
|
+
"text": out["text"][:200],
|
|
268
|
+
"contradiction": "contradiction" in out,
|
|
269
|
+
},
|
|
270
|
+
session_id=self._current_session,
|
|
271
|
+
)
|
|
272
|
+
return out
|
|
273
|
+
|
|
274
|
+
def brain_session(
|
|
275
|
+
self,
|
|
276
|
+
phase: str,
|
|
277
|
+
session_id: str | None = None,
|
|
278
|
+
*,
|
|
279
|
+
executive_state: str | None = None,
|
|
280
|
+
critique: str | None = None,
|
|
281
|
+
sync: str | None = None,
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
"""START block or END writes. See :mod:`brain_core.session`."""
|
|
284
|
+
self._require_capability("memory")
|
|
285
|
+
if phase == "start":
|
|
286
|
+
return session_start(self, session_id)
|
|
287
|
+
if phase == "end":
|
|
288
|
+
return session_end(
|
|
289
|
+
self, session_id, executive_state=executive_state, critique=critique, sync=sync
|
|
290
|
+
)
|
|
291
|
+
raise ValueError("phase must be 'start' or 'end'")
|
|
292
|
+
|
|
293
|
+
def brain_distill(
|
|
294
|
+
self,
|
|
295
|
+
source: str,
|
|
296
|
+
*,
|
|
297
|
+
text: str | None = None,
|
|
298
|
+
title: str | None = None,
|
|
299
|
+
author: str | None = None,
|
|
300
|
+
published: str | None = None,
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
"""Capture a source into ``raw/``. See :mod:`brain_core.distill`."""
|
|
303
|
+
self._require_capability("memory")
|
|
304
|
+
return brain_distill(
|
|
305
|
+
self, source, text=text, title=title, author=author, published=published
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def brain_status(self) -> dict[str, Any]:
|
|
309
|
+
"""Health in one call. See :mod:`brain_core.status`."""
|
|
310
|
+
return brain_status(self)
|
|
311
|
+
|
|
312
|
+
# ── decisions ────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
def decide(
|
|
315
|
+
self,
|
|
316
|
+
action: str,
|
|
317
|
+
reversible: bool,
|
|
318
|
+
risk: Risk = "medium",
|
|
319
|
+
confidence: Confidence = "medium",
|
|
320
|
+
reasoning: str | None = None,
|
|
321
|
+
alternatives: list[str] | None = None,
|
|
322
|
+
) -> Decision:
|
|
323
|
+
self._require_capability("decisions")
|
|
324
|
+
return classifier.decide(
|
|
325
|
+
self._db,
|
|
326
|
+
action=action,
|
|
327
|
+
reversible=reversible,
|
|
328
|
+
risk=risk,
|
|
329
|
+
confidence=confidence,
|
|
330
|
+
reasoning=reasoning,
|
|
331
|
+
alternatives=alternatives,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def reverse(self, decision_id: int, replaced_by: int | None = None) -> Decision:
|
|
335
|
+
self._require_capability("decisions")
|
|
336
|
+
return classifier.reverse(self._db, decision_id, replaced_by=replaced_by)
|
|
337
|
+
|
|
338
|
+
def complete(self, decision_id: int) -> Decision:
|
|
339
|
+
self._require_capability("decisions")
|
|
340
|
+
return classifier.complete(self._db, decision_id)
|
|
341
|
+
|
|
342
|
+
def list_decisions(self, status: Status | None = None, limit: int = 50) -> list[Decision]:
|
|
343
|
+
self._require_capability("decisions")
|
|
344
|
+
return classifier.list_decisions(self._db, status=status, limit=limit)
|
|
345
|
+
|
|
346
|
+
# ── reflection ───────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
def gather_context(
|
|
349
|
+
self,
|
|
350
|
+
session_id: str | None = None,
|
|
351
|
+
event_limit: int = 20,
|
|
352
|
+
decision_limit: int = 20,
|
|
353
|
+
) -> SessionContext:
|
|
354
|
+
self._require_capability("reflection")
|
|
355
|
+
sid = self._require_session(session_id)
|
|
356
|
+
return reflection_session.gather_session_context(
|
|
357
|
+
self._db,
|
|
358
|
+
session_id=sid,
|
|
359
|
+
event_limit=event_limit,
|
|
360
|
+
decision_limit=decision_limit,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def save_reflection(
|
|
364
|
+
self,
|
|
365
|
+
session_id: str | None = None,
|
|
366
|
+
what_went_well: str | None = None,
|
|
367
|
+
what_to_improve: str | None = None,
|
|
368
|
+
apply_next_session: str | None = None,
|
|
369
|
+
auto_generated: bool = False,
|
|
370
|
+
) -> SessionReflection:
|
|
371
|
+
self._require_capability("reflection")
|
|
372
|
+
sid = self._require_session(session_id)
|
|
373
|
+
return reflection_session.save_reflection(
|
|
374
|
+
self._db,
|
|
375
|
+
session_id=sid,
|
|
376
|
+
what_went_well=what_went_well,
|
|
377
|
+
what_to_improve=what_to_improve,
|
|
378
|
+
apply_next_session=apply_next_session,
|
|
379
|
+
auto_generated=auto_generated,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def template_summary(self, session_id: str | None = None) -> str:
|
|
383
|
+
self._require_capability("reflection")
|
|
384
|
+
ctx = self.gather_context(session_id=session_id)
|
|
385
|
+
return reflection_session.template_summary(ctx)
|
|
386
|
+
|
|
387
|
+
# ── lifecycle ────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
def close(self) -> None:
|
|
390
|
+
self._db.close()
|
|
391
|
+
|
|
392
|
+
def __enter__(self) -> Self:
|
|
393
|
+
return self
|
|
394
|
+
|
|
395
|
+
def __exit__(
|
|
396
|
+
self,
|
|
397
|
+
exc_type: type[BaseException] | None,
|
|
398
|
+
exc_val: BaseException | None,
|
|
399
|
+
exc_tb: TracebackType | None,
|
|
400
|
+
) -> None:
|
|
401
|
+
self.close()
|
|
402
|
+
|
|
403
|
+
# ── internal ─────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
def _require_capability(self, name: str) -> None:
|
|
406
|
+
if name not in self._config.capabilities:
|
|
407
|
+
raise RuntimeError(
|
|
408
|
+
f"capability {name!r} is not enabled in brain.yaml "
|
|
409
|
+
f"(enabled: {list(self._config.capabilities)})"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def _require_session(self, explicit: str | None) -> str:
|
|
413
|
+
if explicit is not None:
|
|
414
|
+
if not explicit.strip():
|
|
415
|
+
raise ValueError("session_id must not be empty")
|
|
416
|
+
return explicit
|
|
417
|
+
if self._current_session is None:
|
|
418
|
+
raise RuntimeError("no active session — call start_session() or pass session_id")
|
|
419
|
+
return self._current_session
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _resolve_dir(base: Path, p: Path) -> Path:
|
|
423
|
+
return p if p.is_absolute() else (base / p).resolve()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI entry points. See :mod:`brain_core.cli.main`."""
|
brain_core/cli/doctor.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""``brain doctor`` — health check an agent's state.
|
|
2
|
+
|
|
3
|
+
Exposes :func:`doctor_agent` as the pure implementation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from brain_core.agent import Agent
|
|
13
|
+
|
|
14
|
+
_ROW_COUNT_TABLES = (
|
|
15
|
+
"facts",
|
|
16
|
+
"decisions",
|
|
17
|
+
"events",
|
|
18
|
+
"reflections",
|
|
19
|
+
"hsam_days",
|
|
20
|
+
"metrics",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def doctor_agent(path: Path) -> None:
|
|
25
|
+
"""Run a health check on an agent at ``path``.
|
|
26
|
+
|
|
27
|
+
Exits non-zero on missing brain.yaml, invalid config, or DB access error.
|
|
28
|
+
"""
|
|
29
|
+
config_path = path if path.is_file() else path / "brain.yaml"
|
|
30
|
+
if not config_path.exists():
|
|
31
|
+
typer.echo(f"ERROR: no brain.yaml at {config_path}", err=True)
|
|
32
|
+
raise typer.Exit(code=1)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
agent = Agent.from_config(config_path)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
typer.echo(f"ERROR: failed to load agent: {e}", err=True)
|
|
38
|
+
raise typer.Exit(code=1) from e
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
typer.echo(f"Agent: {agent.name}")
|
|
42
|
+
typer.echo(f" config: {config_path}")
|
|
43
|
+
typer.echo(f" data_dir: {agent.data_dir}")
|
|
44
|
+
typer.echo(f" capabilities: {', '.join(agent.config.capabilities)}")
|
|
45
|
+
typer.echo("")
|
|
46
|
+
typer.echo("Row counts:")
|
|
47
|
+
for table in _ROW_COUNT_TABLES:
|
|
48
|
+
row = agent.db.fetchone(f"SELECT COUNT(*) AS n FROM {table}")
|
|
49
|
+
count = int(row["n"]) if row is not None else 0
|
|
50
|
+
typer.echo(f" {table:<12} {count:>6}")
|
|
51
|
+
|
|
52
|
+
if agent.config.memory.hsam.enabled:
|
|
53
|
+
typer.echo("")
|
|
54
|
+
hsam_row = agent.db.fetchone(
|
|
55
|
+
"SELECT COUNT(*) AS days, COALESCE(SUM(word_count), 0) AS words FROM hsam_days"
|
|
56
|
+
)
|
|
57
|
+
if hsam_row is not None:
|
|
58
|
+
typer.echo(
|
|
59
|
+
f"HSAM: {int(hsam_row['days'])} days archived, "
|
|
60
|
+
f"{int(hsam_row['words'])} words total"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
typer.echo("")
|
|
64
|
+
typer.echo("Health check: OK")
|
|
65
|
+
finally:
|
|
66
|
+
agent.close()
|
brain_core/cli/export.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""``brain export`` — dump agent state as human-readable markdown.
|
|
2
|
+
|
|
3
|
+
Exposes :func:`export_agent` as the pure implementation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from brain_core.agent import Agent
|
|
14
|
+
from brain_core.memory.structured import Fact
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def export_agent(
|
|
18
|
+
path: Path,
|
|
19
|
+
fact_limit: int = 100,
|
|
20
|
+
decision_limit: int = 50,
|
|
21
|
+
event_limit: int = 50,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Dump agent state at ``path`` to stdout as markdown.
|
|
24
|
+
|
|
25
|
+
Exits non-zero if brain.yaml is missing or invalid.
|
|
26
|
+
"""
|
|
27
|
+
config_path = path / "brain.yaml"
|
|
28
|
+
if not config_path.exists():
|
|
29
|
+
typer.echo(f"ERROR: no brain.yaml at {config_path}", err=True)
|
|
30
|
+
raise typer.Exit(code=1)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
agent = Agent.from_config(config_path)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
typer.echo(f"ERROR: failed to load agent: {e}", err=True)
|
|
36
|
+
raise typer.Exit(code=1) from e
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
caps = agent.config.capabilities
|
|
40
|
+
lines: list[str] = [
|
|
41
|
+
f"# {agent.name}",
|
|
42
|
+
"",
|
|
43
|
+
f"_Exported from {agent.data_dir}_",
|
|
44
|
+
"",
|
|
45
|
+
f"**Capabilities:** {', '.join(caps)}",
|
|
46
|
+
"",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
if "memory" in caps:
|
|
50
|
+
lines.extend(_facts_section(agent, fact_limit))
|
|
51
|
+
|
|
52
|
+
if "decisions" in caps:
|
|
53
|
+
lines.extend(_decisions_section(agent, decision_limit))
|
|
54
|
+
|
|
55
|
+
if "memory" in caps:
|
|
56
|
+
lines.extend(_events_section(agent, event_limit))
|
|
57
|
+
|
|
58
|
+
typer.echo("\n".join(lines))
|
|
59
|
+
finally:
|
|
60
|
+
agent.close()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _facts_section(agent: Agent, limit: int) -> list[str]:
|
|
64
|
+
facts = agent.recall(limit=limit)
|
|
65
|
+
lines: list[str] = ["## Facts", ""]
|
|
66
|
+
if not facts:
|
|
67
|
+
lines.append("_No facts remembered yet._")
|
|
68
|
+
lines.append("")
|
|
69
|
+
return lines
|
|
70
|
+
|
|
71
|
+
by_kind: dict[str, list[Fact]] = defaultdict(list)
|
|
72
|
+
for f in facts:
|
|
73
|
+
by_kind[f.kind].append(f)
|
|
74
|
+
|
|
75
|
+
for kind in sorted(by_kind):
|
|
76
|
+
items = by_kind[kind]
|
|
77
|
+
lines.append(f"### {kind} ({len(items)})")
|
|
78
|
+
lines.append("")
|
|
79
|
+
for f in items:
|
|
80
|
+
tags_suffix = f" _tags: {', '.join(f.tags)}_" if f.tags else ""
|
|
81
|
+
lines.append(f"- {f.content}{tags_suffix}")
|
|
82
|
+
lines.append("")
|
|
83
|
+
return lines
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _decisions_section(agent: Agent, limit: int) -> list[str]:
|
|
87
|
+
decisions = agent.list_decisions(limit=limit)
|
|
88
|
+
lines: list[str] = ["## Decisions", ""]
|
|
89
|
+
if not decisions:
|
|
90
|
+
lines.append("_No decisions logged yet._")
|
|
91
|
+
lines.append("")
|
|
92
|
+
return lines
|
|
93
|
+
|
|
94
|
+
for d in decisions:
|
|
95
|
+
reversibility = "reversible" if d.reversible else "IRREVERSIBLE"
|
|
96
|
+
header = (
|
|
97
|
+
f"- **[{d.status}]** {d.action} _({reversibility}, risk={d.risk}, conf={d.confidence})_"
|
|
98
|
+
)
|
|
99
|
+
lines.append(header)
|
|
100
|
+
if d.reasoning:
|
|
101
|
+
lines.append(f" - reasoning: {d.reasoning}")
|
|
102
|
+
if d.reversed_by is not None:
|
|
103
|
+
lines.append(f" - replaced by decision #{d.reversed_by}")
|
|
104
|
+
lines.append("")
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _events_section(agent: Agent, limit: int) -> list[str]:
|
|
109
|
+
events = agent.recent_events(limit=limit)
|
|
110
|
+
lines: list[str] = ["## Recent events", ""]
|
|
111
|
+
if not events:
|
|
112
|
+
lines.append("_No events logged yet._")
|
|
113
|
+
lines.append("")
|
|
114
|
+
return lines
|
|
115
|
+
|
|
116
|
+
for e in events:
|
|
117
|
+
session = f" session={e.session_id}" if e.session_id else ""
|
|
118
|
+
lines.append(f"- `{e.occurred_at.isoformat()}` **{e.event_type}**{session}")
|
|
119
|
+
lines.append("")
|
|
120
|
+
return lines
|