nexo-brain 5.3.26 → 5.3.27

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 (211) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/server.py +3 -0
  4. package/src/tools_sessions.py +6 -1
  5. package/src/dashboard/static/favicon 2.svg +0 -32
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  8. package/src/dashboard/static/style 2.css +0 -2458
  9. package/src/dashboard/templates/adaptive 2.html +0 -118
  10. package/src/dashboard/templates/artifacts 2.html +0 -133
  11. package/src/dashboard/templates/backups 2.html +0 -136
  12. package/src/dashboard/templates/base 2.html +0 -417
  13. package/src/dashboard/templates/calendar 2.html +0 -591
  14. package/src/dashboard/templates/chat 2.html +0 -356
  15. package/src/dashboard/templates/claims 2.html +0 -259
  16. package/src/dashboard/templates/cortex 2.html +0 -321
  17. package/src/dashboard/templates/credentials 2.html +0 -128
  18. package/src/dashboard/templates/crons 2.html +0 -370
  19. package/src/dashboard/templates/dashboard 2.html +0 -494
  20. package/src/dashboard/templates/dreams 2.html +0 -252
  21. package/src/dashboard/templates/email 2.html +0 -160
  22. package/src/dashboard/templates/evolution 2.html +0 -189
  23. package/src/dashboard/templates/feed 2.html +0 -249
  24. package/src/dashboard/templates/followup_health 2.html +0 -170
  25. package/src/dashboard/templates/graph 2.html +0 -201
  26. package/src/dashboard/templates/guard 2.html +0 -259
  27. package/src/dashboard/templates/inbox 2.html +0 -251
  28. package/src/dashboard/templates/memory 2.html +0 -420
  29. package/src/dashboard/templates/operations 2.html +0 -608
  30. package/src/dashboard/templates/plugins 2.html +0 -185
  31. package/src/dashboard/templates/protocol 2.html +0 -199
  32. package/src/dashboard/templates/rules 2.html +0 -246
  33. package/src/dashboard/templates/sentiment 2.html +0 -247
  34. package/src/dashboard/templates/sessions 2.html +0 -218
  35. package/src/dashboard/templates/skills 2.html +0 -329
  36. package/src/dashboard/templates/somatic 2.html +0 -73
  37. package/src/dashboard/templates/triggers 2.html +0 -133
  38. package/src/dashboard/templates/trust 2.html +0 -360
  39. package/src/db/__init__ 2.py +0 -259
  40. package/src/db/_core 2.py +0 -437
  41. package/src/db/_credentials 2.py +0 -124
  42. package/src/db/_episodic 2.py +0 -762
  43. package/src/db/_evolution 2.py +0 -54
  44. package/src/db/_fts 2.py +0 -406
  45. package/src/db/_goal_profiles 2.py +0 -376
  46. package/src/db/_hot_context 2.py +0 -660
  47. package/src/db/_outcomes 2.py +0 -800
  48. package/src/db/_personal_scripts 2.py +0 -582
  49. package/src/db/_sessions 2.py +0 -330
  50. package/src/db/_tasks 2.py +0 -91
  51. package/src/db/_watchers 2.py +0 -173
  52. package/src/doctor/formatters 2.py +0 -52
  53. package/src/doctor/models 2.py +0 -69
  54. package/src/doctor/planes 2.py +0 -87
  55. package/src/doctor/providers/__init__ 2.py +0 -1
  56. package/src/doctor/providers/deep 2.py +0 -367
  57. package/src/evolution_cycle 2.py +0 -519
  58. package/src/hooks/auto_capture 2.py +0 -208
  59. package/src/hooks/caffeinate-guard 2.sh +0 -8
  60. package/src/hooks/capture-session 2.sh +0 -21
  61. package/src/hooks/capture-tool-logs 2.sh +0 -158
  62. package/src/hooks/daily-briefing-check 2.sh +0 -33
  63. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  64. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  65. package/src/hooks/inbox-hook 2.sh +0 -76
  66. package/src/hooks/post-compact 2.sh +0 -152
  67. package/src/hooks/pre-compact 2.sh +0 -169
  68. package/src/hooks/protocol-guardrail 2.sh +0 -10
  69. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  70. package/src/hooks/session-stop 2.sh +0 -52
  71. package/src/kg_populate 2.py +0 -292
  72. package/src/maintenance 2.py +0 -53
  73. package/src/memory_backends 2.py +0 -71
  74. package/src/migrate_embeddings 2.py +0 -124
  75. package/src/nexo_sdk 2.py +0 -103
  76. package/src/observability 2.py +0 -199
  77. package/src/plugin_loader 2.py +0 -217
  78. package/src/plugins/__init__ 2.py +0 -0
  79. package/src/plugins/artifact_registry 2.py +0 -450
  80. package/src/plugins/backup 2.py +0 -127
  81. package/src/plugins/claims_tools 2.py +0 -119
  82. package/src/plugins/cognitive_memory 2.py +0 -609
  83. package/src/plugins/core_rules 2.py +0 -252
  84. package/src/plugins/cortex 2.py +0 -1155
  85. package/src/plugins/entities 2.py +0 -67
  86. package/src/plugins/episodic_memory 2.py +0 -560
  87. package/src/plugins/evolution 2.py +0 -167
  88. package/src/plugins/goal_engine 2.py +0 -142
  89. package/src/plugins/guard 2.py +0 -862
  90. package/src/plugins/impact 2.py +0 -29
  91. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  92. package/src/plugins/media_memory_tools 2.py +0 -98
  93. package/src/plugins/memory_export 2.py +0 -196
  94. package/src/plugins/outcomes 2.py +0 -130
  95. package/src/plugins/personal_scripts 2.py +0 -117
  96. package/src/plugins/preferences 2.py +0 -47
  97. package/src/plugins/protocol 2.py +0 -1449
  98. package/src/plugins/simple_api 2.py +0 -106
  99. package/src/plugins/skills 2.py +0 -341
  100. package/src/plugins/state_watchers 2.py +0 -79
  101. package/src/plugins/update 2.py +0 -986
  102. package/src/plugins/user_state_tools 2.py +0 -43
  103. package/src/plugins/workflow 2.py +0 -588
  104. package/src/protocol_settings 2.py +0 -59
  105. package/src/public_contribution 2.py +0 -466
  106. package/src/public_evolution_queue 2.py +0 -241
  107. package/src/requirements 2.txt +0 -14
  108. package/src/retroactive_learnings 2.py +0 -373
  109. package/src/rules/__init__ 2.py +0 -0
  110. package/src/rules/core-rules 2.json +0 -331
  111. package/src/rules/migrate 2.py +0 -207
  112. package/src/runtime_power 2.py +0 -874
  113. package/src/script_registry 2.py +0 -1559
  114. package/src/scripts/check-context 2.py +0 -272
  115. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  116. package/src/scripts/deep-sleep/collect 2.py +0 -928
  117. package/src/scripts/deep-sleep/extract 2.py +0 -330
  118. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  119. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  120. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  121. package/src/scripts/nexo-agent-run 2.py +0 -75
  122. package/src/scripts/nexo-auto-update 2.py +0 -6
  123. package/src/scripts/nexo-backup 2.sh +0 -25
  124. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  125. package/src/scripts/nexo-catchup 2.py +0 -300
  126. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  127. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  128. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  129. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  130. package/src/scripts/nexo-dashboard 2.sh +0 -29
  131. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  132. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  133. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  134. package/src/scripts/nexo-hook-record 2.py +0 -42
  135. package/src/scripts/nexo-immune 2.py +0 -936
  136. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  137. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  138. package/src/scripts/nexo-install 2.py +0 -6
  139. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  140. package/src/scripts/nexo-learning-validator 2.py +0 -266
  141. package/src/scripts/nexo-migrate 2.py +0 -260
  142. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  143. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  144. package/src/scripts/nexo-pre-commit 2.py +0 -120
  145. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  146. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  147. package/src/scripts/nexo-reflection 2.py +0 -256
  148. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  149. package/src/scripts/nexo-sleep 2.py +0 -631
  150. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  151. package/src/scripts/nexo-sync-clients 2.py +0 -16
  152. package/src/scripts/nexo-synthesis 2.py +0 -475
  153. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  154. package/src/scripts/nexo-update 2.sh +0 -306
  155. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  156. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  157. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  158. package/src/server 2.py +0 -1296
  159. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  160. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  161. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  162. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  163. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  164. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  165. package/src/skills/run-release-final-audit/script 2.py +0 -259
  166. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  167. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  168. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  169. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  170. package/src/skills_runtime 2.py +0 -932
  171. package/src/state_watchers_runtime 2.py +0 -475
  172. package/src/storage_router 2.py +0 -32
  173. package/src/system_catalog 2.py +0 -786
  174. package/src/tools_coordination 2.py +0 -103
  175. package/src/tools_credentials 2.py +0 -68
  176. package/src/tools_drive 2.py +0 -487
  177. package/src/tools_hot_context 2.py +0 -163
  178. package/src/tools_learnings 2.py +0 -612
  179. package/src/tools_menu 2.py +0 -229
  180. package/src/tools_reminders 2.py +0 -88
  181. package/src/tools_reminders_crud 2.py +0 -363
  182. package/src/tools_sessions 2.py +0 -1054
  183. package/src/tools_system_catalog 2.py +0 -19
  184. package/src/tools_task_history 2.py +0 -57
  185. package/src/tools_transcripts 2.py +0 -98
  186. package/src/transcript_utils 2.py +0 -412
  187. package/src/user_context 2.py +0 -46
  188. package/src/user_data_portability 2.py +0 -328
  189. package/src/user_state_model 2.py +0 -170
  190. package/templates/CLAUDE.md 2.template +0 -108
  191. package/templates/CODEX.AGENTS.md 2.template +0 -66
  192. package/templates/launchagents/README 2.md +0 -132
  193. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  194. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  196. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  197. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  199. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  200. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  201. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  202. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  203. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  204. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  205. package/templates/nexo_helper 2.py +0 -301
  206. package/templates/openclaw 2.json +0 -13
  207. package/templates/plugin-template 2.py +0 -40
  208. package/templates/script-template 2.py +0 -59
  209. package/templates/script-template 2.sh +0 -13
  210. package/templates/skill-script-template 2.py +0 -48
  211. package/templates/skill-template 2.md +0 -33
