nexo-brain 2.7.0 → 3.0.1
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 +66 -12
- package/hooks/hooks.json +79 -0
- package/package.json +1 -1
- package/src/agent_runner.py +295 -7
- package/src/cli.py +111 -0
- package/src/client_preferences.py +99 -1
- package/src/client_sync.py +207 -3
- package/src/cognitive/__init__.py +1 -1
- package/src/cognitive/_search.py +39 -19
- package/src/dashboard/app.py +141 -1
- package/src/dashboard/templates/base.html +4 -0
- package/src/dashboard/templates/protocol.html +199 -0
- package/src/db/__init__.py +23 -1
- package/src/db/_learnings.py +31 -4
- package/src/db/_personal_scripts.py +12 -0
- package/src/db/_protocol.py +303 -0
- package/src/db/_schema.py +248 -0
- package/src/db/_watchers.py +173 -0
- package/src/db/_workflow.py +952 -0
- package/src/doctor/providers/boot.py +45 -19
- package/src/doctor/providers/runtime.py +923 -8
- package/src/evolution_cycle.py +62 -0
- package/src/hook_guardrails.py +308 -0
- package/src/hooks/protocol-guardrail.sh +10 -0
- package/src/nexo_sdk.py +103 -0
- package/src/plugins/cognitive_memory.py +18 -0
- package/src/plugins/cortex.py +55 -35
- package/src/plugins/guard.py +132 -16
- package/src/plugins/protocol.py +911 -0
- package/src/plugins/schedule.py +40 -6
- package/src/plugins/simple_api.py +103 -0
- package/src/plugins/skills.py +67 -0
- package/src/plugins/state_watchers.py +79 -0
- package/src/plugins/workflow.py +588 -0
- package/src/public_contribution.py +86 -12
- package/src/requirements.txt +1 -0
- package/src/script_registry.py +142 -0
- package/src/scripts/deep-sleep/apply_findings.py +204 -0
- package/src/scripts/deep-sleep/collect.py +49 -4
- package/src/scripts/nexo-agent-run.py +2 -0
- package/src/scripts/nexo-daily-self-audit.py +843 -5
- package/src/scripts/nexo-evolution-run.py +343 -1
- package/src/server.py +92 -6
- package/src/skills_runtime.py +151 -0
- package/src/state_watchers_runtime.py +334 -0
- package/src/tools_learnings.py +345 -7
- package/src/tools_sessions.py +183 -0
- package/templates/CLAUDE.md.template +9 -1
- package/templates/CODEX.AGENTS.md.template +10 -2
package/src/plugins/schedule.py
CHANGED
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from db import (
|
|
10
10
|
init_db, cron_runs_recent, cron_runs_summary,
|
|
11
11
|
upsert_personal_script, register_personal_script_schedule,
|
|
12
|
+
get_personal_script_schedule,
|
|
12
13
|
)
|
|
13
14
|
from script_registry import (
|
|
14
15
|
PERSONAL_SCHEDULE_MANAGED_ENV,
|
|
@@ -29,13 +30,18 @@ def handle_schedule_status(hours: int = 24, cron_id: str = '') -> str:
|
|
|
29
30
|
runs = cron_runs_recent(hours, cron_id)
|
|
30
31
|
if not runs:
|
|
31
32
|
return f"No runs for '{cron_id}' in the last {hours}h."
|
|
33
|
+
schedule_meta = get_personal_script_schedule(cron_id) or {}
|
|
32
34
|
lines = [f"CRON RUNS — {cron_id} (last {hours}h): {len(runs)} executions"]
|
|
33
35
|
for r in runs:
|
|
34
|
-
status = "
|
|
35
|
-
|
|
36
|
+
status, detail = _run_status_marker(r.get("exit_code"), r.get("summary"), schedule_meta=schedule_meta)
|
|
37
|
+
if schedule_meta.get("schedule_type") == "keep_alive" and r.get("exit_code") is None:
|
|
38
|
+
dur = "daemon active"
|
|
39
|
+
else:
|
|
40
|
+
dur = f"{r['duration_secs']:.0f}s" if r.get("duration_secs") else "running"
|
|
36
41
|
summary = f" — {r['summary'][:100]}" if r.get("summary") else ""
|
|
37
42
|
error = f" ERROR: {r['error'][:100]}" if r.get("error") else ""
|
|
38
|
-
|
|
43
|
+
suffix = f" [{detail}]" if detail else ""
|
|
44
|
+
lines.append(f" {status} {r['started_at']} ({dur}){summary}{error}{suffix}")
|
|
39
45
|
return "\n".join(lines)
|
|
40
46
|
|
|
41
47
|
# Summary view — one line per cron
|
|
@@ -45,15 +51,43 @@ def handle_schedule_status(hours: int = 24, cron_id: str = '') -> str:
|
|
|
45
51
|
|
|
46
52
|
lines = [f"CRON STATUS (last {hours}h):"]
|
|
47
53
|
for s in summary:
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
schedule_meta = get_personal_script_schedule(s["cron_id"]) or {}
|
|
55
|
+
status, detail = _run_status_marker(s.get("last_exit_code"), s.get("last_summary"), schedule_meta=schedule_meta)
|
|
56
|
+
if schedule_meta.get("schedule_type") == "keep_alive" and s.get("last_exit_code") is None:
|
|
57
|
+
rate = "daemon active"
|
|
58
|
+
else:
|
|
59
|
+
rate = f"{s['succeeded']}/{s['total_runs']}"
|
|
50
60
|
dur = f"{s['avg_duration']:.0f}s avg" if s.get("avg_duration") else ""
|
|
51
61
|
summary_txt = f" — {s['last_summary'][:80]}" if s.get("last_summary") else ""
|
|
52
|
-
|
|
62
|
+
suffix = f" [{detail}]" if detail else ""
|
|
63
|
+
lines.append(f" {status} {s['cron_id']}: {rate}, {dur}{summary_txt}{suffix}")
|
|
53
64
|
|
|
54
65
|
return "\n".join(lines)
|
|
55
66
|
|
|
56
67
|
|
|
68
|
+
def _summary_has_warning(summary: str = "") -> bool:
|
|
69
|
+
lowered = str(summary or "").strip().lower()
|
|
70
|
+
if not lowered:
|
|
71
|
+
return False
|
|
72
|
+
if "no warning" in lowered or "without warnings" in lowered:
|
|
73
|
+
return False
|
|
74
|
+
warning_tokens = ("warning", "warnings", "warn:", "degraded", "partial failure", "issues detected")
|
|
75
|
+
return any(token in lowered for token in warning_tokens)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run_status_marker(exit_code, summary: str = "", *, schedule_meta: dict | None = None) -> tuple[str, str]:
|
|
79
|
+
schedule_meta = schedule_meta or {}
|
|
80
|
+
if schedule_meta.get("schedule_type") == "keep_alive" and exit_code is None:
|
|
81
|
+
return "🟢", "keep_alive daemon active"
|
|
82
|
+
if exit_code == 0 and _summary_has_warning(summary):
|
|
83
|
+
return "⚠", "exit 0 with warnings"
|
|
84
|
+
if exit_code == 0:
|
|
85
|
+
return "✅", "exit 0"
|
|
86
|
+
if exit_code is None:
|
|
87
|
+
return "❌", "missing exit code"
|
|
88
|
+
return "❌", f"exit {exit_code}"
|
|
89
|
+
|
|
90
|
+
|
|
57
91
|
def handle_schedule_add(cron_id: str, script: str, schedule: str = '',
|
|
58
92
|
interval_seconds: int = 0, description: str = '',
|
|
59
93
|
script_type: str = 'auto', keep_alive: bool = False) -> str:
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Minimal public API wrappers for the core NEXO mental model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import cognitive
|
|
9
|
+
|
|
10
|
+
from plugins.episodic_memory import handle_recall
|
|
11
|
+
from plugins.workflow import handle_workflow_open
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handle_remember(
|
|
15
|
+
content: str,
|
|
16
|
+
title: str = "",
|
|
17
|
+
domain: str = "",
|
|
18
|
+
source_type: str = "note",
|
|
19
|
+
tags: str = "",
|
|
20
|
+
bypass_gate: bool = True,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Store one durable memory item with a single high-level call."""
|
|
23
|
+
clean_content = (content or "").strip()
|
|
24
|
+
if not clean_content:
|
|
25
|
+
return json.dumps({"ok": False, "error": "content is required"}, ensure_ascii=False, indent=2)
|
|
26
|
+
|
|
27
|
+
clean_title = (title or "").strip()[:120]
|
|
28
|
+
source_id = hashlib.sha1(f"{clean_title}|{clean_content}".encode("utf-8")).hexdigest()[:12]
|
|
29
|
+
memory_id = cognitive.ingest_to_ltm(
|
|
30
|
+
clean_content,
|
|
31
|
+
source_type=(source_type or "note").strip()[:40],
|
|
32
|
+
source_id=source_id,
|
|
33
|
+
source_title=clean_title or clean_content[:80],
|
|
34
|
+
domain=(domain or "").strip()[:120],
|
|
35
|
+
tags=(tags or "").strip()[:200],
|
|
36
|
+
bypass_gate=bool(bypass_gate),
|
|
37
|
+
)
|
|
38
|
+
return json.dumps(
|
|
39
|
+
{
|
|
40
|
+
"ok": bool(memory_id),
|
|
41
|
+
"memory_id": int(memory_id or 0),
|
|
42
|
+
"source_type": (source_type or "note").strip()[:40],
|
|
43
|
+
"title": clean_title or clean_content[:80],
|
|
44
|
+
"domain": (domain or "").strip()[:120],
|
|
45
|
+
},
|
|
46
|
+
ensure_ascii=False,
|
|
47
|
+
indent=2,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def handle_memory_recall(query: str, days: int = 30) -> str:
|
|
52
|
+
"""High-level memory lookup wrapper around nexo_recall."""
|
|
53
|
+
return handle_recall((query or "").strip(), days=max(1, int(days or 30)))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def handle_consolidate(
|
|
57
|
+
max_insights: int = 12,
|
|
58
|
+
threshold: float = 0.9,
|
|
59
|
+
dry_run: bool = False,
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Run the core memory consolidation cycle explicitly."""
|
|
62
|
+
promoted = cognitive.promote_stm_to_ltm()
|
|
63
|
+
quarantine = cognitive.process_quarantine()
|
|
64
|
+
dreamed = cognitive.dream_cycle(max_insights=max(1, int(max_insights or 12)))
|
|
65
|
+
semantic = cognitive.consolidate_semantic(threshold=float(threshold or 0.9), dry_run=bool(dry_run))
|
|
66
|
+
payload = {
|
|
67
|
+
"ok": True,
|
|
68
|
+
"promoted_to_ltm": int(promoted or 0),
|
|
69
|
+
"quarantine": quarantine,
|
|
70
|
+
"dream_cycle": dreamed,
|
|
71
|
+
"semantic_consolidation": semantic,
|
|
72
|
+
"dry_run": bool(dry_run),
|
|
73
|
+
}
|
|
74
|
+
return json.dumps(payload, ensure_ascii=False, indent=2)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def handle_run_workflow(
|
|
78
|
+
sid: str,
|
|
79
|
+
goal: str,
|
|
80
|
+
steps: str = "[]",
|
|
81
|
+
goal_id: str = "",
|
|
82
|
+
shared_state: str = "{}",
|
|
83
|
+
owner: str = "",
|
|
84
|
+
idempotency_key: str = "",
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Open a durable workflow with the public mental-model naming."""
|
|
87
|
+
return handle_workflow_open(
|
|
88
|
+
sid=sid,
|
|
89
|
+
goal=goal,
|
|
90
|
+
steps=steps,
|
|
91
|
+
goal_id=goal_id,
|
|
92
|
+
shared_state=shared_state,
|
|
93
|
+
owner=owner,
|
|
94
|
+
idempotency_key=idempotency_key,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
TOOLS = [
|
|
99
|
+
(handle_remember, "nexo_remember", "High-level memory write: store one durable memory item."),
|
|
100
|
+
(handle_memory_recall, "nexo_memory_recall", "High-level memory lookup wrapper around nexo_recall."),
|
|
101
|
+
(handle_consolidate, "nexo_consolidate", "Run NEXO memory consolidation explicitly: promote, process quarantine, dream, consolidate."),
|
|
102
|
+
(handle_run_workflow, "nexo_run_workflow", "High-level durable workflow entry point for the public API surface."),
|
|
103
|
+
]
|
package/src/plugins/skills.py
CHANGED
|
@@ -19,9 +19,13 @@ from db import (
|
|
|
19
19
|
from skills_runtime import (
|
|
20
20
|
apply_skill,
|
|
21
21
|
approve_skill_execution,
|
|
22
|
+
compose_skills,
|
|
22
23
|
get_featured_skill_summaries,
|
|
23
24
|
list_evolution_candidates,
|
|
25
|
+
promote_skill,
|
|
26
|
+
retire_skill,
|
|
24
27
|
sync_skills,
|
|
28
|
+
test_skill,
|
|
25
29
|
)
|
|
26
30
|
|
|
27
31
|
|
|
@@ -171,6 +175,10 @@ def handle_skill_apply(id: str, params: str = "{}", mode: str = "auto", dry_run:
|
|
|
171
175
|
return json.dumps(apply_skill(id, params=params, mode=mode, dry_run=dry_run, context=context), ensure_ascii=False)
|
|
172
176
|
|
|
173
177
|
|
|
178
|
+
def handle_skill_test(id: str, params: str = "{}", mode: str = "auto", context: str = "") -> str:
|
|
179
|
+
return json.dumps(test_skill(id, params=params, mode=mode, context=context), ensure_ascii=False)
|
|
180
|
+
|
|
181
|
+
|
|
174
182
|
def handle_skill_approve(id: str, execution_level: str = "", approved_by: str = "") -> str:
|
|
175
183
|
result = approve_skill_execution(id, execution_level=execution_level, approved_by=approved_by)
|
|
176
184
|
if "error" in result:
|
|
@@ -196,6 +204,57 @@ def handle_skill_evolution_candidates() -> str:
|
|
|
196
204
|
return json.dumps(list_evolution_candidates(), ensure_ascii=False)
|
|
197
205
|
|
|
198
206
|
|
|
207
|
+
def handle_skill_promote(id: str, target_level: str = "published", reason: str = "") -> str:
|
|
208
|
+
return json.dumps(promote_skill(id, target_level=target_level, reason=reason), ensure_ascii=False)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def handle_skill_retire(id: str, replacement_id: str = "", reason: str = "") -> str:
|
|
212
|
+
return json.dumps(retire_skill(id, replacement_id=replacement_id, reason=reason), ensure_ascii=False)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def handle_skill_compose(
|
|
216
|
+
new_id: str,
|
|
217
|
+
name: str,
|
|
218
|
+
component_ids: str = "[]",
|
|
219
|
+
description: str = "",
|
|
220
|
+
level: str = "draft",
|
|
221
|
+
mode: str = "guide",
|
|
222
|
+
tags: str = "[]",
|
|
223
|
+
trigger_patterns: str = "[]",
|
|
224
|
+
) -> str:
|
|
225
|
+
try:
|
|
226
|
+
component_list = json.loads(component_ids) if str(component_ids or "").strip().startswith("[") else [
|
|
227
|
+
item.strip() for item in str(component_ids or "").split(",") if item.strip()
|
|
228
|
+
]
|
|
229
|
+
except json.JSONDecodeError:
|
|
230
|
+
component_list = []
|
|
231
|
+
try:
|
|
232
|
+
tags_list = json.loads(tags) if str(tags or "").strip().startswith("[") else [
|
|
233
|
+
item.strip() for item in str(tags or "").split(",") if item.strip()
|
|
234
|
+
]
|
|
235
|
+
except json.JSONDecodeError:
|
|
236
|
+
tags_list = []
|
|
237
|
+
try:
|
|
238
|
+
trigger_list = json.loads(trigger_patterns) if str(trigger_patterns or "").strip().startswith("[") else [
|
|
239
|
+
item.strip() for item in str(trigger_patterns or "").split(",") if item.strip()
|
|
240
|
+
]
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
trigger_list = []
|
|
243
|
+
return json.dumps(
|
|
244
|
+
compose_skills(
|
|
245
|
+
new_skill_id=new_id,
|
|
246
|
+
name=name,
|
|
247
|
+
component_ids=component_list,
|
|
248
|
+
description=description,
|
|
249
|
+
level=level,
|
|
250
|
+
mode=mode,
|
|
251
|
+
tags=tags_list,
|
|
252
|
+
trigger_patterns=trigger_list,
|
|
253
|
+
),
|
|
254
|
+
ensure_ascii=False,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
199
258
|
TOOLS = [
|
|
200
259
|
(handle_skill_create, "nexo_skill_create",
|
|
201
260
|
"Create a new skill with guide/execute/hybrid metadata, triggers, params schema, and execution level."),
|
|
@@ -213,6 +272,8 @@ TOOLS = [
|
|
|
213
272
|
"Show aggregate skill statistics."),
|
|
214
273
|
(handle_skill_apply, "nexo_skill_apply",
|
|
215
274
|
"Apply a skill in guide, execute, or hybrid mode. Execution goes through the stable nexo scripts runtime."),
|
|
275
|
+
(handle_skill_test, "nexo_skill_test",
|
|
276
|
+
"Test a skill through the canonical runtime in dry-run mode before wider use."),
|
|
216
277
|
(handle_skill_approve, "nexo_skill_approve",
|
|
217
278
|
"Approve a local/remote executable skill so it can run."),
|
|
218
279
|
(handle_skill_sync, "nexo_skill_sync",
|
|
@@ -221,4 +282,10 @@ TOOLS = [
|
|
|
221
282
|
"Return featured published/stable skills for startup discovery."),
|
|
222
283
|
(handle_skill_evolution_candidates, "nexo_skill_evolution_candidates",
|
|
223
284
|
"Return candidates for skill improvement or text-to-script evolution."),
|
|
285
|
+
(handle_skill_promote, "nexo_skill_promote",
|
|
286
|
+
"Promote a skill to a stronger published/stable lifecycle stage."),
|
|
287
|
+
(handle_skill_retire, "nexo_skill_retire",
|
|
288
|
+
"Retire a skill cleanly so it leaves the active lifecycle."),
|
|
289
|
+
(handle_skill_compose, "nexo_skill_compose",
|
|
290
|
+
"Compose multiple existing skills into one higher-level reusable skill."),
|
|
224
291
|
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""State watchers plugin — persistent drift/health/expiry watchers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from db import create_state_watcher, list_state_watchers, update_state_watcher
|
|
8
|
+
from state_watchers_runtime import run_state_watchers
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_state_watcher_create(
|
|
12
|
+
watcher_type: str,
|
|
13
|
+
title: str,
|
|
14
|
+
target: str = "",
|
|
15
|
+
severity: str = "warn",
|
|
16
|
+
status: str = "active",
|
|
17
|
+
config: str = "{}",
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Create a persistent state watcher for drift, health, or expiry."""
|
|
20
|
+
try:
|
|
21
|
+
watcher = create_state_watcher(
|
|
22
|
+
watcher_type,
|
|
23
|
+
title,
|
|
24
|
+
target=target,
|
|
25
|
+
severity=severity,
|
|
26
|
+
status=status,
|
|
27
|
+
config=json.loads(config) if str(config).strip() else {},
|
|
28
|
+
)
|
|
29
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
30
|
+
return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, indent=2)
|
|
31
|
+
return json.dumps({"ok": True, "watcher": watcher}, ensure_ascii=False, indent=2)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def handle_state_watcher_update(
|
|
35
|
+
watcher_id: str,
|
|
36
|
+
title: str = "",
|
|
37
|
+
target: str = "",
|
|
38
|
+
severity: str = "",
|
|
39
|
+
status: str = "",
|
|
40
|
+
config: str = "",
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Update an existing state watcher."""
|
|
43
|
+
payload = None
|
|
44
|
+
if str(config).strip():
|
|
45
|
+
try:
|
|
46
|
+
payload = json.loads(config)
|
|
47
|
+
except json.JSONDecodeError as exc:
|
|
48
|
+
return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, indent=2)
|
|
49
|
+
watcher = update_state_watcher(
|
|
50
|
+
watcher_id,
|
|
51
|
+
title=(title or None),
|
|
52
|
+
target=(target or None),
|
|
53
|
+
severity=(severity or None),
|
|
54
|
+
status=(status or None),
|
|
55
|
+
config=payload,
|
|
56
|
+
)
|
|
57
|
+
if not watcher:
|
|
58
|
+
return json.dumps({"ok": False, "error": f"Unknown watcher_id: {watcher_id}"}, ensure_ascii=False, indent=2)
|
|
59
|
+
return json.dumps({"ok": True, "watcher": watcher}, ensure_ascii=False, indent=2)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def handle_state_watcher_list(status: str = "", watcher_type: str = "", limit: int = 50) -> str:
|
|
63
|
+
"""List configured state watchers."""
|
|
64
|
+
watchers = list_state_watchers(status=status, watcher_type=watcher_type, limit=max(1, int(limit or 50)))
|
|
65
|
+
return json.dumps({"ok": True, "count": len(watchers), "watchers": watchers}, ensure_ascii=False, indent=2)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def handle_state_watcher_run(status: str = "active", persist: bool = True) -> str:
|
|
69
|
+
"""Run active state watchers and return their current health."""
|
|
70
|
+
summary = run_state_watchers(status=status or "active", persist=bool(persist))
|
|
71
|
+
return json.dumps({"ok": True, **summary}, ensure_ascii=False, indent=2)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
TOOLS = [
|
|
75
|
+
(handle_state_watcher_create, "nexo_state_watcher_create", "Create a persistent state watcher for repo drift, cron drift, API health, environment drift, or expiry."),
|
|
76
|
+
(handle_state_watcher_update, "nexo_state_watcher_update", "Update an existing persistent state watcher."),
|
|
77
|
+
(handle_state_watcher_list, "nexo_state_watcher_list", "List persistent state watchers."),
|
|
78
|
+
(handle_state_watcher_run, "nexo_state_watcher_run", "Run persistent state watchers and return their current health summary."),
|
|
79
|
+
]
|