claude-memory-agent 3.0.3 → 3.2.0
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.
- package/bin/lib/installer.js +3 -1
- package/bin/lib/steps/advanced.js +1 -1
- package/config.py +10 -2
- package/dashboard.html +88 -38
- package/hooks/auto-detect-response.py +3 -6
- package/hooks/detect-correction.py +8 -7
- package/hooks/extract_memories.py +104 -0
- package/hooks/grounding-hook-v2.py +169 -33
- package/hooks/grounding-hook.py +19 -3
- package/hooks/log-tool-use.py +3 -6
- package/hooks/log-user-request.py +14 -6
- package/hooks/pre-tool-decision.py +3 -6
- package/hooks/pre_compact_hook.py +269 -5
- package/hooks/session_end_hook.py +37 -0
- package/hooks/session_start.py +10 -0
- package/hooks/stop_hook.py +315 -14
- package/install.py +141 -46
- package/main.py +522 -13
- package/mcp_proxy.py +93 -6
- package/package.json +2 -2
- package/services/agent_registry.py +260 -12
- package/services/database.py +453 -1
- package/services/embeddings.py +1 -1
- package/services/retry_queue.py +5 -1
- package/services/soul.py +791 -0
- package/services/vector_index.py +5 -1
- package/update_system.py +34 -8
package/main.py
CHANGED
|
@@ -111,11 +111,15 @@ from services.claude_md_sync import get_claude_md_sync
|
|
|
111
111
|
# Cross-session awareness
|
|
112
112
|
from services.session_awareness import get_session_awareness
|
|
113
113
|
|
|
114
|
+
# Soul layer — persistent personality and learning
|
|
115
|
+
from services.soul import SoulService
|
|
116
|
+
|
|
114
117
|
# Agent registry for dashboard
|
|
115
118
|
from services.agent_registry import (
|
|
116
119
|
AVAILABLE_AGENTS, AVAILABLE_MCPS, AVAILABLE_HOOKS,
|
|
117
120
|
AGENT_CATEGORIES, get_agents_by_category, get_agent_by_id,
|
|
118
|
-
load_configured_hooks, load_configured_mcps
|
|
121
|
+
load_configured_hooks, load_configured_mcps,
|
|
122
|
+
discover_agents, discover_categories, find_agent_by_id, toggle_agent
|
|
119
123
|
)
|
|
120
124
|
|
|
121
125
|
load_dotenv()
|
|
@@ -186,6 +190,7 @@ metrics = OperationMetrics()
|
|
|
186
190
|
|
|
187
191
|
# Initialize services
|
|
188
192
|
db = DatabaseService()
|
|
193
|
+
soul_service = SoulService(db)
|
|
189
194
|
from config import config as _cfg
|
|
190
195
|
embeddings = EmbeddingService(
|
|
191
196
|
provider_type=_cfg.EMBEDDING_PROVIDER,
|
|
@@ -1772,6 +1777,37 @@ async def _handle_session_append_file(query, params, session_id):
|
|
|
1772
1777
|
)
|
|
1773
1778
|
|
|
1774
1779
|
|
|
1780
|
+
# ============================================================
|
|
1781
|
+
# SOUL LAYER SKILL HANDLERS
|
|
1782
|
+
# ============================================================
|
|
1783
|
+
|
|
1784
|
+
async def _handle_soul_brief(query, params, session_id):
|
|
1785
|
+
project_path = params.get("project_path", "")
|
|
1786
|
+
brief = await soul_service.generate_soul_brief(project_path)
|
|
1787
|
+
return {"success": True, "brief": brief}
|
|
1788
|
+
|
|
1789
|
+
async def _handle_soul_capture(query, params, session_id):
|
|
1790
|
+
sid = params.get("session_id") or session_id or ""
|
|
1791
|
+
project_path = params.get("project_path", "")
|
|
1792
|
+
fragment_type = params.get("fragment_type", "")
|
|
1793
|
+
content = params.get("content", "")
|
|
1794
|
+
if not fragment_type or not content:
|
|
1795
|
+
return {"success": False, "error": "fragment_type and content required"}
|
|
1796
|
+
frag_id = await soul_service.capture_soul_fragment(
|
|
1797
|
+
session_id=sid,
|
|
1798
|
+
fragment_type=fragment_type,
|
|
1799
|
+
content=content,
|
|
1800
|
+
project_path=project_path,
|
|
1801
|
+
)
|
|
1802
|
+
return {"success": frag_id is not None, "fragment_id": frag_id}
|
|
1803
|
+
|
|
1804
|
+
async def _handle_soul_integrate(query, params, session_id):
|
|
1805
|
+
sid = params.get("session_id") or session_id or ""
|
|
1806
|
+
project_path = params.get("project_path", "")
|
|
1807
|
+
result = await soul_service.run_soul_integration(sid, project_path)
|
|
1808
|
+
return {"success": True, **result}
|
|
1809
|
+
|
|
1810
|
+
|
|
1775
1811
|
# ============================================================
|
|
1776
1812
|
# SKILL DISPATCH TABLE
|
|
1777
1813
|
# ============================================================
|
|
@@ -1928,6 +1964,11 @@ SKILL_DISPATCH = {
|
|
|
1928
1964
|
"session_conflicts": _handle_session_conflicts,
|
|
1929
1965
|
"session_post_activity": _handle_session_post_activity,
|
|
1930
1966
|
"session_append_file": _handle_session_append_file,
|
|
1967
|
+
|
|
1968
|
+
# Soul Layer
|
|
1969
|
+
"soul_brief": _handle_soul_brief,
|
|
1970
|
+
"soul_capture": _handle_soul_capture,
|
|
1971
|
+
"soul_integrate": _handle_soul_integrate,
|
|
1931
1972
|
}
|
|
1932
1973
|
|
|
1933
1974
|
|
|
@@ -4362,17 +4403,41 @@ async def api_mark_insight_applied(insight_id: int):
|
|
|
4362
4403
|
# ============= Agent Configuration API =============
|
|
4363
4404
|
|
|
4364
4405
|
@app.get("/api/agents")
|
|
4365
|
-
async def get_all_agents():
|
|
4366
|
-
"""Get all
|
|
4406
|
+
async def get_all_agents(project_path: Optional[str] = None):
|
|
4407
|
+
"""Get all discovered agents from disk with categories."""
|
|
4408
|
+
agents = discover_agents(project_path)
|
|
4409
|
+
categories = discover_categories(agents)
|
|
4410
|
+
enabled_count = sum(1 for a in agents if a["enabled"])
|
|
4367
4411
|
return {
|
|
4368
4412
|
"success": True,
|
|
4369
|
-
"agents":
|
|
4370
|
-
"categories":
|
|
4371
|
-
"by_category": get_agents_by_category(),
|
|
4372
|
-
"total": len(
|
|
4413
|
+
"agents": agents,
|
|
4414
|
+
"categories": categories,
|
|
4415
|
+
"by_category": get_agents_by_category(agents),
|
|
4416
|
+
"total": len(agents),
|
|
4417
|
+
"enabled": enabled_count,
|
|
4373
4418
|
}
|
|
4374
4419
|
|
|
4375
4420
|
|
|
4421
|
+
@app.post("/api/agents/{agent_id}/toggle")
|
|
4422
|
+
async def toggle_agent_endpoint(agent_id: str, request: Request):
|
|
4423
|
+
"""Toggle an agent between enabled/disabled by moving files on disk."""
|
|
4424
|
+
try:
|
|
4425
|
+
body = await request.json()
|
|
4426
|
+
enabled = body.get("enabled", False)
|
|
4427
|
+
project_path = body.get("project_path")
|
|
4428
|
+
|
|
4429
|
+
agent = toggle_agent(agent_id, enabled, project_path)
|
|
4430
|
+
return {
|
|
4431
|
+
"success": True,
|
|
4432
|
+
"agent": agent,
|
|
4433
|
+
}
|
|
4434
|
+
except FileNotFoundError as e:
|
|
4435
|
+
return {"success": False, "error": str(e)}
|
|
4436
|
+
except Exception as e:
|
|
4437
|
+
logger.error(f"Error toggling agent {agent_id}: {e}")
|
|
4438
|
+
return {"success": False, "error": str(e)}
|
|
4439
|
+
|
|
4440
|
+
|
|
4376
4441
|
@app.get("/api/mcps")
|
|
4377
4442
|
async def get_all_mcps():
|
|
4378
4443
|
"""Get all available MCP servers with live configured status."""
|
|
@@ -4435,7 +4500,8 @@ async def get_project_config(project_path: str):
|
|
|
4435
4500
|
(project_path,)
|
|
4436
4501
|
)
|
|
4437
4502
|
|
|
4438
|
-
# Build agent status map
|
|
4503
|
+
# Build agent status map from disk discovery
|
|
4504
|
+
live_agents = discover_agents(project_path)
|
|
4439
4505
|
agent_status = {}
|
|
4440
4506
|
for config in (agent_configs or []):
|
|
4441
4507
|
agent_status[config['agent_id']] = {
|
|
@@ -4444,12 +4510,12 @@ async def get_project_config(project_path: str):
|
|
|
4444
4510
|
'settings': json.loads(config['settings']) if config['settings'] else {}
|
|
4445
4511
|
}
|
|
4446
4512
|
|
|
4447
|
-
# Fill in defaults for
|
|
4448
|
-
for agent in
|
|
4513
|
+
# Fill in defaults for agents not in DB config
|
|
4514
|
+
for agent in live_agents:
|
|
4449
4515
|
if agent['id'] not in agent_status:
|
|
4450
4516
|
agent_status[agent['id']] = {
|
|
4451
|
-
'enabled': agent['
|
|
4452
|
-
'priority':
|
|
4517
|
+
'enabled': agent['enabled'],
|
|
4518
|
+
'priority': 5,
|
|
4453
4519
|
'settings': {}
|
|
4454
4520
|
}
|
|
4455
4521
|
|
|
@@ -4502,7 +4568,7 @@ async def get_project_config(project_path: str):
|
|
|
4502
4568
|
"hooks": hook_status,
|
|
4503
4569
|
"stats": {
|
|
4504
4570
|
"enabled_agents": sum(1 for a in agent_status.values() if a['enabled']),
|
|
4505
|
-
"total_agents": len(
|
|
4571
|
+
"total_agents": len(live_agents),
|
|
4506
4572
|
"enabled_mcps": sum(1 for m in mcp_status.values() if m.get('enabled')),
|
|
4507
4573
|
"total_mcps": len(live_mcps),
|
|
4508
4574
|
"configured_mcps": sum(1 for m in mcp_status.values() if m.get('configured')),
|
|
@@ -5645,6 +5711,60 @@ async def api_post_session_activity(request: Request):
|
|
|
5645
5711
|
return {"success": False, "error": str(e)}
|
|
5646
5712
|
|
|
5647
5713
|
|
|
5714
|
+
# ============= Soul Layer REST Endpoints =============
|
|
5715
|
+
|
|
5716
|
+
|
|
5717
|
+
@app.get("/api/soul/brief")
|
|
5718
|
+
async def api_soul_brief(project_path: str = ""):
|
|
5719
|
+
"""Get the soul brief for a project."""
|
|
5720
|
+
try:
|
|
5721
|
+
brief = await soul_service.generate_soul_brief(project_path)
|
|
5722
|
+
return {"success": True, "brief": brief}
|
|
5723
|
+
except Exception as e:
|
|
5724
|
+
logger.error(f"Soul brief failed: {e}")
|
|
5725
|
+
return {"success": False, "error": str(e), "brief": ""}
|
|
5726
|
+
|
|
5727
|
+
|
|
5728
|
+
@app.post("/api/soul/capture")
|
|
5729
|
+
async def api_soul_capture(request: Request):
|
|
5730
|
+
"""Capture a soul fragment from a session response."""
|
|
5731
|
+
try:
|
|
5732
|
+
body = await request.json()
|
|
5733
|
+
session_id = body.get("session_id", "")
|
|
5734
|
+
project_path = body.get("project_path", "")
|
|
5735
|
+
fragment_type = body.get("fragment_type", "")
|
|
5736
|
+
content = body.get("content", "")
|
|
5737
|
+
|
|
5738
|
+
if not fragment_type or not content:
|
|
5739
|
+
return {"success": False, "error": "fragment_type and content required"}
|
|
5740
|
+
|
|
5741
|
+
frag_id = await soul_service.capture_soul_fragment(
|
|
5742
|
+
session_id=session_id,
|
|
5743
|
+
fragment_type=fragment_type,
|
|
5744
|
+
content=content,
|
|
5745
|
+
project_path=project_path,
|
|
5746
|
+
)
|
|
5747
|
+
return {"success": frag_id is not None, "fragment_id": frag_id}
|
|
5748
|
+
except Exception as e:
|
|
5749
|
+
logger.error(f"Soul capture failed: {e}")
|
|
5750
|
+
return {"success": False, "error": str(e)}
|
|
5751
|
+
|
|
5752
|
+
|
|
5753
|
+
@app.post("/api/soul/integrate")
|
|
5754
|
+
async def api_soul_integrate(request: Request):
|
|
5755
|
+
"""Run soul integration for a session — merges fragments into soul_state."""
|
|
5756
|
+
try:
|
|
5757
|
+
body = await request.json()
|
|
5758
|
+
session_id = body.get("session_id", "")
|
|
5759
|
+
project_path = body.get("project_path", "")
|
|
5760
|
+
|
|
5761
|
+
result = await soul_service.run_soul_integration(session_id, project_path)
|
|
5762
|
+
return {"success": True, **result}
|
|
5763
|
+
except Exception as e:
|
|
5764
|
+
logger.error(f"Soul integration failed: {e}")
|
|
5765
|
+
return {"success": False, "error": str(e)}
|
|
5766
|
+
|
|
5767
|
+
|
|
5648
5768
|
# ============= Aggregated Grounding Context (v2) =============
|
|
5649
5769
|
|
|
5650
5770
|
|
|
@@ -5810,6 +5930,395 @@ async def api_grounding_context(request: Request):
|
|
|
5810
5930
|
}
|
|
5811
5931
|
|
|
5812
5932
|
|
|
5933
|
+
# ============= Session Recovery System (v3.2.0) =============
|
|
5934
|
+
|
|
5935
|
+
|
|
5936
|
+
@app.post("/api/workflow/capture")
|
|
5937
|
+
async def api_workflow_capture(request: Request):
|
|
5938
|
+
"""Capture or upsert a workflow (name + steps + commands).
|
|
5939
|
+
|
|
5940
|
+
If same workflow name exists for this project, increments success_count and merges commands.
|
|
5941
|
+
"""
|
|
5942
|
+
try:
|
|
5943
|
+
body = await request.json()
|
|
5944
|
+
name = body.get("name", "").strip()
|
|
5945
|
+
if not name:
|
|
5946
|
+
return {"success": False, "error": "name required"}
|
|
5947
|
+
|
|
5948
|
+
workflow_id = await db.store_workflow_knowledge(
|
|
5949
|
+
name=name,
|
|
5950
|
+
steps=body.get("steps"),
|
|
5951
|
+
commands=body.get("commands"),
|
|
5952
|
+
tags=body.get("tags"),
|
|
5953
|
+
project_path=body.get("project_path") or None,
|
|
5954
|
+
)
|
|
5955
|
+
return {"success": workflow_id > 0, "workflow_id": workflow_id}
|
|
5956
|
+
except Exception as e:
|
|
5957
|
+
logger.error(f"Workflow capture failed: {e}")
|
|
5958
|
+
return {"success": False, "error": str(e)}
|
|
5959
|
+
|
|
5960
|
+
|
|
5961
|
+
@app.post("/api/session/brain-dump")
|
|
5962
|
+
async def api_session_brain_dump(request: Request):
|
|
5963
|
+
"""Aggregate full session state for writing to MEMORY.md before compaction.
|
|
5964
|
+
|
|
5965
|
+
Runs parallel queries: session_state, latest checkpoint, recent memories,
|
|
5966
|
+
workflow_knowledge, and soul brief.
|
|
5967
|
+
|
|
5968
|
+
Body:
|
|
5969
|
+
session_id: str
|
|
5970
|
+
project_path: str
|
|
5971
|
+
"""
|
|
5972
|
+
try:
|
|
5973
|
+
body = await request.json()
|
|
5974
|
+
except Exception:
|
|
5975
|
+
body = {}
|
|
5976
|
+
|
|
5977
|
+
session_id = body.get("session_id", "")
|
|
5978
|
+
project_path = body.get("project_path", "")
|
|
5979
|
+
|
|
5980
|
+
result = {
|
|
5981
|
+
"success": True,
|
|
5982
|
+
"timestamp": datetime.now().isoformat(),
|
|
5983
|
+
"session_id": session_id,
|
|
5984
|
+
}
|
|
5985
|
+
|
|
5986
|
+
tasks_dict = {}
|
|
5987
|
+
|
|
5988
|
+
# Session state
|
|
5989
|
+
if session_id:
|
|
5990
|
+
async def _get_state():
|
|
5991
|
+
try:
|
|
5992
|
+
cursor = db.conn.cursor()
|
|
5993
|
+
cursor.execute(
|
|
5994
|
+
"SELECT * FROM session_state WHERE session_id = ?",
|
|
5995
|
+
(session_id,)
|
|
5996
|
+
)
|
|
5997
|
+
row = cursor.fetchone()
|
|
5998
|
+
if row:
|
|
5999
|
+
return {
|
|
6000
|
+
"current_goal": row["current_goal"],
|
|
6001
|
+
"entity_registry": json.loads(row["entity_registry"]) if row["entity_registry"] else {},
|
|
6002
|
+
"decisions_summary": row["decisions_summary"],
|
|
6003
|
+
"pending_questions": json.loads(row["pending_questions"]) if row["pending_questions"] else [],
|
|
6004
|
+
}
|
|
6005
|
+
return None
|
|
6006
|
+
except Exception:
|
|
6007
|
+
return None
|
|
6008
|
+
tasks_dict["state"] = _get_state()
|
|
6009
|
+
|
|
6010
|
+
# Latest checkpoint
|
|
6011
|
+
if session_id:
|
|
6012
|
+
tasks_dict["checkpoint"] = db.get_latest_checkpoint(session_id)
|
|
6013
|
+
|
|
6014
|
+
# Recent memories (last 10 high-importance)
|
|
6015
|
+
if project_path:
|
|
6016
|
+
async def _get_recent_memories():
|
|
6017
|
+
try:
|
|
6018
|
+
cursor = db.conn.cursor()
|
|
6019
|
+
cursor.execute(
|
|
6020
|
+
"""SELECT content, type, importance, tags, created_at
|
|
6021
|
+
FROM memories
|
|
6022
|
+
WHERE project_path = ? AND importance >= 6
|
|
6023
|
+
ORDER BY created_at DESC LIMIT 10""",
|
|
6024
|
+
(project_path,)
|
|
6025
|
+
)
|
|
6026
|
+
return [dict(r) for r in cursor.fetchall()]
|
|
6027
|
+
except Exception:
|
|
6028
|
+
return []
|
|
6029
|
+
tasks_dict["memories"] = _get_recent_memories()
|
|
6030
|
+
|
|
6031
|
+
# Workflow knowledge
|
|
6032
|
+
tasks_dict["workflows"] = db.get_workflow_knowledge(
|
|
6033
|
+
project_path=project_path, limit=10
|
|
6034
|
+
)
|
|
6035
|
+
|
|
6036
|
+
# Soul brief
|
|
6037
|
+
if project_path:
|
|
6038
|
+
async def _get_soul():
|
|
6039
|
+
try:
|
|
6040
|
+
return await soul_service.generate_soul_brief(project_path)
|
|
6041
|
+
except Exception:
|
|
6042
|
+
return ""
|
|
6043
|
+
tasks_dict["soul"] = _get_soul()
|
|
6044
|
+
|
|
6045
|
+
# Run all in parallel
|
|
6046
|
+
keys = list(tasks_dict.keys())
|
|
6047
|
+
gathered = await asyncio.gather(
|
|
6048
|
+
*[tasks_dict[k] for k in keys],
|
|
6049
|
+
return_exceptions=True,
|
|
6050
|
+
)
|
|
6051
|
+
|
|
6052
|
+
for k, v in zip(keys, gathered):
|
|
6053
|
+
if isinstance(v, Exception):
|
|
6054
|
+
result[k] = None
|
|
6055
|
+
else:
|
|
6056
|
+
result[k] = v
|
|
6057
|
+
|
|
6058
|
+
return result
|
|
6059
|
+
|
|
6060
|
+
|
|
6061
|
+
@app.post("/api/session/resume")
|
|
6062
|
+
async def api_session_resume(request: Request):
|
|
6063
|
+
"""Complete catch-up package for resuming a session after context clear.
|
|
6064
|
+
|
|
6065
|
+
Returns: goal, decisions, entity registry, learned workflows,
|
|
6066
|
+
recent memories, and soul brief — all in one call.
|
|
6067
|
+
|
|
6068
|
+
Body:
|
|
6069
|
+
session_id: str (optional — uses latest for project if missing)
|
|
6070
|
+
project_path: str
|
|
6071
|
+
"""
|
|
6072
|
+
try:
|
|
6073
|
+
body = await request.json()
|
|
6074
|
+
except Exception:
|
|
6075
|
+
body = {}
|
|
6076
|
+
|
|
6077
|
+
session_id = body.get("session_id", "")
|
|
6078
|
+
project_path = body.get("project_path", "")
|
|
6079
|
+
|
|
6080
|
+
result = {"success": True}
|
|
6081
|
+
|
|
6082
|
+
tasks_dict = {}
|
|
6083
|
+
|
|
6084
|
+
# Session state — try by ID first, fall back to latest for project
|
|
6085
|
+
async def _get_state():
|
|
6086
|
+
try:
|
|
6087
|
+
if session_id:
|
|
6088
|
+
cursor = db.conn.cursor()
|
|
6089
|
+
cursor.execute(
|
|
6090
|
+
"SELECT * FROM session_state WHERE session_id = ?",
|
|
6091
|
+
(session_id,)
|
|
6092
|
+
)
|
|
6093
|
+
row = cursor.fetchone()
|
|
6094
|
+
if row:
|
|
6095
|
+
return {
|
|
6096
|
+
"session_id": row["session_id"],
|
|
6097
|
+
"current_goal": row["current_goal"],
|
|
6098
|
+
"entity_registry": json.loads(row["entity_registry"]) if row["entity_registry"] else {},
|
|
6099
|
+
"decisions_summary": row["decisions_summary"],
|
|
6100
|
+
"pending_questions": json.loads(row["pending_questions"]) if row["pending_questions"] else [],
|
|
6101
|
+
}
|
|
6102
|
+
if project_path:
|
|
6103
|
+
return await db.get_latest_session_for_project(project_path)
|
|
6104
|
+
return None
|
|
6105
|
+
except Exception:
|
|
6106
|
+
return None
|
|
6107
|
+
tasks_dict["session_state"] = _get_state()
|
|
6108
|
+
|
|
6109
|
+
# Latest checkpoint
|
|
6110
|
+
async def _get_checkpoint():
|
|
6111
|
+
try:
|
|
6112
|
+
if session_id:
|
|
6113
|
+
return await db.get_latest_checkpoint(session_id)
|
|
6114
|
+
if project_path:
|
|
6115
|
+
state = await db.get_latest_session_for_project(project_path)
|
|
6116
|
+
if state:
|
|
6117
|
+
return await db.get_latest_checkpoint(state["session_id"])
|
|
6118
|
+
return None
|
|
6119
|
+
except Exception:
|
|
6120
|
+
return None
|
|
6121
|
+
tasks_dict["checkpoint"] = _get_checkpoint()
|
|
6122
|
+
|
|
6123
|
+
# Workflow knowledge
|
|
6124
|
+
tasks_dict["workflows"] = db.get_workflow_knowledge(
|
|
6125
|
+
project_path=project_path, limit=10
|
|
6126
|
+
)
|
|
6127
|
+
|
|
6128
|
+
# Recent high-importance memories
|
|
6129
|
+
async def _get_memories():
|
|
6130
|
+
try:
|
|
6131
|
+
cursor = db.conn.cursor()
|
|
6132
|
+
if project_path:
|
|
6133
|
+
cursor.execute(
|
|
6134
|
+
"""SELECT id, content, type, importance, tags, created_at
|
|
6135
|
+
FROM memories
|
|
6136
|
+
WHERE project_path = ? AND importance >= 5
|
|
6137
|
+
ORDER BY importance DESC, created_at DESC
|
|
6138
|
+
LIMIT 15""",
|
|
6139
|
+
(project_path,)
|
|
6140
|
+
)
|
|
6141
|
+
else:
|
|
6142
|
+
cursor.execute(
|
|
6143
|
+
"""SELECT id, content, type, importance, tags, created_at
|
|
6144
|
+
FROM memories
|
|
6145
|
+
WHERE importance >= 6
|
|
6146
|
+
ORDER BY importance DESC, created_at DESC
|
|
6147
|
+
LIMIT 10"""
|
|
6148
|
+
)
|
|
6149
|
+
return [dict(r) for r in cursor.fetchall()]
|
|
6150
|
+
except Exception:
|
|
6151
|
+
return []
|
|
6152
|
+
tasks_dict["memories"] = _get_memories()
|
|
6153
|
+
|
|
6154
|
+
# Soul brief
|
|
6155
|
+
async def _get_soul():
|
|
6156
|
+
try:
|
|
6157
|
+
if project_path:
|
|
6158
|
+
return await soul_service.generate_soul_brief(project_path)
|
|
6159
|
+
return ""
|
|
6160
|
+
except Exception:
|
|
6161
|
+
return ""
|
|
6162
|
+
tasks_dict["soul_brief"] = _get_soul()
|
|
6163
|
+
|
|
6164
|
+
# Run all in parallel
|
|
6165
|
+
keys = list(tasks_dict.keys())
|
|
6166
|
+
gathered = await asyncio.gather(
|
|
6167
|
+
*[tasks_dict[k] for k in keys],
|
|
6168
|
+
return_exceptions=True,
|
|
6169
|
+
)
|
|
6170
|
+
|
|
6171
|
+
for k, v in zip(keys, gathered):
|
|
6172
|
+
result[k] = v if not isinstance(v, Exception) else None
|
|
6173
|
+
|
|
6174
|
+
return result
|
|
6175
|
+
|
|
6176
|
+
|
|
6177
|
+
@app.post("/api/grounding-context/rich")
|
|
6178
|
+
async def api_grounding_context_rich(request: Request):
|
|
6179
|
+
"""Rich multi-section grounding context for fresh sessions (~500-800 tokens).
|
|
6180
|
+
|
|
6181
|
+
Used by grounding hook when detecting a fresh/resumed session.
|
|
6182
|
+
Includes soul brief, checkpoint summary, workflows, entities, pending items.
|
|
6183
|
+
|
|
6184
|
+
Body:
|
|
6185
|
+
session_id: str
|
|
6186
|
+
project_path: str
|
|
6187
|
+
|
|
6188
|
+
Returns:
|
|
6189
|
+
{"success": true, "context": "[MEM:RESUME] ...", "token_estimate": N}
|
|
6190
|
+
"""
|
|
6191
|
+
try:
|
|
6192
|
+
body = await request.json()
|
|
6193
|
+
except Exception:
|
|
6194
|
+
body = {}
|
|
6195
|
+
|
|
6196
|
+
session_id = body.get("session_id", "")
|
|
6197
|
+
project_path = body.get("project_path", "")
|
|
6198
|
+
|
|
6199
|
+
tasks_dict = {}
|
|
6200
|
+
|
|
6201
|
+
# Session state (by ID or latest for project)
|
|
6202
|
+
async def _get_state():
|
|
6203
|
+
try:
|
|
6204
|
+
if session_id:
|
|
6205
|
+
cursor = db.conn.cursor()
|
|
6206
|
+
cursor.execute(
|
|
6207
|
+
"SELECT * FROM session_state WHERE session_id = ?",
|
|
6208
|
+
(session_id,)
|
|
6209
|
+
)
|
|
6210
|
+
row = cursor.fetchone()
|
|
6211
|
+
if row:
|
|
6212
|
+
return {
|
|
6213
|
+
"current_goal": row["current_goal"],
|
|
6214
|
+
"entity_registry": json.loads(row["entity_registry"]) if row["entity_registry"] else {},
|
|
6215
|
+
"decisions_summary": row["decisions_summary"],
|
|
6216
|
+
"pending_questions": json.loads(row["pending_questions"]) if row["pending_questions"] else [],
|
|
6217
|
+
}
|
|
6218
|
+
if project_path:
|
|
6219
|
+
return await db.get_latest_session_for_project(project_path)
|
|
6220
|
+
return None
|
|
6221
|
+
except Exception:
|
|
6222
|
+
return None
|
|
6223
|
+
tasks_dict["state"] = _get_state()
|
|
6224
|
+
|
|
6225
|
+
# Latest checkpoint
|
|
6226
|
+
async def _get_checkpoint():
|
|
6227
|
+
try:
|
|
6228
|
+
if session_id:
|
|
6229
|
+
return await db.get_latest_checkpoint(session_id)
|
|
6230
|
+
if project_path:
|
|
6231
|
+
st = await db.get_latest_session_for_project(project_path)
|
|
6232
|
+
if st:
|
|
6233
|
+
return await db.get_latest_checkpoint(st["session_id"])
|
|
6234
|
+
return None
|
|
6235
|
+
except Exception:
|
|
6236
|
+
return None
|
|
6237
|
+
tasks_dict["checkpoint"] = _get_checkpoint()
|
|
6238
|
+
|
|
6239
|
+
# Workflow knowledge (top 5)
|
|
6240
|
+
tasks_dict["workflows"] = db.get_workflow_knowledge(
|
|
6241
|
+
project_path=project_path, limit=5
|
|
6242
|
+
)
|
|
6243
|
+
|
|
6244
|
+
# Soul brief
|
|
6245
|
+
async def _get_soul():
|
|
6246
|
+
try:
|
|
6247
|
+
if project_path:
|
|
6248
|
+
return await soul_service.generate_soul_brief(project_path)
|
|
6249
|
+
return ""
|
|
6250
|
+
except Exception:
|
|
6251
|
+
return ""
|
|
6252
|
+
tasks_dict["soul"] = _get_soul()
|
|
6253
|
+
|
|
6254
|
+
# Run all in parallel
|
|
6255
|
+
keys = list(tasks_dict.keys())
|
|
6256
|
+
gathered = await asyncio.gather(
|
|
6257
|
+
*[tasks_dict[k] for k in keys],
|
|
6258
|
+
return_exceptions=True,
|
|
6259
|
+
)
|
|
6260
|
+
results = {}
|
|
6261
|
+
for k, v in zip(keys, gathered):
|
|
6262
|
+
results[k] = v if not isinstance(v, Exception) else None
|
|
6263
|
+
|
|
6264
|
+
# -- Build rich context (budget ~550 tokens / 2200 chars) --
|
|
6265
|
+
sections = []
|
|
6266
|
+
|
|
6267
|
+
# Soul brief (cap 100 tokens)
|
|
6268
|
+
soul = results.get("soul") or ""
|
|
6269
|
+
if soul:
|
|
6270
|
+
sections.append(f"## Soul\n{soul[:400]}")
|
|
6271
|
+
|
|
6272
|
+
# Goal & decisions
|
|
6273
|
+
state = results.get("state")
|
|
6274
|
+
if state and isinstance(state, dict):
|
|
6275
|
+
goal = state.get("current_goal", "")
|
|
6276
|
+
if goal:
|
|
6277
|
+
sections.append(f"## Goal\n{goal[:150]}")
|
|
6278
|
+
decisions = state.get("decisions_summary", "")
|
|
6279
|
+
if decisions:
|
|
6280
|
+
sections.append(f"## Decisions\n{decisions[:300]}")
|
|
6281
|
+
pending = state.get("pending_questions") or state.get("pending_items") or []
|
|
6282
|
+
if pending:
|
|
6283
|
+
items = [f"- {p[:60]}" for p in pending[:5]]
|
|
6284
|
+
sections.append(f"## Pending\n" + "\n".join(items))
|
|
6285
|
+
entities = state.get("entity_registry") or {}
|
|
6286
|
+
if entities:
|
|
6287
|
+
ent_lines = [f"- {k}: {v[:40]}" for k, v in list(entities.items())[:8]]
|
|
6288
|
+
sections.append(f"## Entities\n" + "\n".join(ent_lines))
|
|
6289
|
+
|
|
6290
|
+
# Checkpoint summary
|
|
6291
|
+
cp = results.get("checkpoint")
|
|
6292
|
+
if cp and isinstance(cp, dict):
|
|
6293
|
+
summary = cp.get("summary", "")
|
|
6294
|
+
if summary:
|
|
6295
|
+
sections.append(f"## Last Checkpoint\n{summary[:200]}")
|
|
6296
|
+
|
|
6297
|
+
# Workflows
|
|
6298
|
+
workflows = results.get("workflows") or []
|
|
6299
|
+
if workflows:
|
|
6300
|
+
wf_lines = []
|
|
6301
|
+
for wf in workflows[:5]:
|
|
6302
|
+
cmds = wf.get("commands", [])
|
|
6303
|
+
cmd_str = " -> ".join(cmds[:3]) if cmds else ""
|
|
6304
|
+
wf_lines.append(f"- **{wf['name']}**: {cmd_str[:80]}")
|
|
6305
|
+
sections.append(f"## Workflows\n" + "\n".join(wf_lines))
|
|
6306
|
+
|
|
6307
|
+
if sections:
|
|
6308
|
+
context = "[MEM:RESUME]\n" + "\n\n".join(sections)
|
|
6309
|
+
# Hard cap at ~2200 chars to stay within token budget
|
|
6310
|
+
if len(context) > 2200:
|
|
6311
|
+
context = context[:2200] + "\n..."
|
|
6312
|
+
else:
|
|
6313
|
+
context = ""
|
|
6314
|
+
|
|
6315
|
+
return {
|
|
6316
|
+
"success": True,
|
|
6317
|
+
"context": context,
|
|
6318
|
+
"token_estimate": len(context.split()),
|
|
6319
|
+
}
|
|
6320
|
+
|
|
6321
|
+
|
|
5813
6322
|
if __name__ == "__main__":
|
|
5814
6323
|
import uvicorn
|
|
5815
6324
|
uvicorn.run(
|