nexo-brain 7.27.6 → 7.29.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/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/automation_controls.py +7 -0
- package/src/automation_preferences.py +323 -0
- package/src/causal_graph.py +763 -0
- package/src/cli.py +120 -0
- package/src/cli_email.py +95 -0
- package/src/cognitive/_core.py +3 -0
- package/src/cognitive_control_observatory.py +2 -0
- package/src/db/_entities.py +98 -11
- package/src/db/_memory_v2.py +78 -0
- package/src/db/_schema.py +546 -0
- package/src/email_presentation.py +243 -0
- package/src/entity_live_profile.py +1073 -0
- package/src/failure_prevention.py +1052 -0
- package/src/knowledge_graph.py +46 -9
- package/src/local_context/usage_events.py +273 -8
- package/src/memory_executive.py +620 -0
- package/src/memory_utility.py +952 -0
- package/src/morning_briefing.py +281 -0
- package/src/plugin_loader.py +9 -5
- package/src/plugins/entities.py +84 -7
- package/src/plugins/entity_live_profile.py +101 -0
- package/src/plugins/failure_prevention.py +162 -0
- package/src/plugins/memory_export.py +55 -18
- package/src/plugins/protocol.py +37 -0
- package/src/plugins/semantic_layers.py +138 -0
- package/src/pre_answer_router.py +324 -22
- package/src/pre_answer_runtime.py +463 -18
- package/src/script_registry.py +15 -0
- package/src/scripts/nexo-morning-agent.py +118 -69
- package/src/scripts/nexo-send-reply.py +20 -25
- package/src/semantic_layers.py +1153 -0
- package/src/server.py +4 -2
- package/src/tools_sessions.py +88 -31
- package/src/tools_transcripts.py +38 -22
- package/src/user_state_model.py +971 -0
- package/templates/core-prompts/morning-agent-json-output.md +1 -1
- package/templates/core-prompts/morning-agent.md +5 -2
- package/tool-enforcement-map.json +230 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.29.0",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.
|
|
21
|
+
Version `7.29.0` is the current packaged-runtime line. Minor release over v7.28.0 - the morning briefing now has structured content preferences, Desktop-facing briefing presentation state, and per-account email signature support.
|
|
22
|
+
|
|
23
|
+
Previously in `7.28.0`: minor release over v7.27.6 - the Brain memory architecture now links action authorship, reasons, operational state, entity profiles, failure prevention, semantic layers, and release-gated runtime memory benchmarks.
|
|
24
|
+
|
|
25
|
+
Previously in `7.27.6`: patch release over v7.27.5 - operational memory continuity now persists promises as commitments, routes pre-answer questions through evidence-backed memory, and exposes observation-queue convergence in health checks.
|
|
22
26
|
|
|
23
27
|
Previously in `7.27.5`: patch release over v7.27.4 - Desktop onboarding asks for the user's name directly in Spanish with `¿Cómo te llamas?`, matching Desktop fallback copy.
|
|
24
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.29.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
@@ -672,10 +672,17 @@ def get_script_runtime_contract(name: str) -> dict[str, Any]:
|
|
|
672
672
|
blocked_reason = str(recipient_status.get("reason") or "")
|
|
673
673
|
blocked_reason_code = str(recipient_status.get("reason_code") or "")
|
|
674
674
|
|
|
675
|
+
try:
|
|
676
|
+
from automation_preferences import supports_automation_preferences
|
|
677
|
+
preferences_supported = supports_automation_preferences(clean_name)
|
|
678
|
+
except Exception:
|
|
679
|
+
preferences_supported = False
|
|
680
|
+
|
|
675
681
|
return {
|
|
676
682
|
"name": clean_name,
|
|
677
683
|
"toggleable_core": is_toggleable_core_script(clean_name),
|
|
678
684
|
"supports_extra_instructions": supports_operator_extra_instructions(clean_name),
|
|
685
|
+
"supports_automation_preferences": preferences_supported,
|
|
679
686
|
"schedule_configurable": bool(schedule_state.get("schedule_configurable")),
|
|
680
687
|
"schedule_type": str(schedule_state.get("schedule_type") or ""),
|
|
681
688
|
"schedule_source": str(schedule_state.get("schedule_source") or ""),
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Structured operator preferences for product automations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import unicodedata
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
AUTOMATION_PREFERENCES_METADATA_KEY = "automation_preferences"
|
|
13
|
+
SUPPORTED_AUTOMATIONS = {"morning-agent"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MORNING_AGENT_SCHEMA: dict[str, Any] = {
|
|
17
|
+
"schema_version": 1,
|
|
18
|
+
"automation": "morning-agent",
|
|
19
|
+
"title": "Morning briefing content",
|
|
20
|
+
"groups": [
|
|
21
|
+
{
|
|
22
|
+
"id": "content",
|
|
23
|
+
"label": "Content",
|
|
24
|
+
"items": [
|
|
25
|
+
{"id": "priorities", "type": "boolean", "label": "Priorities", "default": True},
|
|
26
|
+
{"id": "agenda", "type": "boolean", "label": "Agenda", "default": True},
|
|
27
|
+
{"id": "reminders", "type": "boolean", "label": "Reminders and tasks", "default": True},
|
|
28
|
+
{"id": "followups", "type": "boolean", "label": "Follow-ups", "default": True},
|
|
29
|
+
{"id": "decisions", "type": "boolean", "label": "Recent decisions", "default": True},
|
|
30
|
+
{"id": "email_activity", "type": "boolean", "label": "Recent sent email", "default": True},
|
|
31
|
+
{"id": "blockers", "type": "boolean", "label": "Blockers and risks", "default": True},
|
|
32
|
+
{"id": "internal_refs", "type": "boolean", "label": "Internal references", "default": False},
|
|
33
|
+
{
|
|
34
|
+
"id": "news",
|
|
35
|
+
"type": "boolean",
|
|
36
|
+
"label": "News",
|
|
37
|
+
"default": False,
|
|
38
|
+
"disabled": True,
|
|
39
|
+
"disabled_reason": "No verified news source is connected yet.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "weather",
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"label": "Weather",
|
|
45
|
+
"default": False,
|
|
46
|
+
"disabled": True,
|
|
47
|
+
"disabled_reason": "No verified weather source is connected yet.",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "style",
|
|
53
|
+
"label": "Style",
|
|
54
|
+
"items": [
|
|
55
|
+
{
|
|
56
|
+
"id": "length",
|
|
57
|
+
"type": "choice",
|
|
58
|
+
"label": "Length",
|
|
59
|
+
"default": "normal",
|
|
60
|
+
"options": ["short", "normal", "detailed"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "tone",
|
|
64
|
+
"type": "choice",
|
|
65
|
+
"label": "Tone",
|
|
66
|
+
"default": "direct",
|
|
67
|
+
"options": ["direct", "warm", "executive", "personal"],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "format",
|
|
71
|
+
"type": "choice",
|
|
72
|
+
"label": "Format",
|
|
73
|
+
"default": "sections",
|
|
74
|
+
"options": ["sections", "bullets", "narrative"],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "delivery",
|
|
80
|
+
"label": "Delivery",
|
|
81
|
+
"items": [
|
|
82
|
+
{
|
|
83
|
+
"id": "quiet_days",
|
|
84
|
+
"type": "choice",
|
|
85
|
+
"label": "Quiet days",
|
|
86
|
+
"default": "summary_if_anything_important",
|
|
87
|
+
"options": ["always_send", "summary_if_anything_important", "skip_if_empty"],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "audience",
|
|
91
|
+
"type": "choice",
|
|
92
|
+
"label": "User type",
|
|
93
|
+
"default": "operator",
|
|
94
|
+
"options": ["operator", "founder", "student", "sales", "technical", "personal"],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _schema_for(name: str) -> dict[str, Any] | None:
|
|
103
|
+
clean = normalize_automation_name(name)
|
|
104
|
+
if clean == "morning-agent":
|
|
105
|
+
return copy.deepcopy(MORNING_AGENT_SCHEMA)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def normalize_automation_name(name: str) -> str:
|
|
110
|
+
return str(name or "").strip().lower().replace("_", "-")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def supports_automation_preferences(name: str) -> bool:
|
|
114
|
+
return normalize_automation_name(name) in SUPPORTED_AUTOMATIONS
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_automation_preference_schema(name: str) -> dict[str, Any]:
|
|
118
|
+
schema = _schema_for(name)
|
|
119
|
+
if not schema:
|
|
120
|
+
return {
|
|
121
|
+
"schema_version": 1,
|
|
122
|
+
"automation": normalize_automation_name(name),
|
|
123
|
+
"title": "Automation content",
|
|
124
|
+
"groups": [],
|
|
125
|
+
}
|
|
126
|
+
return schema
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _iter_schema_items(schema: dict[str, Any]):
|
|
130
|
+
for group in list(schema.get("groups") or []):
|
|
131
|
+
for item in list(group.get("items") or []):
|
|
132
|
+
if isinstance(item, dict) and item.get("id"):
|
|
133
|
+
yield item
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def default_automation_preferences(name: str) -> dict[str, Any]:
|
|
137
|
+
schema = get_automation_preference_schema(name)
|
|
138
|
+
values: dict[str, Any] = {}
|
|
139
|
+
for item in _iter_schema_items(schema):
|
|
140
|
+
values[str(item["id"])] = copy.deepcopy(item.get("default"))
|
|
141
|
+
return {
|
|
142
|
+
"schema_version": int(schema.get("schema_version") or 1),
|
|
143
|
+
"automation": normalize_automation_name(name),
|
|
144
|
+
"values": values,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def validate_automation_preferences(name: str, payload: dict[str, Any] | None) -> dict[str, Any]:
|
|
149
|
+
schema = get_automation_preference_schema(name)
|
|
150
|
+
defaults = default_automation_preferences(name)
|
|
151
|
+
source_values = {}
|
|
152
|
+
if isinstance(payload, dict):
|
|
153
|
+
if isinstance(payload.get("values"), dict):
|
|
154
|
+
source_values = payload.get("values") or {}
|
|
155
|
+
else:
|
|
156
|
+
source_values = payload
|
|
157
|
+
values = dict(defaults["values"])
|
|
158
|
+
warnings: list[str] = []
|
|
159
|
+
for item in _iter_schema_items(schema):
|
|
160
|
+
key = str(item["id"])
|
|
161
|
+
if key not in source_values:
|
|
162
|
+
continue
|
|
163
|
+
if item.get("disabled"):
|
|
164
|
+
values[key] = copy.deepcopy(item.get("default"))
|
|
165
|
+
warnings.append(f"{key}: disabled")
|
|
166
|
+
continue
|
|
167
|
+
kind = str(item.get("type") or "text")
|
|
168
|
+
raw_value = source_values.get(key)
|
|
169
|
+
if kind == "boolean":
|
|
170
|
+
values[key] = bool(raw_value)
|
|
171
|
+
elif kind == "choice":
|
|
172
|
+
options = [str(v) for v in list(item.get("options") or [])]
|
|
173
|
+
clean = str(raw_value or "").strip()
|
|
174
|
+
if clean in options:
|
|
175
|
+
values[key] = clean
|
|
176
|
+
else:
|
|
177
|
+
warnings.append(f"{key}: invalid choice")
|
|
178
|
+
elif kind == "number":
|
|
179
|
+
try:
|
|
180
|
+
values[key] = int(raw_value)
|
|
181
|
+
except Exception:
|
|
182
|
+
warnings.append(f"{key}: invalid number")
|
|
183
|
+
else:
|
|
184
|
+
values[key] = str(raw_value or "").strip()[:1000]
|
|
185
|
+
return {
|
|
186
|
+
"schema_version": int(schema.get("schema_version") or 1),
|
|
187
|
+
"automation": normalize_automation_name(name),
|
|
188
|
+
"values": values,
|
|
189
|
+
"warnings": warnings,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _script_row_for(name_or_path: str) -> tuple[dict, dict] | tuple[None, None]:
|
|
194
|
+
from db import init_db
|
|
195
|
+
from db._personal_scripts import get_personal_script
|
|
196
|
+
from script_registry import resolve_script, sync_personal_scripts
|
|
197
|
+
|
|
198
|
+
init_db()
|
|
199
|
+
sync_personal_scripts()
|
|
200
|
+
script = get_personal_script(name_or_path, include_core=True) or resolve_script(name_or_path)
|
|
201
|
+
if not script and normalize_automation_name(name_or_path) == "morning-agent":
|
|
202
|
+
script_path = Path(__file__).resolve().parent / "scripts" / "nexo-morning-agent.py"
|
|
203
|
+
script = {
|
|
204
|
+
"name": "morning-agent",
|
|
205
|
+
"path": str(script_path),
|
|
206
|
+
"description": "Generate and send the operator's daily morning briefing email.",
|
|
207
|
+
"runtime": "python",
|
|
208
|
+
"core": True,
|
|
209
|
+
"metadata": {},
|
|
210
|
+
"origin": "core",
|
|
211
|
+
}
|
|
212
|
+
if not script:
|
|
213
|
+
return None, None
|
|
214
|
+
existing = get_personal_script(script.get("path", ""), include_core=True) or script
|
|
215
|
+
return script, existing
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_automation_preferences(name_or_path: str) -> dict[str, Any]:
|
|
219
|
+
clean_name = normalize_automation_name(name_or_path)
|
|
220
|
+
script, existing = _script_row_for(name_or_path)
|
|
221
|
+
if script:
|
|
222
|
+
clean_name = normalize_automation_name(script.get("name") or clean_name)
|
|
223
|
+
metadata = (existing or {}).get("metadata") if isinstance((existing or {}).get("metadata"), dict) else {}
|
|
224
|
+
stored = metadata.get(AUTOMATION_PREFERENCES_METADATA_KEY) if isinstance(metadata, dict) else {}
|
|
225
|
+
validated = validate_automation_preferences(clean_name, stored if isinstance(stored, dict) else {})
|
|
226
|
+
return {
|
|
227
|
+
"ok": True,
|
|
228
|
+
"name": clean_name,
|
|
229
|
+
"schema": get_automation_preference_schema(clean_name),
|
|
230
|
+
"preferences": validated,
|
|
231
|
+
"supports_automation_preferences": supports_automation_preferences(clean_name),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def set_automation_preferences(name_or_path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
236
|
+
from db._personal_scripts import upsert_personal_script
|
|
237
|
+
|
|
238
|
+
script, existing = _script_row_for(name_or_path)
|
|
239
|
+
if not script:
|
|
240
|
+
return {"ok": False, "error": f"Automation not found: {name_or_path}"}
|
|
241
|
+
clean_name = normalize_automation_name(script.get("name") or name_or_path)
|
|
242
|
+
if not supports_automation_preferences(clean_name):
|
|
243
|
+
return {"ok": False, "error": "This automation does not support structured preferences."}
|
|
244
|
+
validated = validate_automation_preferences(clean_name, payload)
|
|
245
|
+
metadata = dict((existing or script).get("metadata") or {})
|
|
246
|
+
metadata[AUTOMATION_PREFERENCES_METADATA_KEY] = {
|
|
247
|
+
"schema_version": validated["schema_version"],
|
|
248
|
+
"values": validated["values"],
|
|
249
|
+
}
|
|
250
|
+
script_origin = "core" if (bool(script.get("core")) or str(script.get("origin") or "") == "core") else "user"
|
|
251
|
+
upsert_personal_script(
|
|
252
|
+
name=script.get("name", clean_name),
|
|
253
|
+
path=script.get("path", ""),
|
|
254
|
+
description=script.get("description", ""),
|
|
255
|
+
runtime=script.get("runtime", "unknown"),
|
|
256
|
+
metadata=metadata,
|
|
257
|
+
created_by="nexo-core" if script_origin == "core" else "manual",
|
|
258
|
+
source="core-toggle" if script_origin == "core" else "filesystem",
|
|
259
|
+
origin=script_origin,
|
|
260
|
+
enabled=bool((existing or script).get("enabled", True)),
|
|
261
|
+
has_inline_metadata=bool(script.get("metadata")),
|
|
262
|
+
)
|
|
263
|
+
return {
|
|
264
|
+
"ok": True,
|
|
265
|
+
"name": clean_name,
|
|
266
|
+
"preferences": validated,
|
|
267
|
+
"supports_automation_preferences": True,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def search_automation_preference_schema(name: str, query: str) -> list[dict[str, Any]]:
|
|
272
|
+
clean_query = _fold_text(query)
|
|
273
|
+
if not clean_query:
|
|
274
|
+
return []
|
|
275
|
+
matches: list[dict[str, Any]] = []
|
|
276
|
+
for group in list(get_automation_preference_schema(name).get("groups") or []):
|
|
277
|
+
for item in list(group.get("items") or []):
|
|
278
|
+
text = " ".join([
|
|
279
|
+
str(item.get("id") or ""),
|
|
280
|
+
str(item.get("label") or ""),
|
|
281
|
+
str(item.get("disabled_reason") or ""),
|
|
282
|
+
str(group.get("label") or ""),
|
|
283
|
+
])
|
|
284
|
+
if clean_query in _fold_text(text):
|
|
285
|
+
matches.append({"group": group.get("id"), **item})
|
|
286
|
+
return matches
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _fold_text(value: str) -> str:
|
|
290
|
+
normalized = unicodedata.normalize("NFKD", str(value or ""))
|
|
291
|
+
asciiish = "".join(ch for ch in normalized if not unicodedata.combining(ch))
|
|
292
|
+
return asciiish.casefold()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def format_automation_preferences_prompt_block(name_or_path: str) -> str:
|
|
296
|
+
result = get_automation_preferences(name_or_path)
|
|
297
|
+
if not result.get("supports_automation_preferences"):
|
|
298
|
+
return ""
|
|
299
|
+
prefs = result.get("preferences") or {}
|
|
300
|
+
values = prefs.get("values") if isinstance(prefs, dict) else {}
|
|
301
|
+
if not isinstance(values, dict):
|
|
302
|
+
values = {}
|
|
303
|
+
compact = json.dumps(values, ensure_ascii=False, sort_keys=True)
|
|
304
|
+
return (
|
|
305
|
+
"\n== STRUCTURED CONTENT PREFERENCES FOR THIS AUTOMATION ==\n"
|
|
306
|
+
f"{compact}\n"
|
|
307
|
+
"Use these preferences to decide what to include, omit, and emphasize. "
|
|
308
|
+
"Disabled/unavailable data sources must not be invented.\n"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
__all__ = [
|
|
313
|
+
"AUTOMATION_PREFERENCES_METADATA_KEY",
|
|
314
|
+
"default_automation_preferences",
|
|
315
|
+
"format_automation_preferences_prompt_block",
|
|
316
|
+
"get_automation_preference_schema",
|
|
317
|
+
"get_automation_preferences",
|
|
318
|
+
"normalize_automation_name",
|
|
319
|
+
"search_automation_preference_schema",
|
|
320
|
+
"set_automation_preferences",
|
|
321
|
+
"supports_automation_preferences",
|
|
322
|
+
"validate_automation_preferences",
|
|
323
|
+
]
|