nexo-brain 2.7.0 → 3.0.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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +66 -12
  3. package/hooks/hooks.json +79 -0
  4. package/package.json +1 -1
  5. package/src/agent_runner.py +290 -6
  6. package/src/cli.py +111 -0
  7. package/src/client_preferences.py +94 -0
  8. package/src/client_sync.py +202 -2
  9. package/src/cognitive/__init__.py +1 -1
  10. package/src/cognitive/_search.py +39 -19
  11. package/src/dashboard/app.py +140 -0
  12. package/src/dashboard/templates/base.html +4 -0
  13. package/src/dashboard/templates/protocol.html +199 -0
  14. package/src/db/__init__.py +23 -1
  15. package/src/db/_learnings.py +31 -4
  16. package/src/db/_personal_scripts.py +12 -0
  17. package/src/db/_protocol.py +303 -0
  18. package/src/db/_schema.py +248 -0
  19. package/src/db/_watchers.py +173 -0
  20. package/src/db/_workflow.py +952 -0
  21. package/src/doctor/providers/runtime.py +918 -7
  22. package/src/evolution_cycle.py +62 -0
  23. package/src/hook_guardrails.py +308 -0
  24. package/src/hooks/protocol-guardrail.sh +10 -0
  25. package/src/nexo_sdk.py +103 -0
  26. package/src/plugins/cognitive_memory.py +18 -0
  27. package/src/plugins/cortex.py +55 -35
  28. package/src/plugins/guard.py +132 -16
  29. package/src/plugins/protocol.py +911 -0
  30. package/src/plugins/schedule.py +40 -6
  31. package/src/plugins/simple_api.py +103 -0
  32. package/src/plugins/skills.py +67 -0
  33. package/src/plugins/state_watchers.py +79 -0
  34. package/src/plugins/workflow.py +588 -0
  35. package/src/public_contribution.py +86 -12
  36. package/src/script_registry.py +142 -0
  37. package/src/scripts/deep-sleep/apply_findings.py +204 -0
  38. package/src/scripts/deep-sleep/collect.py +49 -4
  39. package/src/scripts/nexo-agent-run.py +2 -0
  40. package/src/scripts/nexo-daily-self-audit.py +843 -5
  41. package/src/scripts/nexo-evolution-run.py +343 -1
  42. package/src/server.py +92 -6
  43. package/src/skills_runtime.py +151 -0
  44. package/src/state_watchers_runtime.py +334 -0
  45. package/src/tools_learnings.py +345 -7
  46. package/src/tools_sessions.py +183 -0
  47. package/templates/CLAUDE.md.template +9 -1
  48. package/templates/CODEX.AGENTS.md.template +10 -2
@@ -25,6 +25,7 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", ""))
25
25
  DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
26
26
  NEXO_DB = NEXO_HOME / "data" / "nexo.db"
27
27
  COGNITIVE_DB = NEXO_HOME / "data" / "cognitive.db"
28
+ _TABLE_COLUMNS_CACHE: dict[tuple[str, str], set[str]] = {}
28
29
 
29
30
  MIN_USER_MESSAGES = 3 # Skip trivial sessions
30
31
 
@@ -304,6 +305,33 @@ def safe_query(db_path: Path, query: str, params: tuple = ()) -> list[dict]:
304
305
  return []
305
306
 
306
307
 
308
+ def _table_columns(db_path: Path, table_name: str) -> set[str]:
309
+ cache_key = (str(db_path), table_name)
310
+ cached = _TABLE_COLUMNS_CACHE.get(cache_key)
311
+ if cached is not None:
312
+ return cached
313
+ if not db_path.exists():
314
+ _TABLE_COLUMNS_CACHE[cache_key] = set()
315
+ return set()
316
+ try:
317
+ conn = sqlite3.connect(str(db_path))
318
+ conn.row_factory = sqlite3.Row
319
+ rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
320
+ conn.close()
321
+ except Exception:
322
+ _TABLE_COLUMNS_CACHE[cache_key] = set()
323
+ return set()
324
+ columns = {str(row["name"]) for row in rows}
325
+ _TABLE_COLUMNS_CACHE[cache_key] = columns
326
+ return columns
327
+
328
+
329
+ def _optional_column_sql(db_path: Path, table_name: str, column_name: str, default_sql: str = "''") -> str:
330
+ if column_name in _table_columns(db_path, table_name):
331
+ return column_name
332
+ return f"{default_sql} AS {column_name}"
333
+
334
+
307
335
  def collect_followups() -> list[dict]:
308
336
  """Active followups from nexo.db."""