@@ -1,53 +0,0 @@
1
- """Placeholder module for historical `maintenance` runner.
2
-
3
- HISTORICAL CONTEXT (NEXO-AUDIT-2026-04-11 finding, Learning #194)
4
- ==================================================================
5
-
6
- This module previously exposed `check_and_run_overdue()` and a private
7
- `_run_task()` dispatcher that walked the `maintenance_schedule` table and
8
- executed cognitive decay, synthesis, self audit, weight learning, somatic
9
- decay, somatic projection, drive decay and graph maintenance as needed.
10
-
11
- None of that code was ever called from anywhere. A repository-wide grep
12
- for `check_and_run_overdue`, `from maintenance`, `import maintenance` and
13
- `maintenance.check` produced zero hits during the 2026-04-11 audit. Each
14
- of those tasks actually runs from its own LaunchAgent via its own script
15
- in `src/scripts/` (e.g. `nexo-cognitive-decay.py`, `nexo-daily-self-audit
16
- .py`, `nexo-evolution-run.py`), not through this dispatcher. The
17
- `maintenance_schedule` table in SQLite is still populated by migrations
18
- (see `db/_schema.py::_m9_maintenance_schedule` and the drive_decay
19
- registration) but is effectively dead data: nothing reads it.
20
-
21
- Why the dispatcher was removed
22
- ------------------------------
23
- The dead dispatcher was actively misleading: developers reading the code
24
- could reasonably conclude that adding a row to `maintenance_schedule`
25
- would cause the named task to run. It would not. That false contract
26
- was the root of a near-miss during the Item 9 fix of the 2026-04-11
27
- audit, where the first plan was to register a new `read_token_cleanup`
28
- task via this mechanism before discovering that the mechanism is never
29
- invoked. See `src/db/_reminders.py::_purge_expired_read_tokens_if_due`
30
- for the opportunistic-cleanup pattern that replaced that initial plan.
31
-
32
- What to do if you need scheduled maintenance
33
- --------------------------------------------
34
- Do NOT reintroduce a dispatcher in this module. Pick one of:
35
-
36
- * Add your work to an existing LaunchAgent script under
37
- `src/scripts/` that already runs on the cadence you need.
38
- * Register a new personal script via `nexo_personal_script_create` and
39
- let the schedule/LaunchAgent system handle it.
40
- * Run the cleanup opportunistically inside the hot path, throttled
41
- by wall-clock (see `_purge_expired_read_tokens_if_due`).
42
-
43
- What happens to the `maintenance_schedule` table
44
- -------------------------------------------------
45
- The table is intentionally left in place. Removing it would require a
46
- destructive migration for every installed user with no benefit — the
47
- rows do no harm, cost a few KB each, and their removal is deferred to a
48
- future cleanup pass when migration numbering is renegotiated.
49
- """
50
-
51
- from __future__ import annotations
52
-
53
- __all__: list[str] = []
@@ -1,71 +0,0 @@
1
- from __future__ import annotations
2
-
3
- """Explicit backend registry for memory expansion layers.
4
-
5
- NEXO's historical memory system is still heavily SQLite-shaped, but newer layers
6
- should not keep backend assumptions implicit forever. This module introduces a
7
- small registry/contract that expansion surfaces can use today while SQLite
8
- remains the default backend.
9
- """
10
-
11
- from dataclasses import dataclass
12
- import os
13
-
14
-
15
- @dataclass(frozen=True)
16
- class MemoryBackendInfo:
17
- key: str
18
- label: str
19
- description: str
20
- supports: tuple[str, ...]
21
- maturity: str = "stable"
22
-
23
-
24
- _REGISTRY: dict[str, MemoryBackendInfo] = {}
25
-
26
-
27
- def register_backend(info: MemoryBackendInfo) -> None:
28
- _REGISTRY[info.key] = info
29
-
30
-
31
- def active_backend_key() -> str:
32
- return (os.environ.get("NEXO_MEMORY_BACKEND", "sqlite") or "sqlite").strip().lower()
33
-
34
-
35
- def get_backend(key: str = "") -> MemoryBackendInfo:
36
- selected = (key or active_backend_key()).strip().lower()
37
- return _REGISTRY.get(selected, _REGISTRY["sqlite"])
38
-
39
-
40
- def list_backends() -> list[dict]:
41
- active = active_backend_key()
42
- results = []
43
- for key in sorted(_REGISTRY):
44
- info = _REGISTRY[key]
45
- item = {
46
- "key": info.key,
47
- "label": info.label,
48
- "description": info.description,
49
- "supports": list(info.supports),
50
- "maturity": info.maturity,
51
- "active": info.key == active,
52
- }
53
- results.append(item)
54
- return results
55
-
56
-
57
- register_backend(
58
- MemoryBackendInfo(
59
- key="sqlite",
60
- label="SQLite + FTS5",
61
- description="Local-first default backend used by NEXO runtime surfaces.",
62
- supports=(
63
- "cognitive_core",
64
- "claims",
65
- "media_memory",
66
- "user_state",
67
- "memory_export",
68
- "auto_flush",
69
- ),
70
- )
71
- )
@@ -1,124 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Migrate cognitive.db embeddings between models.
4
-
5
- Usage:
6
- python migrate_embeddings.py upgrade # 384 → 768 (bge-small → bge-base)
7
- python migrate_embeddings.py rollback # Restore from backup
8
- python migrate_embeddings.py verify # Check current embedding dims
9
- """
10
-
11
- import os
12
- import shutil
13
- import sqlite3
14
- import sys
15
- import time
16
- import numpy as np
17
-
18
- NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
19
- _data_dir = os.path.join(NEXO_HOME, "data")
20
- os.makedirs(_data_dir, exist_ok=True)
21
- DB_PATH = os.path.join(_data_dir, "cognitive.db")
22
- BACKUP_PATH = DB_PATH + ".bak-384dims-pre-upgrade"
23
-
24
- MODELS = {
25
- "small": ("BAAI/bge-small-en-v1.5", 384),
26
- "base": ("BAAI/bge-base-en-v1.5", 768),
27
- }
28
-
29
-
30
- def verify():
31
- """Check current embedding dimensions in the database."""
32
- conn = sqlite3.connect(DB_PATH)
33
- try:
34
- for table in ["stm_memories", "ltm_memories"]:
35
- count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
36
- if count == 0:
37
- print(f" {table}: {count} rows (empty)")
38
- continue
39
- row = conn.execute(f"SELECT embedding FROM {table} LIMIT 1").fetchone()
40
- vec = np.frombuffer(row[0], dtype=np.float32)
41
- print(f" {table}: {count} rows, embedding dim = {len(vec)}")
42
- finally:
43
- conn.close()
44
-
45
-
46
- def upgrade():
47
- """Re-embed all memories from bge-small (384) to bge-base (768)."""
48
- from fastembed import TextEmbedding
49
-
50
- # Verify current state
51
- print("Current state:")
52
- verify()
53
-
54
- # Verify backup exists
55
- if not os.path.exists(BACKUP_PATH):
56
- print(f"\nCreating backup at {BACKUP_PATH}")
57
- shutil.copy2(DB_PATH, BACKUP_PATH)
58
- else:
59
- print(f"\nBackup already exists at {BACKUP_PATH}")
60
-
61
- # Load new model
62
- model_name, expected_dim = MODELS["base"]
63
- print(f"\nLoading {model_name}...")
64
- model = TextEmbedding(model_name)
65
-
66
- conn = sqlite3.connect(DB_PATH)
67
- try:
68
- for table in ["stm_memories", "ltm_memories"]:
69
- rows = conn.execute(f"SELECT id, content FROM {table}").fetchall()
70
- if not rows:
71
- print(f"\n{table}: empty, skipping")
72
- continue
73
-
74
- print(f"\n{table}: re-embedding {len(rows)} memories...")
75
- t0 = time.time()
76
-
77
- # Batch embed for speed
78
- contents = [r[1] for r in rows]
79
- ids = [r[0] for r in rows]
80
-
81
- embeddings = list(model.embed(contents))
82
-
83
- for mem_id, emb in zip(ids, embeddings):
84
- blob = np.array(emb, dtype=np.float32).tobytes()
85
- conn.execute(f"UPDATE {table} SET embedding = ? WHERE id = ?", (blob, mem_id))
86
-
87
- conn.commit()
88
- elapsed = time.time() - t0
89
- print(f" Done: {len(rows)} memories in {elapsed:.1f}s ({elapsed/len(rows)*1000:.0f}ms/memory)")
90
- finally:
91
- conn.close()
92
-
93
- print("\nAfter upgrade:")
94
- verify()
95
- print("\nUpgrade complete. Run 'verify' to confirm.")
96
-
97
-
98
- def rollback():
99
- """Restore database from pre-upgrade backup."""
100
- if not os.path.exists(BACKUP_PATH):
101
- print(f"ERROR: Backup not found at {BACKUP_PATH}")
102
- sys.exit(1)
103
-
104
- print(f"Restoring from {BACKUP_PATH}...")
105
- shutil.copy2(BACKUP_PATH, DB_PATH)
106
- print("Restored. Current state:")
107
- verify()
108
-
109
-
110
- if __name__ == "__main__":
111
- if len(sys.argv) < 2:
112
- print("Usage: python migrate_embeddings.py [upgrade|rollback|verify]")
113
- sys.exit(1)
114
-
115
- cmd = sys.argv[1]
116
- if cmd == "upgrade":
117
- upgrade()
118
- elif cmd == "rollback":
119
- rollback()
120
- elif cmd == "verify":
121
- verify()
122
- else:
123
- print(f"Unknown command: {cmd}")
124
- sys.exit(1)
@@ -1,103 +0,0 @@
1
- """Minimal Python SDK for the public NEXO mental model."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import subprocess
7
- from dataclasses import dataclass
8
-
9
-
10
- @dataclass
11
- class NEXOClient:
12
- """Tiny Python wrapper around `nexo call` for common public operations."""
13
-
14
- nexo_bin: str = "nexo"
15
-
16
- def call(self, tool: str, payload: dict | None = None) -> dict | list | str:
17
- result = subprocess.run(
18
- [
19
- self.nexo_bin,
20
- "call",
21
- tool,
22
- "--input",
23
- json.dumps(payload or {}, ensure_ascii=False),
24
- "--json-output",
25
- ],
26
- capture_output=True,
27
- text=True,
28
- check=False,
29
- )
30
- if result.returncode != 0:
31
- raise RuntimeError((result.stderr or result.stdout or f"{tool} failed").strip())
32
- text = (result.stdout or "").strip()
33
- if not text:
34
- return {}
35
- try:
36
- return json.loads(text)
37
- except json.JSONDecodeError:
38
- return {"result": text}
39
-
40
- def remember(
41
- self,
42
- content: str,
43
- *,
44
- title: str = "",
45
- domain: str = "",
46
- source_type: str = "note",
47
- tags: str = "",
48
- bypass_gate: bool = True,
49
- ) -> dict | list | str:
50
- return self.call(
51
- "nexo_remember",
52
- {
53
- "content": content,
54
- "title": title,
55
- "domain": domain,
56
- "source_type": source_type,
57
- "tags": tags,
58
- "bypass_gate": bypass_gate,
59
- },
60
- )
61
-
62
- def recall(self, query: str, *, days: int = 30) -> dict | list | str:
63
- return self.call("nexo_memory_recall", {"query": query, "days": days})
64
-
65
- def consolidate(
66
- self,
67
- *,
68
- max_insights: int = 12,
69
- threshold: float = 0.9,
70
- dry_run: bool = False,
71
- ) -> dict | list | str:
72
- return self.call(
73
- "nexo_consolidate",
74
- {
75
- "max_insights": max_insights,
76
- "threshold": threshold,
77
- "dry_run": dry_run,
78
- },
79
- )
80
-
81
- def run_workflow(
82
- self,
83
- sid: str,
84
- goal: str,
85
- *,
86
- steps: list[dict] | str,
87
- goal_id: str = "",
88
- shared_state: dict | str | None = None,
89
- owner: str = "",
90
- idempotency_key: str = "",
91
- ) -> dict | list | str:
92
- return self.call(
93
- "nexo_run_workflow",
94
- {
95
- "sid": sid,
96
- "goal": goal,
97
- "steps": steps if isinstance(steps, str) else json.dumps(steps, ensure_ascii=False),
98
- "goal_id": goal_id,
99
- "shared_state": shared_state if isinstance(shared_state, str) else json.dumps(shared_state or {}, ensure_ascii=False),
100
- "owner": owner,
101
- "idempotency_key": idempotency_key,
102
- },
103
- )
@@ -1,199 +0,0 @@
1
- """OpenTelemetry observability for NEXO Brain — Fase 5 item 2.
2
-
3
- Closes Fase 5 item 2 of NEXO-AUDIT-2026-04-11. The audit asked for
4
- observability with OTEL / Langfuse / Phoenix integration. This module
5
- is the soft-import host: it adds tracing primitives that NEXO can call
6
- unconditionally, but only emit real spans when:
7
-
8
- 1. The opentelemetry-api package is installed in the user's environment
9
- (`pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp`).
10
- 2. The OTEL_EXPORTER_OTLP_ENDPOINT environment variable is set
11
- (or OTEL_SERVICE_NAME is set, indicating the user already
12
- bootstrapped a tracer provider externally).
13
-
14
- Otherwise every primitive is a no-op so the runtime cost on installs
15
- without OTEL is exactly zero (no try/except per call site, no extra
16
- import time on the hot path).
17
-
18
- Why this design:
19
- - NEXO core does NOT require opentelemetry as a hard dependency.
20
- The 10k+ active users would have an extra 30 MB in their venv with
21
- no benefit unless they opted in.
22
- - Users who DO want telemetry get a single env var to flip on.
23
- - The shape (span name, attributes, status) follows the OpenTelemetry
24
- semantic conventions for "ai.tool" so dashboards in Langfuse,
25
- Arize Phoenix, Honeycomb, Jaeger, and Grafana Tempo all render
26
- NEXO traces with their built-in views.
27
-
28
- Usage:
29
-
30
- from observability import tool_span
31
-
32
- with tool_span("nexo_heartbeat", attributes={"sid": sid}) as span:
33
- result = handle_heartbeat(sid, task)
34
- if span is not None:
35
- span.set_attribute("nexo.heartbeat.task", task[:200])
36
- return result
37
-
38
- The `with` block always works whether or not OTEL is installed; the
39
- span is None when telemetry is disabled.
40
- """
41
-
42
- from __future__ import annotations
43
-
44
- import os
45
- from contextlib import contextmanager
46
- from typing import Any, Iterator
47
-
48
-
49
- # ── OTEL availability detection ──────────────────────────────────────────
50
-
51
-
52
- _otel_available: bool | None = None
53
- _tracer = None # cached after first use
54
-
55
-
56
- def _truthy_env(var: str) -> bool:
57
- return bool((os.environ.get(var) or "").strip())
58
-
59
-
60
- def is_otel_enabled() -> bool:
61
- """Return True if OpenTelemetry is installed AND a configuration is set.
62
-
63
- The two activation conditions are:
64
- - opentelemetry-api importable
65
- - One of OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_SERVICE_NAME is set
66
- (the latter signals an externally-bootstrapped tracer provider).
67
-
68
- Cached after first call so the hot path is a single bool lookup.
69
- """
70
- global _otel_available
71
- if _otel_available is not None:
72
- return _otel_available
73
-
74
- if not (_truthy_env("OTEL_EXPORTER_OTLP_ENDPOINT") or _truthy_env("OTEL_SERVICE_NAME")):
75
- _otel_available = False
76
- return False
77
-
78
- try:
79
- import opentelemetry # noqa: F401
80
- from opentelemetry import trace # noqa: F401
81
- except ImportError:
82
- _otel_available = False
83
- return False
84
-
85
- _otel_available = True
86
- return True
87
-
88
-
89
- def get_tracer():
90
- """Return a cached opentelemetry.trace.Tracer or None when OTEL is off.
91
-
92
- Lazy: only constructs the tracer the first time it is needed so
93
- installs without OTEL never pay any import cost.
94
- """
95
- global _tracer
96
- if _tracer is not None:
97
- return _tracer
98
- if not is_otel_enabled():
99
- return None
100
- try:
101
- from opentelemetry import trace
102
- _tracer = trace.get_tracer("nexo-brain")
103
- return _tracer
104
- except Exception:
105
- return None
106
-
107
-
108
- # ── span context manager ─────────────────────────────────────────────────
109
-
110
-
111
- @contextmanager
112
- def tool_span(
113
- name: str,
114
- *,
115
- attributes: dict[str, Any] | None = None,
116
- ) -> Iterator[Any]:
117
- """Context manager that emits an OTEL span when telemetry is enabled.
118
-
119
- The span name follows the OTEL semantic convention prefix "ai.tool."
120
- so dashboards that already group by ai.tool.* automatically pick
121
- NEXO traces up.
122
-
123
- On success: status = OK.
124
- On exception: status = ERROR with the exception message recorded as
125
- an attribute, and the exception is re-raised so callers see it.
126
-
127
- When telemetry is disabled, the context manager yields None and
128
- does nothing else — the cost is one is_otel_enabled() bool lookup
129
- plus the empty `with` block, which the Python compiler optimizes.
130
-
131
- Args:
132
- name: short tool name. The full span name becomes "ai.tool.<name>".
133
- attributes: optional dict of OTEL attributes to set on the span.
134
- """
135
- if not is_otel_enabled():
136
- yield None
137
- return
138
-
139
- tracer = get_tracer()
140
- if tracer is None:
141
- yield None
142
- return
143
-
144
- try:
145
- from opentelemetry import trace as _trace
146
- span_name = f"ai.tool.{name}" if not name.startswith("ai.tool.") else name
147
- with tracer.start_as_current_span(span_name) as span:
148
- try:
149
- if attributes:
150
- for key, value in attributes.items():
151
- try:
152
- span.set_attribute(key, value)
153
- except Exception:
154
- # Some values (e.g. dicts) are not OTEL-compatible.
155
- try:
156
- span.set_attribute(key, str(value)[:1000])
157
- except Exception:
158
- pass
159
- yield span
160
- span.set_status(_trace.Status(_trace.StatusCode.OK))
161
- except Exception as exc:
162
- try:
163
- span.record_exception(exc)
164
- span.set_status(
165
- _trace.Status(_trace.StatusCode.ERROR, str(exc)[:300])
166
- )
167
- except Exception:
168
- pass
169
- raise
170
- except Exception:
171
- # If anything goes wrong with the OTEL machinery itself, fall
172
- # back to a no-op so the caller never sees a telemetry-induced
173
- # exception. The original work still runs.
174
- yield None
175
-
176
-
177
- def record_tool_attributes(span: Any, attributes: dict[str, Any]) -> None:
178
- """Set OTEL attributes on a span if it is non-None and OTEL is enabled.
179
-
180
- Convenience helper so callers do not need to write the `if span is
181
- not None` guard at every call site.
182
- """
183
- if span is None:
184
- return
185
- for key, value in (attributes or {}).items():
186
- try:
187
- span.set_attribute(key, value)
188
- except Exception:
189
- try:
190
- span.set_attribute(key, str(value)[:1000])
191
- except Exception:
192
- pass
193
-
194
-
195
- def _reset_for_tests() -> None:
196
- """Test-only helper to reset the cached availability + tracer."""
197
- global _otel_available, _tracer
198
- _otel_available = None
199
- _tracer = None