309
337
  return safe_query(
@@ -513,9 +541,13 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
513
541
  for project in candidates:
514
542
  bump(project, 3.0 * recency_bonus, "diary_sessions", "recent session diary activity")
515
543
 
544
+ learning_priority_sql = _optional_column_sql(NEXO_DB, "learnings", "priority", "'medium'")
545
+ learning_weight_sql = _optional_column_sql(NEXO_DB, "learnings", "weight", "0")
546
+ learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
516
547
  learning_rows = safe_query(
517
548
  NEXO_DB,
518
- "SELECT category, title, content, created_at, updated_at, priority, weight, applies_to FROM learnings "
549
+ f"SELECT category, title, content, created_at, updated_at, {learning_priority_sql}, "
550
+ f"{learning_weight_sql}, {learning_applies_sql} FROM learnings "
519
551
  "ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 160",
520
552
  )
521
553
  for row in learning_rows:
@@ -535,9 +567,12 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
535
567
  for project in matched:
536
568
  bump(project, score, "learnings", "recent leverage-bearing learning")
537
569
 
570
+ followup_priority_sql = _optional_column_sql(NEXO_DB, "followups", "priority", "'medium'")
571
+ followup_reasoning_sql = _optional_column_sql(NEXO_DB, "followups", "reasoning", "''")
538
572
  followup_rows = safe_query(
539
573
  NEXO_DB,
540
- "SELECT id, description, date, status, priority, created_at, updated_at, reasoning FROM followups "
574
+ f"SELECT id, description, date, status, {followup_priority_sql}, created_at, updated_at, "
575
+ f"{followup_reasoning_sql} FROM followups "
541
576
  "WHERE status NOT IN ('COMPLETED', 'CANCELLED') ORDER BY date ASC, created_at ASC LIMIT 120",
542
577
  )
543
578
  for row in followup_rows:
@@ -565,9 +600,13 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
565
600
  for project in matched:
566
601
  bump(project, score, "followups", "open followup pressure")
567
602
 
603
+ decision_status_sql = _optional_column_sql(NEXO_DB, "decisions", "status", "''")
604
+ decision_reasoning_sql = _optional_column_sql(NEXO_DB, "decisions", "reasoning", "''")
605
+ decision_review_due_sql = _optional_column_sql(NEXO_DB, "decisions", "review_due_at", "NULL")
568
606
  decision_rows = safe_query(
569
607
  NEXO_DB,
570
- "SELECT domain, outcome, status, reasoning, created_at, review_due_at FROM decisions "
608
+ f"SELECT domain, outcome, {decision_status_sql}, {decision_reasoning_sql}, decision, based_on, created_at, "
609
+ f"{decision_review_due_sql} FROM decisions "
571
610
  "ORDER BY COALESCE(created_at, review_due_at) DESC LIMIT 120",
572
611
  )
573
612
  for row in decision_rows:
@@ -579,6 +618,8 @@ def _project_priority_signals(target_day: datetime, compact_diaries: list[dict])
579
618
  " ".join(
580
619
  [
581
620
  str(row.get("reasoning", "") or ""),
621
+ str(row.get("decision", "") or ""),
622
+ str(row.get("based_on", "") or ""),
582
623
  str(row.get("outcome", "") or ""),
583
624
  str(row.get("status", "") or ""),
584
625
  ]
@@ -647,9 +688,13 @@ def collect_long_horizon_context(
647
688
  recurring_states = Counter(row["mental_state"] for row in compact_diaries if row.get("mental_state"))
648
689
  recurring_critiques = Counter(row["self_critique"] for row in compact_diaries if row.get("self_critique"))
649
690
 
691
+ learning_reasoning_sql = _optional_column_sql(NEXO_DB, "learnings", "reasoning", "''")
692
+ learning_prevention_sql = _optional_column_sql(NEXO_DB, "learnings", "prevention", "''")
693
+ learning_applies_sql = _optional_column_sql(NEXO_DB, "learnings", "applies_to", "''")
650
694
  learning_rows = safe_query(
651
695
  NEXO_DB,
652
- "SELECT category, title, content, created_at, updated_at, reasoning, prevention, applies_to "
696
+ f"SELECT category, title, content, created_at, updated_at, {learning_reasoning_sql}, "
697
+ f"{learning_prevention_sql}, {learning_applies_sql} "
653
698
  "FROM learnings ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 120"
654
699
  )
655
700
  long_horizon_learnings = []
@@ -29,6 +29,7 @@ def main(argv: list[str] | None = None) -> int:
29
29
  parser.add_argument("--prompt", default="", help="Prompt text")
30
30
  parser.add_argument("--prompt-file", default="", help="Read prompt text from a file")
31
31
  parser.add_argument("--cwd", default="", help="Working directory for the backend")
32
+ parser.add_argument("--task-profile", default="", help="Automation task profile: default|fast|balanced|deep")
32
33
  parser.add_argument("--model", default="", help="Backend model hint")
33
34
  parser.add_argument("--reasoning-effort", default="", help="Backend reasoning effort/profile")
34
35
  parser.add_argument("--timeout", type=int, default=21600, help="Timeout in seconds")
@@ -51,6 +52,7 @@ def main(argv: list[str] | None = None) -> int:
51
52
  result = run_automation_prompt(
52
53
  prompt,
53
54
  cwd=args.cwd or None,
55
+ task_profile=args.task_profile,
54
56
  model=args.model,
55
57
  reasoning_effort=args.reasoning_effort,
56
58
  timeout=args.timeout,