nexo-brain 5.3.20 → 5.3.21
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/package.json +1 -1
- package/src/auto_update.py +11 -8
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-template 2.md +33 -0
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
"""Cognitive Cortex plugin — middleware cognitive layer for NEXO Brain.
|
|
2
|
+
|
|
3
|
+
Provides structured pre-action reasoning with architectural inhibitory control.
|
|
4
|
+
The Cortex does NOT generate answers — it gates, plans, and validates actions.
|
|
5
|
+
|
|
6
|
+
Activation: event-driven, not on every turn. Only on:
|
|
7
|
+
- Tool intent (edit, execute, delegate)
|
|
8
|
+
- Ambiguity in user request
|
|
9
|
+
- Destructive actions
|
|
10
|
+
- Multi-step tasks
|
|
11
|
+
- Retry after failure
|
|
12
|
+
- Contradictions with known facts
|
|
13
|
+
|
|
14
|
+
v0.1: Single MCP tool + middleware validation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import secrets
|
|
21
|
+
import time
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from db import VALID_IMPACT_LEVELS, VALID_TASK_TYPES, validate_impact_level, validate_task_type
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_db():
|
|
29
|
+
from db import get_db
|
|
30
|
+
return get_db()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_core_rules_for_task(task_type: str) -> list[str]:
|
|
34
|
+
"""Get relevant Core Rules for the given task type."""
|
|
35
|
+
conn = _get_db()
|
|
36
|
+
try:
|
|
37
|
+
# Map task type to rule categories
|
|
38
|
+
category_map = {
|
|
39
|
+
"edit": ["integrity", "execution"],
|
|
40
|
+
"execute": ["integrity", "execution", "delegation"],
|
|
41
|
+
"delegate": ["delegation"],
|
|
42
|
+
"analyze": ["execution", "memory"],
|
|
43
|
+
"answer": ["communication"],
|
|
44
|
+
}
|
|
45
|
+
categories = category_map.get(task_type, ["integrity", "execution"])
|
|
46
|
+
placeholders = ",".join("?" * len(categories))
|
|
47
|
+
|
|
48
|
+
rows = conn.execute(
|
|
49
|
+
f"SELECT id, rule FROM core_rules WHERE category IN ({placeholders}) AND is_active = 1 AND type = 'blocking' ORDER BY importance DESC LIMIT 5",
|
|
50
|
+
categories
|
|
51
|
+
).fetchall()
|
|
52
|
+
return [f"{r['id']}: {r['rule']}" for r in rows]
|
|
53
|
+
except Exception:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_trust_score() -> float:
|
|
58
|
+
"""Get current trust score from cognitive.db."""
|
|
59
|
+
try:
|
|
60
|
+
import cognitive
|
|
61
|
+
return cognitive.get_trust_score()
|
|
62
|
+
except Exception:
|
|
63
|
+
return 50.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
SAFE_TERMS = {
|
|
67
|
+
"verify", "verification", "test", "smoke", "rollback", "monitor",
|
|
68
|
+
"staged", "stage", "incremental", "safe", "guard", "contract",
|
|
69
|
+
"document", "docs", "reconcile", "doctor",
|
|
70
|
+
}
|
|
71
|
+
RISK_TERMS = {
|
|
72
|
+
"force", "delete", "bypass", "skip", "manual", "direct", "hotfix",
|
|
73
|
+
"reset", "hardcode", "production", "launchagent", "plist",
|
|
74
|
+
}
|
|
75
|
+
DIRECT_IMPACT_TERMS = {
|
|
76
|
+
"fix", "close", "resolve", "ship", "release", "deploy", "migrate",
|
|
77
|
+
"automate", "integrate", "register", "repair",
|
|
78
|
+
}
|
|
79
|
+
POSITIVE_OUTCOME_TERMS = {
|
|
80
|
+
"met", "success", "resolved", "clean", "improved", "green", "healthy", "done",
|
|
81
|
+
}
|
|
82
|
+
NEGATIVE_OUTCOME_TERMS = {
|
|
83
|
+
"missed", "failed", "failure", "regressed", "blocked", "error", "degraded",
|
|
84
|
+
}
|
|
85
|
+
STOP_WORDS = {
|
|
86
|
+
"about", "after", "again", "before", "being", "between", "could", "should",
|
|
87
|
+
"there", "their", "would", "while", "using", "used", "from", "with",
|
|
88
|
+
"that", "this", "into", "over", "have", "must", "will", "your",
|
|
89
|
+
}
|
|
90
|
+
HISTORICAL_OUTCOME_MIN_RESOLVED = 2
|
|
91
|
+
HISTORICAL_OUTCOME_LOOKBACK = 12
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _term_hits(text: str, terms: set[str]) -> int:
|
|
95
|
+
lowered = (text or "").lower()
|
|
96
|
+
return sum(1 for term in terms if term in lowered)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _validate_state(state: dict) -> dict:
|
|
100
|
+
"""Validate cognitive state and determine action mode.
|
|
101
|
+
|
|
102
|
+
Returns dict with: mode, warnings, injected_rules, blocked_reason
|
|
103
|
+
"""
|
|
104
|
+
warnings = []
|
|
105
|
+
mode = "act" # default: allow action
|
|
106
|
+
blocked_reason = None
|
|
107
|
+
|
|
108
|
+
task_type = state.get("task_type", "answer")
|
|
109
|
+
plan = state.get("plan", [])
|
|
110
|
+
unknowns = state.get("unknowns", [])
|
|
111
|
+
evidence = state.get("evidence_refs", [])
|
|
112
|
+
verification = state.get("verification_step", "")
|
|
113
|
+
constraints = state.get("constraints", [])
|
|
114
|
+
goal = state.get("goal", "")
|
|
115
|
+
|
|
116
|
+
# === INHIBITION RULES (architectural, not advisory) ===
|
|
117
|
+
|
|
118
|
+
# Rule 1: unknowns exist → force ASK mode
|
|
119
|
+
if unknowns:
|
|
120
|
+
mode = "ask"
|
|
121
|
+
blocked_reason = f"Cannot act with {len(unknowns)} unknown(s). Resolve first."
|
|
122
|
+
warnings.append(f"UNKNOWNS: {', '.join(unknowns[:3])}")
|
|
123
|
+
|
|
124
|
+
# Rule 2: edit/execute without plan → force PROPOSE
|
|
125
|
+
if task_type in ("edit", "execute", "delegate") and not plan and mode == "act":
|
|
126
|
+
mode = "propose"
|
|
127
|
+
blocked_reason = "No plan defined for action task. Propose plan first."
|
|
128
|
+
warnings.append("MISSING PLAN: define steps before executing")
|
|
129
|
+
|
|
130
|
+
# Rule 3: edit/execute without verification → force PROPOSE
|
|
131
|
+
if task_type in ("edit", "execute") and not verification and mode == "act":
|
|
132
|
+
mode = "propose"
|
|
133
|
+
blocked_reason = "No verification step. How will you confirm it worked?"
|
|
134
|
+
warnings.append("MISSING VERIFICATION: define how to verify")
|
|
135
|
+
|
|
136
|
+
# Rule 4: execute without evidence → force PROPOSE
|
|
137
|
+
if task_type == "execute" and not evidence and mode == "act":
|
|
138
|
+
mode = "propose"
|
|
139
|
+
blocked_reason = "No evidence supporting this action."
|
|
140
|
+
warnings.append("MISSING EVIDENCE: what supports this action?")
|
|
141
|
+
|
|
142
|
+
# Rule 5: no goal → force ASK
|
|
143
|
+
if not goal:
|
|
144
|
+
mode = "ask"
|
|
145
|
+
blocked_reason = "No goal defined."
|
|
146
|
+
warnings.append("NO GOAL: what are you trying to achieve?")
|
|
147
|
+
|
|
148
|
+
# === TRUST-BASED ADJUSTMENTS ===
|
|
149
|
+
trust = _get_trust_score()
|
|
150
|
+
if trust < 30 and mode == "act" and task_type in ("edit", "execute"):
|
|
151
|
+
mode = "propose"
|
|
152
|
+
blocked_reason = f"Trust score {trust:.0f}/100 — propose before acting."
|
|
153
|
+
warnings.append(f"LOW TRUST ({trust:.0f}): extra verification required")
|
|
154
|
+
|
|
155
|
+
# === INJECT RELEVANT RULES ===
|
|
156
|
+
rules = _get_core_rules_for_task(task_type)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"mode": mode,
|
|
160
|
+
"tools_available": _tools_for_mode(mode),
|
|
161
|
+
"warnings": warnings,
|
|
162
|
+
"blocked_reason": blocked_reason,
|
|
163
|
+
"injected_rules": rules,
|
|
164
|
+
"trust_score": round(trust),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _tools_for_mode(mode: str) -> list[str]:
|
|
169
|
+
"""Define which tool categories are available per mode."""
|
|
170
|
+
if mode == "ask":
|
|
171
|
+
return ["read", "search", "ask_user"]
|
|
172
|
+
elif mode == "propose":
|
|
173
|
+
return ["read", "search", "analyze", "propose_plan"]
|
|
174
|
+
else: # act
|
|
175
|
+
return ["all"]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_json_list(value) -> list:
|
|
179
|
+
try:
|
|
180
|
+
parsed = json.loads(value) if isinstance(value, str) else value
|
|
181
|
+
return parsed if isinstance(parsed, list) else []
|
|
182
|
+
except (json.JSONDecodeError, TypeError):
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _parse_alternatives(value) -> list[dict]:
|
|
187
|
+
if isinstance(value, list):
|
|
188
|
+
raw_items = value
|
|
189
|
+
elif isinstance(value, str):
|
|
190
|
+
stripped = value.strip()
|
|
191
|
+
if not stripped:
|
|
192
|
+
return []
|
|
193
|
+
try:
|
|
194
|
+
parsed = json.loads(stripped)
|
|
195
|
+
except json.JSONDecodeError:
|
|
196
|
+
parsed = None
|
|
197
|
+
if isinstance(parsed, list):
|
|
198
|
+
raw_items = parsed
|
|
199
|
+
else:
|
|
200
|
+
lines = [line.strip("-* \t") for line in stripped.splitlines() if line.strip()]
|
|
201
|
+
raw_items = lines if lines else [item.strip() for item in stripped.split("|") if item.strip()]
|
|
202
|
+
else:
|
|
203
|
+
raw_items = [value]
|
|
204
|
+
|
|
205
|
+
normalized = []
|
|
206
|
+
for idx, item in enumerate(raw_items, start=1):
|
|
207
|
+
if isinstance(item, dict):
|
|
208
|
+
name = str(item.get("name") or item.get("title") or f"alternative_{idx}").strip()
|
|
209
|
+
description = str(item.get("description") or "").strip()
|
|
210
|
+
pros = item.get("pros") or []
|
|
211
|
+
cons = item.get("cons") or []
|
|
212
|
+
if isinstance(pros, str):
|
|
213
|
+
pros = [pros]
|
|
214
|
+
if isinstance(cons, str):
|
|
215
|
+
cons = [cons]
|
|
216
|
+
normalized.append({
|
|
217
|
+
"name": name,
|
|
218
|
+
"description": description,
|
|
219
|
+
"pros": [str(x).strip() for x in pros if str(x).strip()],
|
|
220
|
+
"cons": [str(x).strip() for x in cons if str(x).strip()],
|
|
221
|
+
})
|
|
222
|
+
continue
|
|
223
|
+
text = str(item).strip()
|
|
224
|
+
if not text:
|
|
225
|
+
continue
|
|
226
|
+
normalized.append({
|
|
227
|
+
"name": f"alternative_{idx}",
|
|
228
|
+
"description": text,
|
|
229
|
+
"pros": [],
|
|
230
|
+
"cons": [],
|
|
231
|
+
})
|
|
232
|
+
return normalized
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _tokenize(text: str, limit: int = 12) -> list[str]:
|
|
236
|
+
tokens = []
|
|
237
|
+
for token in re.findall(r"[a-z0-9_]{4,}", (text or "").lower()):
|
|
238
|
+
if token in STOP_WORDS:
|
|
239
|
+
continue
|
|
240
|
+
if token not in tokens:
|
|
241
|
+
tokens.append(token)
|
|
242
|
+
if len(tokens) >= limit:
|
|
243
|
+
break
|
|
244
|
+
return tokens
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _contains_any(text: str, terms: set[str]) -> bool:
|
|
248
|
+
lowered = (text or "").lower()
|
|
249
|
+
return any(term in lowered for term in terms)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _impact_base(impact_level: str) -> float:
|
|
253
|
+
return {
|
|
254
|
+
"critical": 8.5,
|
|
255
|
+
"high": 7.0,
|
|
256
|
+
"medium": 5.5,
|
|
257
|
+
}.get((impact_level or "").lower(), 7.0)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _constraint_penalty(text: str, constraints: list[str]) -> tuple[float, list[str]]:
|
|
261
|
+
penalty = 0.0
|
|
262
|
+
reasons: list[str] = []
|
|
263
|
+
lowered = (text or "").lower()
|
|
264
|
+
for constraint in constraints[:8]:
|
|
265
|
+
item = (constraint or "").strip()
|
|
266
|
+
lowered_constraint = item.lower()
|
|
267
|
+
if not item:
|
|
268
|
+
continue
|
|
269
|
+
if any(marker in lowered_constraint for marker in ("no ", "never", "must not", "do not", "without")):
|
|
270
|
+
tokens = _tokenize(lowered_constraint, limit=4)
|
|
271
|
+
if tokens and any(token in lowered for token in tokens):
|
|
272
|
+
penalty += 1.5
|
|
273
|
+
reasons.append(f"rozando constraint: {item[:80]}")
|
|
274
|
+
return penalty, reasons[:2]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _history_signal(text: str, *, area: str = "", goal: str = "") -> dict:
|
|
278
|
+
conn = _get_db()
|
|
279
|
+
tokens = _tokenize(" ".join(part for part in [text, area, goal] if part), limit=6)
|
|
280
|
+
if not tokens:
|
|
281
|
+
return {"positive": 0.0, "negative": 0.0, "matched_decisions": 0, "matched_outcomes": 0}
|
|
282
|
+
|
|
283
|
+
decision_positive = 0
|
|
284
|
+
decision_negative = 0
|
|
285
|
+
matched_decisions = 0
|
|
286
|
+
for token in tokens[:3]:
|
|
287
|
+
rows = conn.execute(
|
|
288
|
+
"""SELECT outcome FROM decisions
|
|
289
|
+
WHERE lower(decision) LIKE ? OR lower(alternatives) LIKE ? OR lower(based_on) LIKE ?
|
|
290
|
+
ORDER BY created_at DESC LIMIT 6""",
|
|
291
|
+
tuple(f"%{token}%" for _ in range(3)),
|
|
292
|
+
).fetchall()
|
|
293
|
+
for row in rows:
|
|
294
|
+
matched_decisions += 1
|
|
295
|
+
outcome = (row["outcome"] or "").lower()
|
|
296
|
+
if _contains_any(outcome, NEGATIVE_OUTCOME_TERMS):
|
|
297
|
+
decision_negative += 1
|
|
298
|
+
elif _contains_any(outcome, POSITIVE_OUTCOME_TERMS):
|
|
299
|
+
decision_positive += 1
|
|
300
|
+
|
|
301
|
+
outcome_positive = 0
|
|
302
|
+
outcome_negative = 0
|
|
303
|
+
matched_outcomes = 0
|
|
304
|
+
if conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='outcomes'").fetchone():
|
|
305
|
+
for token in tokens[:3]:
|
|
306
|
+
rows = conn.execute(
|
|
307
|
+
"""SELECT status FROM outcomes
|
|
308
|
+
WHERE lower(description) LIKE ? OR lower(expected_result) LIKE ? OR lower(action_type) LIKE ?
|
|
309
|
+
ORDER BY created_at DESC LIMIT 6""",
|
|
310
|
+
tuple(f"%{token}%" for _ in range(3)),
|
|
311
|
+
).fetchall()
|
|
312
|
+
for row in rows:
|
|
313
|
+
matched_outcomes += 1
|
|
314
|
+
status = (row["status"] or "").lower()
|
|
315
|
+
if status == "met":
|
|
316
|
+
outcome_positive += 1
|
|
317
|
+
elif status in {"missed", "expired"}:
|
|
318
|
+
outcome_negative += 1
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
"positive": min(2.5, (decision_positive * 0.4) + (outcome_positive * 0.5)),
|
|
322
|
+
"negative": min(3.0, (decision_negative * 0.6) + (outcome_negative * 0.7)),
|
|
323
|
+
"matched_decisions": matched_decisions,
|
|
324
|
+
"matched_outcomes": matched_outcomes,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _historical_outcome_signal(
|
|
329
|
+
choice_name: str,
|
|
330
|
+
*,
|
|
331
|
+
area: str = "",
|
|
332
|
+
task_type: str = "",
|
|
333
|
+
goal_profile_id: str = "",
|
|
334
|
+
) -> dict:
|
|
335
|
+
conn = _get_db()
|
|
336
|
+
clean_choice = (choice_name or "").strip().lower()
|
|
337
|
+
if not clean_choice:
|
|
338
|
+
return {
|
|
339
|
+
"active": False,
|
|
340
|
+
"threshold": HISTORICAL_OUTCOME_MIN_RESOLVED,
|
|
341
|
+
"resolved_outcomes": 0,
|
|
342
|
+
"met": 0,
|
|
343
|
+
"missed": 0,
|
|
344
|
+
"success_rate": None,
|
|
345
|
+
"success_adjustment": 0.0,
|
|
346
|
+
"risk_adjustment": 0.0,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
has_eval = conn.execute(
|
|
350
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='cortex_evaluations'"
|
|
351
|
+
).fetchone()
|
|
352
|
+
has_outcomes = conn.execute(
|
|
353
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='outcomes'"
|
|
354
|
+
).fetchone()
|
|
355
|
+
if not has_eval or not has_outcomes:
|
|
356
|
+
return {
|
|
357
|
+
"active": False,
|
|
358
|
+
"threshold": HISTORICAL_OUTCOME_MIN_RESOLVED,
|
|
359
|
+
"resolved_outcomes": 0,
|
|
360
|
+
"met": 0,
|
|
361
|
+
"missed": 0,
|
|
362
|
+
"success_rate": None,
|
|
363
|
+
"success_adjustment": 0.0,
|
|
364
|
+
"risk_adjustment": 0.0,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
clauses = [
|
|
368
|
+
"lower(e.selected_choice) = ?",
|
|
369
|
+
"o.status IN ('met', 'missed')",
|
|
370
|
+
]
|
|
371
|
+
params: list[object] = [clean_choice]
|
|
372
|
+
if (area or "").strip():
|
|
373
|
+
clauses.append("e.area = ?")
|
|
374
|
+
params.append(area.strip())
|
|
375
|
+
if (task_type or "").strip():
|
|
376
|
+
clauses.append("e.task_type = ?")
|
|
377
|
+
params.append(task_type.strip())
|
|
378
|
+
if (goal_profile_id or "").strip():
|
|
379
|
+
clauses.append("e.goal_profile_id = ?")
|
|
380
|
+
params.append(goal_profile_id.strip())
|
|
381
|
+
|
|
382
|
+
rows = conn.execute(
|
|
383
|
+
f"""SELECT o.status
|
|
384
|
+
FROM cortex_evaluations e
|
|
385
|
+
JOIN outcomes o ON o.id = e.linked_outcome_id
|
|
386
|
+
WHERE {' AND '.join(clauses)}
|
|
387
|
+
ORDER BY e.created_at DESC, e.id DESC
|
|
388
|
+
LIMIT ?""",
|
|
389
|
+
params + [HISTORICAL_OUTCOME_LOOKBACK],
|
|
390
|
+
).fetchall()
|
|
391
|
+
met = sum(1 for row in rows if (row["status"] or "").lower() == "met")
|
|
392
|
+
missed = sum(1 for row in rows if (row["status"] or "").lower() == "missed")
|
|
393
|
+
resolved = met + missed
|
|
394
|
+
active = resolved >= HISTORICAL_OUTCOME_MIN_RESOLVED
|
|
395
|
+
success_rate = round(met / resolved, 3) if resolved else None
|
|
396
|
+
|
|
397
|
+
success_adjustment = 0.0
|
|
398
|
+
risk_adjustment = 0.0
|
|
399
|
+
if active and success_rate is not None:
|
|
400
|
+
centered = success_rate - 0.5
|
|
401
|
+
success_adjustment = round(centered * 5.0, 2)
|
|
402
|
+
risk_adjustment = round((0.5 - success_rate) * 3.6, 2)
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
"active": active,
|
|
406
|
+
"threshold": HISTORICAL_OUTCOME_MIN_RESOLVED,
|
|
407
|
+
"resolved_outcomes": resolved,
|
|
408
|
+
"met": met,
|
|
409
|
+
"missed": missed,
|
|
410
|
+
"success_rate": success_rate,
|
|
411
|
+
"success_adjustment": success_adjustment,
|
|
412
|
+
"risk_adjustment": risk_adjustment,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _pattern_learning_signal(
|
|
417
|
+
choice_name: str,
|
|
418
|
+
*,
|
|
419
|
+
area: str = "",
|
|
420
|
+
task_type: str = "",
|
|
421
|
+
goal_profile_id: str = "",
|
|
422
|
+
) -> dict:
|
|
423
|
+
clean_choice = (choice_name or "").strip()
|
|
424
|
+
if not clean_choice:
|
|
425
|
+
return {
|
|
426
|
+
"active": False,
|
|
427
|
+
"pattern_key": "",
|
|
428
|
+
"learning_id": 0,
|
|
429
|
+
"mode": "",
|
|
430
|
+
"title": "",
|
|
431
|
+
"success_adjustment": 0.0,
|
|
432
|
+
"risk_adjustment": 0.0,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
from db._outcomes import get_outcome_pattern_learning_signal
|
|
437
|
+
except Exception:
|
|
438
|
+
return {
|
|
439
|
+
"active": False,
|
|
440
|
+
"pattern_key": "",
|
|
441
|
+
"learning_id": 0,
|
|
442
|
+
"mode": "",
|
|
443
|
+
"title": "",
|
|
444
|
+
"success_adjustment": 0.0,
|
|
445
|
+
"risk_adjustment": 0.0,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return get_outcome_pattern_learning_signal(
|
|
449
|
+
area=area,
|
|
450
|
+
task_type=task_type,
|
|
451
|
+
goal_profile_id=goal_profile_id,
|
|
452
|
+
selected_choice=clean_choice,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _somatic_penalty(*parts: str) -> float:
|
|
457
|
+
conn = _get_db()
|
|
458
|
+
if not conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='somatic_events'").fetchone():
|
|
459
|
+
return 0.0
|
|
460
|
+
|
|
461
|
+
query_terms = [token for token in _tokenize(" ".join(parts), limit=4) if token]
|
|
462
|
+
if not query_terms:
|
|
463
|
+
return 0.0
|
|
464
|
+
|
|
465
|
+
penalty = 0.0
|
|
466
|
+
for term in query_terms[:3]:
|
|
467
|
+
rows = conn.execute(
|
|
468
|
+
"""SELECT delta FROM somatic_events
|
|
469
|
+
WHERE projected = 0 AND lower(target) LIKE ?
|
|
470
|
+
ORDER BY timestamp DESC LIMIT 8""",
|
|
471
|
+
(f"%{term}%",),
|
|
472
|
+
).fetchall()
|
|
473
|
+
for row in rows:
|
|
474
|
+
delta = float(row["delta"] or 0.0)
|
|
475
|
+
if delta < 0:
|
|
476
|
+
penalty += abs(delta)
|
|
477
|
+
return round(min(5.0, penalty), 2)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _resolve_linked_outcome_id(*, linked_outcome_id: int | str | None = None, task_id: str = "") -> int | None:
|
|
481
|
+
try:
|
|
482
|
+
explicit = int(linked_outcome_id or 0)
|
|
483
|
+
except (TypeError, ValueError):
|
|
484
|
+
explicit = 0
|
|
485
|
+
if explicit > 0:
|
|
486
|
+
return explicit
|
|
487
|
+
|
|
488
|
+
clean_task_id = (task_id or "").strip()
|
|
489
|
+
if not clean_task_id:
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
conn = _get_db()
|
|
493
|
+
if not conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='outcomes'").fetchone():
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
row = conn.execute(
|
|
497
|
+
"""SELECT id FROM outcomes
|
|
498
|
+
WHERE action_id = ? AND status = 'pending'
|
|
499
|
+
ORDER BY
|
|
500
|
+
CASE metric_source
|
|
501
|
+
WHEN 'protocol_task_status' THEN 0
|
|
502
|
+
WHEN 'decision_outcome' THEN 1
|
|
503
|
+
ELSE 2
|
|
504
|
+
END,
|
|
505
|
+
deadline ASC,
|
|
506
|
+
created_at DESC
|
|
507
|
+
LIMIT 1""",
|
|
508
|
+
(clean_task_id,),
|
|
509
|
+
).fetchone()
|
|
510
|
+
return int(row["id"]) if row else None
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _score_alternative(
|
|
514
|
+
alternative: dict,
|
|
515
|
+
*,
|
|
516
|
+
goal: str,
|
|
517
|
+
area: str,
|
|
518
|
+
task_type: str,
|
|
519
|
+
impact_level: str,
|
|
520
|
+
constraints: list[str],
|
|
521
|
+
evidence_refs: list[str],
|
|
522
|
+
goal_profile: dict,
|
|
523
|
+
) -> dict:
|
|
524
|
+
text = " ".join([
|
|
525
|
+
alternative.get("name", ""),
|
|
526
|
+
alternative.get("description", ""),
|
|
527
|
+
" ".join(alternative.get("pros") or []),
|
|
528
|
+
" ".join(alternative.get("cons") or []),
|
|
529
|
+
]).strip()
|
|
530
|
+
lowered = text.lower()
|
|
531
|
+
impact = _impact_base(impact_level)
|
|
532
|
+
success = 5.0 + min(2.0, len(evidence_refs) * 0.4)
|
|
533
|
+
risk = 2.5
|
|
534
|
+
reasons: list[str] = []
|
|
535
|
+
weights = goal_profile.get("weights") or {}
|
|
536
|
+
direct_hits = _term_hits(lowered, DIRECT_IMPACT_TERMS)
|
|
537
|
+
safe_hits = _term_hits(lowered, SAFE_TERMS)
|
|
538
|
+
risk_hits = _term_hits(lowered, RISK_TERMS)
|
|
539
|
+
focus = max(weights, key=weights.get) if weights else "impact"
|
|
540
|
+
|
|
541
|
+
if direct_hits:
|
|
542
|
+
impact += min(1.6, direct_hits * 0.4)
|
|
543
|
+
reasons.append("apunta directo al objetivo")
|
|
544
|
+
if safe_hits:
|
|
545
|
+
success += min(1.8, safe_hits * 0.45)
|
|
546
|
+
risk = max(1.0, risk - min(1.1, safe_hits * 0.35))
|
|
547
|
+
reasons.append("incluye verificación o despliegue seguro")
|
|
548
|
+
if not safe_hits and task_type in {"edit", "execute"}:
|
|
549
|
+
risk += 1.2
|
|
550
|
+
reasons.append("no explicita verificación")
|
|
551
|
+
if risk_hits:
|
|
552
|
+
risk += min(2.8, risk_hits * 0.7)
|
|
553
|
+
reasons.append("contiene señales de alto riesgo")
|
|
554
|
+
|
|
555
|
+
if focus == "impact" and direct_hits:
|
|
556
|
+
impact += 0.45
|
|
557
|
+
risk = max(1.0, risk - 0.35)
|
|
558
|
+
reasons.append("el perfil activo prioriza impacto")
|
|
559
|
+
elif focus == "impact":
|
|
560
|
+
impact = max(1.0, impact - 0.35)
|
|
561
|
+
reasons.append("el perfil activo penaliza opciones de bajo empuje")
|
|
562
|
+
elif focus == "success" and safe_hits:
|
|
563
|
+
success += 0.45
|
|
564
|
+
reasons.append("el perfil activo prioriza exito verificable")
|
|
565
|
+
elif focus == "risk":
|
|
566
|
+
if safe_hits:
|
|
567
|
+
risk = max(1.0, risk - 0.4)
|
|
568
|
+
if risk_hits:
|
|
569
|
+
risk += 0.8
|
|
570
|
+
reasons.append("el perfil activo penaliza riesgo")
|
|
571
|
+
elif focus == "somatic":
|
|
572
|
+
reasons.append("el perfil activo da peso a la huella somática")
|
|
573
|
+
|
|
574
|
+
history = _history_signal(lowered, area=area, goal=goal)
|
|
575
|
+
success += history["positive"]
|
|
576
|
+
risk += history["negative"]
|
|
577
|
+
if history["positive"]:
|
|
578
|
+
reasons.append("histórico parecido favorable")
|
|
579
|
+
if history["negative"]:
|
|
580
|
+
reasons.append("histórico parecido conflictivo")
|
|
581
|
+
|
|
582
|
+
historical = _historical_outcome_signal(
|
|
583
|
+
alternative.get("name", ""),
|
|
584
|
+
area=area,
|
|
585
|
+
task_type=task_type,
|
|
586
|
+
goal_profile_id=(goal_profile.get("profile_id") or ""),
|
|
587
|
+
)
|
|
588
|
+
if historical["active"]:
|
|
589
|
+
success += historical["success_adjustment"]
|
|
590
|
+
risk += historical["risk_adjustment"]
|
|
591
|
+
if historical["success_adjustment"] > 0:
|
|
592
|
+
reasons.append(
|
|
593
|
+
f"histórico resuelto favorable ({historical['met']}/{historical['resolved_outcomes']} met)"
|
|
594
|
+
)
|
|
595
|
+
elif historical["success_adjustment"] < 0:
|
|
596
|
+
reasons.append(
|
|
597
|
+
f"histórico resuelto flojo ({historical['missed']}/{historical['resolved_outcomes']} missed)"
|
|
598
|
+
)
|
|
599
|
+
elif historical["resolved_outcomes"] > 0:
|
|
600
|
+
reasons.append(
|
|
601
|
+
f"histórico insuficiente aún ({historical['resolved_outcomes']}/{historical['threshold']} outcomes)"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
pattern_learning = _pattern_learning_signal(
|
|
605
|
+
alternative.get("name", ""),
|
|
606
|
+
area=area,
|
|
607
|
+
task_type=task_type,
|
|
608
|
+
goal_profile_id=(goal_profile.get("profile_id") or ""),
|
|
609
|
+
)
|
|
610
|
+
if pattern_learning["active"]:
|
|
611
|
+
success += pattern_learning["success_adjustment"]
|
|
612
|
+
risk += pattern_learning["risk_adjustment"]
|
|
613
|
+
if pattern_learning["mode"] == "prefer":
|
|
614
|
+
reasons.append("regla estructurada capturada favorece esta estrategia")
|
|
615
|
+
elif pattern_learning["mode"] == "avoid":
|
|
616
|
+
reasons.append("regla estructurada capturada penaliza esta estrategia")
|
|
617
|
+
|
|
618
|
+
constraint_penalty, constraint_reasons = _constraint_penalty(lowered, constraints)
|
|
619
|
+
if constraint_penalty:
|
|
620
|
+
risk += constraint_penalty
|
|
621
|
+
reasons.extend(constraint_reasons)
|
|
622
|
+
|
|
623
|
+
somatic = _somatic_penalty(area, goal, lowered)
|
|
624
|
+
total = round(
|
|
625
|
+
(impact * float(weights.get("impact", 0.35)))
|
|
626
|
+
+ (success * float(weights.get("success", 0.30)))
|
|
627
|
+
- (risk * float(weights.get("risk", 0.20)))
|
|
628
|
+
- (somatic * float(weights.get("somatic", 0.15))),
|
|
629
|
+
3,
|
|
630
|
+
)
|
|
631
|
+
return {
|
|
632
|
+
"name": alternative.get("name", ""),
|
|
633
|
+
"impact": round(max(1.0, min(10.0, impact)), 2),
|
|
634
|
+
"success_probability": round(max(1.0, min(10.0, success)), 2),
|
|
635
|
+
"risk_level": round(max(1.0, min(10.0, risk)), 2),
|
|
636
|
+
"somatic_penalty": round(max(0.0, min(5.0, somatic)), 2),
|
|
637
|
+
"total_score": total,
|
|
638
|
+
"notes": reasons[:4],
|
|
639
|
+
"goal_profile_focus": focus,
|
|
640
|
+
"history_matches": {
|
|
641
|
+
"decisions": history["matched_decisions"],
|
|
642
|
+
"outcomes": history["matched_outcomes"],
|
|
643
|
+
},
|
|
644
|
+
"historical_signal": historical,
|
|
645
|
+
"pattern_learning_signal": pattern_learning,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def evaluate_cortex_state(state: dict) -> dict:
|
|
650
|
+
"""Return structured Cortex evaluation for internal callers."""
|
|
651
|
+
result = _validate_state(state)
|
|
652
|
+
result["check_id"] = f"CTX-{int(time.time())}-{secrets.randbelow(100000)}"
|
|
653
|
+
result["expires_at_epoch"] = int(time.time()) + 1200
|
|
654
|
+
return result
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _log_cortex_activation(goal: str, task_type: str, result: dict):
|
|
658
|
+
try:
|
|
659
|
+
conn = _get_db()
|
|
660
|
+
conn.execute(
|
|
661
|
+
"""CREATE TABLE IF NOT EXISTS cortex_log (
|
|
662
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
663
|
+
goal TEXT,
|
|
664
|
+
task_type TEXT,
|
|
665
|
+
mode TEXT,
|
|
666
|
+
warnings TEXT,
|
|
667
|
+
trust_score INTEGER,
|
|
668
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
669
|
+
)"""
|
|
670
|
+
)
|
|
671
|
+
conn.execute(
|
|
672
|
+
"INSERT INTO cortex_log (goal, task_type, mode, warnings, trust_score) VALUES (?, ?, ?, ?, ?)",
|
|
673
|
+
(
|
|
674
|
+
goal[:200],
|
|
675
|
+
task_type,
|
|
676
|
+
result["mode"],
|
|
677
|
+
json.dumps(result["warnings"]),
|
|
678
|
+
result["trust_score"],
|
|
679
|
+
),
|
|
680
|
+
)
|
|
681
|
+
conn.commit()
|
|
682
|
+
except Exception:
|
|
683
|
+
pass
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _format_decision_summary(recommended: dict, alternatives_scored: list[dict]) -> str:
|
|
687
|
+
notes = ", ".join(recommended.get("notes") or []) or "balance general más sólido"
|
|
688
|
+
historical = recommended.get("historical_signal") or {}
|
|
689
|
+
second_gap = 0.0
|
|
690
|
+
if len(alternatives_scored) > 1:
|
|
691
|
+
second_gap = recommended["total_score"] - alternatives_scored[1]["total_score"]
|
|
692
|
+
if historical.get("active"):
|
|
693
|
+
notes = (
|
|
694
|
+
f"{notes}; histórico resuelto {historical.get('met', 0)}/"
|
|
695
|
+
f"{historical.get('resolved_outcomes', 0)} favorable en contexto comparable"
|
|
696
|
+
)
|
|
697
|
+
if second_gap > 0.2:
|
|
698
|
+
return f"Recomendada por margen claro ({second_gap:.2f}) y porque {notes}."
|
|
699
|
+
return f"Recomendada por el mejor balance entre impacto, éxito, riesgo y huella somática; {notes}."
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def handle_cortex_check(
|
|
703
|
+
goal: str,
|
|
704
|
+
task_type: str = "answer",
|
|
705
|
+
plan: str = "[]",
|
|
706
|
+
known_facts: str = "[]",
|
|
707
|
+
unknowns: str = "[]",
|
|
708
|
+
constraints: str = "[]",
|
|
709
|
+
evidence_refs: str = "[]",
|
|
710
|
+
verification_step: str = "",
|
|
711
|
+
) -> str:
|
|
712
|
+
"""Cognitive Cortex pre-action check. Call BEFORE significant actions.
|
|
713
|
+
|
|
714
|
+
Validates your reasoning state and determines if you can act, should propose,
|
|
715
|
+
or need to ask for clarification first. Implements architectural inhibitory control.
|
|
716
|
+
|
|
717
|
+
WHEN TO CALL:
|
|
718
|
+
- Before editing files or running commands
|
|
719
|
+
- Before delegating to subagents
|
|
720
|
+
- When the task has multiple possible approaches
|
|
721
|
+
- After a failed attempt (before retrying)
|
|
722
|
+
- When user instruction seems to conflict with known facts
|
|
723
|
+
|
|
724
|
+
DO NOT CALL for simple chat responses, greetings, or explanations.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
goal: What you are trying to achieve (required)
|
|
728
|
+
task_type: One of: answer, analyze, edit, execute, delegate
|
|
729
|
+
plan: JSON array of planned steps (e.g. '["read file", "edit function", "test"]')
|
|
730
|
+
known_facts: JSON array of facts you have (from user, memory, files)
|
|
731
|
+
unknowns: JSON array of things you don't know yet but need
|
|
732
|
+
constraints: JSON array of rules or limitations that apply
|
|
733
|
+
evidence_refs: JSON array of evidence supporting your plan (learnings, user statements, file contents)
|
|
734
|
+
verification_step: How you will verify the action worked
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
Mode (ask/propose/act), available tools, warnings, and relevant Core Rules
|
|
738
|
+
"""
|
|
739
|
+
try:
|
|
740
|
+
clean_type = validate_task_type(task_type)
|
|
741
|
+
except ValueError as exc:
|
|
742
|
+
return "\n".join(
|
|
743
|
+
[
|
|
744
|
+
f"ERROR: {exc}",
|
|
745
|
+
f"Valid task types: {', '.join(sorted(VALID_TASK_TYPES))}",
|
|
746
|
+
]
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
state = {
|
|
750
|
+
"goal": goal.strip() if goal else "",
|
|
751
|
+
"task_type": clean_type,
|
|
752
|
+
"plan": _parse_json_list(plan),
|
|
753
|
+
"known_facts": _parse_json_list(known_facts),
|
|
754
|
+
"unknowns": _parse_json_list(unknowns),
|
|
755
|
+
"constraints": _parse_json_list(constraints),
|
|
756
|
+
"evidence_refs": _parse_json_list(evidence_refs),
|
|
757
|
+
"verification_step": verification_step.strip() if verification_step else "",
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
result = evaluate_cortex_state(state)
|
|
761
|
+
|
|
762
|
+
# Format response
|
|
763
|
+
lines = [
|
|
764
|
+
f"CORTEX CHECK — mode: {result['mode'].upper()}",
|
|
765
|
+
f"Trust: {result['trust_score']}/100",
|
|
766
|
+
f"Check ID: {result['check_id']}",
|
|
767
|
+
f"Valid until epoch: {result['expires_at_epoch']}",
|
|
768
|
+
]
|
|
769
|
+
|
|
770
|
+
if result["mode"] == "act":
|
|
771
|
+
lines.append("CLEARED: You may proceed with the action.")
|
|
772
|
+
elif result["mode"] == "propose":
|
|
773
|
+
lines.append(f"PROPOSE ONLY: {result['blocked_reason']}")
|
|
774
|
+
lines.append("Show the user your plan and get approval before executing.")
|
|
775
|
+
elif result["mode"] == "ask":
|
|
776
|
+
lines.append(f"ASK FIRST: {result['blocked_reason']}")
|
|
777
|
+
lines.append("Gather the missing information before proceeding.")
|
|
778
|
+
|
|
779
|
+
if result["warnings"]:
|
|
780
|
+
lines.append("")
|
|
781
|
+
lines.append("Warnings:")
|
|
782
|
+
for w in result["warnings"]:
|
|
783
|
+
lines.append(f" - {w}")
|
|
784
|
+
|
|
785
|
+
if result["injected_rules"]:
|
|
786
|
+
lines.append("")
|
|
787
|
+
lines.append("Applicable Core Rules:")
|
|
788
|
+
for r in result["injected_rules"]:
|
|
789
|
+
lines.append(f" - {r}")
|
|
790
|
+
|
|
791
|
+
lines.append("")
|
|
792
|
+
lines.append(f"Tools available: {', '.join(result['tools_available'])}")
|
|
793
|
+
|
|
794
|
+
_log_cortex_activation(goal, task_type, result)
|
|
795
|
+
|
|
796
|
+
return "\n".join(lines)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def handle_cortex_stats(days: int = 7) -> str:
|
|
800
|
+
"""View Cortex activation statistics — how often it activates, modes, warnings.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
days: Period to analyze (default 7)
|
|
804
|
+
"""
|
|
805
|
+
conn = _get_db()
|
|
806
|
+
try:
|
|
807
|
+
conn.execute("SELECT 1 FROM cortex_log LIMIT 1")
|
|
808
|
+
except Exception:
|
|
809
|
+
return "No Cortex data yet. The Cortex activates on significant actions."
|
|
810
|
+
|
|
811
|
+
cutoff = f"datetime('now', '-{days} days')"
|
|
812
|
+
|
|
813
|
+
total = conn.execute(f"SELECT COUNT(*) FROM cortex_log WHERE created_at >= {cutoff}").fetchone()[0]
|
|
814
|
+
by_mode = conn.execute(
|
|
815
|
+
f"SELECT mode, COUNT(*) as c FROM cortex_log WHERE created_at >= {cutoff} GROUP BY mode ORDER BY c DESC"
|
|
816
|
+
).fetchall()
|
|
817
|
+
by_type = conn.execute(
|
|
818
|
+
f"SELECT task_type, COUNT(*) as c FROM cortex_log WHERE created_at >= {cutoff} GROUP BY task_type ORDER BY c DESC"
|
|
819
|
+
).fetchall()
|
|
820
|
+
|
|
821
|
+
lines = [
|
|
822
|
+
f"CORTEX STATS — last {days} days",
|
|
823
|
+
f"Total activations: {total}",
|
|
824
|
+
"",
|
|
825
|
+
"By mode:",
|
|
826
|
+
]
|
|
827
|
+
for r in by_mode:
|
|
828
|
+
pct = (r["c"] / total * 100) if total > 0 else 0
|
|
829
|
+
lines.append(f" {r['mode']}: {r['c']} ({pct:.0f}%)")
|
|
830
|
+
|
|
831
|
+
lines.append("")
|
|
832
|
+
lines.append("By task type:")
|
|
833
|
+
for r in by_type:
|
|
834
|
+
lines.append(f" {r['task_type']}: {r['c']}")
|
|
835
|
+
|
|
836
|
+
# Inhibition rate = % of activations that resulted in ask or propose (not act)
|
|
837
|
+
inhibited = sum(r["c"] for r in by_mode if r["mode"] != "act")
|
|
838
|
+
inhibition_rate = (inhibited / total * 100) if total > 0 else 0
|
|
839
|
+
lines.append(f"\nInhibition rate: {inhibition_rate:.0f}% (target: 30-60%)")
|
|
840
|
+
|
|
841
|
+
return "\n".join(lines)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def handle_cortex_decide(
|
|
845
|
+
goal: str,
|
|
846
|
+
alternatives: str,
|
|
847
|
+
task_type: str = "execute",
|
|
848
|
+
impact_level: str = "high",
|
|
849
|
+
context_hint: str = "",
|
|
850
|
+
area: str = "",
|
|
851
|
+
constraints: str = "[]",
|
|
852
|
+
evidence_refs: str = "[]",
|
|
853
|
+
session_id: str = "",
|
|
854
|
+
task_id: str = "",
|
|
855
|
+
linked_outcome_id: int = 0,
|
|
856
|
+
goal_profile_id: str = "",
|
|
857
|
+
goal_id: str = "",
|
|
858
|
+
) -> str:
|
|
859
|
+
"""Evaluate concrete alternatives for a high-impact task using the existing Cortex."""
|
|
860
|
+
clean_goal = (goal or "").strip()
|
|
861
|
+
if not clean_goal:
|
|
862
|
+
return json.dumps({"ok": False, "error": "goal is required"}, ensure_ascii=False, indent=2)
|
|
863
|
+
|
|
864
|
+
parsed_alternatives = _parse_alternatives(alternatives)
|
|
865
|
+
if len(parsed_alternatives) < 2:
|
|
866
|
+
return json.dumps(
|
|
867
|
+
{
|
|
868
|
+
"ok": False,
|
|
869
|
+
"error": "Provide at least 2 alternatives so the Cortex can rank tradeoffs.",
|
|
870
|
+
},
|
|
871
|
+
ensure_ascii=False,
|
|
872
|
+
indent=2,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
try:
|
|
876
|
+
clean_type = validate_task_type(task_type)
|
|
877
|
+
except ValueError as exc:
|
|
878
|
+
return json.dumps(
|
|
879
|
+
{
|
|
880
|
+
"ok": False,
|
|
881
|
+
"error": str(exc),
|
|
882
|
+
"valid_task_types": sorted(VALID_TASK_TYPES),
|
|
883
|
+
},
|
|
884
|
+
ensure_ascii=False,
|
|
885
|
+
indent=2,
|
|
886
|
+
)
|
|
887
|
+
try:
|
|
888
|
+
clean_level = validate_impact_level(impact_level)
|
|
889
|
+
except ValueError as exc:
|
|
890
|
+
return json.dumps(
|
|
891
|
+
{
|
|
892
|
+
"ok": False,
|
|
893
|
+
"error": str(exc),
|
|
894
|
+
"valid_impact_levels": sorted(VALID_IMPACT_LEVELS),
|
|
895
|
+
},
|
|
896
|
+
ensure_ascii=False,
|
|
897
|
+
indent=2,
|
|
898
|
+
)
|
|
899
|
+
parsed_constraints = _parse_json_list(constraints)
|
|
900
|
+
parsed_evidence = _parse_json_list(evidence_refs)
|
|
901
|
+
try:
|
|
902
|
+
from db import resolve_goal_profile
|
|
903
|
+
|
|
904
|
+
resolved_goal_profile = resolve_goal_profile(
|
|
905
|
+
profile_id=goal_profile_id,
|
|
906
|
+
area=area.strip(),
|
|
907
|
+
task_type=clean_type,
|
|
908
|
+
goal_id=goal_id,
|
|
909
|
+
)
|
|
910
|
+
except Exception as exc:
|
|
911
|
+
return json.dumps({"ok": False, "error": f"Failed to resolve goal profile: {exc}"}, ensure_ascii=False, indent=2)
|
|
912
|
+
|
|
913
|
+
scored = [
|
|
914
|
+
_score_alternative(
|
|
915
|
+
item,
|
|
916
|
+
goal=clean_goal,
|
|
917
|
+
area=area.strip(),
|
|
918
|
+
task_type=clean_type,
|
|
919
|
+
impact_level=clean_level,
|
|
920
|
+
constraints=parsed_constraints,
|
|
921
|
+
evidence_refs=parsed_evidence,
|
|
922
|
+
goal_profile=resolved_goal_profile,
|
|
923
|
+
)
|
|
924
|
+
for item in parsed_alternatives
|
|
925
|
+
]
|
|
926
|
+
scored.sort(key=lambda item: item["total_score"], reverse=True)
|
|
927
|
+
recommended = scored[0]
|
|
928
|
+
reasoning = _format_decision_summary(recommended, scored)
|
|
929
|
+
resolved_outcome_id = _resolve_linked_outcome_id(
|
|
930
|
+
linked_outcome_id=linked_outcome_id,
|
|
931
|
+
task_id=task_id,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Auto-create outcome when none exists, so cortex decisions
|
|
935
|
+
# get verified by outcome-checker and close the feedback loop.
|
|
936
|
+
if resolved_outcome_id is None and clean_goal and task_id:
|
|
937
|
+
try:
|
|
938
|
+
from db import create_outcome
|
|
939
|
+
|
|
940
|
+
_deadline = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
|
|
941
|
+
_outcome = create_outcome(
|
|
942
|
+
action_type="cortex_decision",
|
|
943
|
+
description=f"Cortex decision: {clean_goal[:120]}",
|
|
944
|
+
expected_result=f"Recommended '{scored[0]['name']}' succeeds",
|
|
945
|
+
metric_source="decision_outcome",
|
|
946
|
+
action_id=task_id,
|
|
947
|
+
session_id=session_id,
|
|
948
|
+
deadline=_deadline,
|
|
949
|
+
)
|
|
950
|
+
if isinstance(_outcome, dict) and _outcome.get("id"):
|
|
951
|
+
resolved_outcome_id = int(_outcome["id"])
|
|
952
|
+
except Exception:
|
|
953
|
+
pass # non-critical: decision still records without outcome
|
|
954
|
+
|
|
955
|
+
try:
|
|
956
|
+
from db import create_cortex_evaluation
|
|
957
|
+
|
|
958
|
+
record = create_cortex_evaluation(
|
|
959
|
+
session_id=session_id,
|
|
960
|
+
task_id=task_id,
|
|
961
|
+
goal=clean_goal,
|
|
962
|
+
task_type=clean_type,
|
|
963
|
+
area=area,
|
|
964
|
+
impact_level=clean_level,
|
|
965
|
+
context_hint=context_hint,
|
|
966
|
+
alternatives=parsed_alternatives,
|
|
967
|
+
scores=scored,
|
|
968
|
+
recommended_choice=recommended["name"],
|
|
969
|
+
recommended_reasoning=reasoning,
|
|
970
|
+
linked_outcome_id=resolved_outcome_id,
|
|
971
|
+
goal_profile_id=resolved_goal_profile.get("profile_id", ""),
|
|
972
|
+
goal_profile_labels=resolved_goal_profile.get("goal_labels", []),
|
|
973
|
+
goal_profile_weights=resolved_goal_profile.get("weights", {}),
|
|
974
|
+
selected_choice=recommended["name"],
|
|
975
|
+
selection_reason=reasoning,
|
|
976
|
+
selection_source="recommended",
|
|
977
|
+
)
|
|
978
|
+
except Exception as exc:
|
|
979
|
+
return json.dumps(
|
|
980
|
+
{
|
|
981
|
+
"ok": False,
|
|
982
|
+
"error": f"Failed to persist cortex evaluation: {exc}",
|
|
983
|
+
},
|
|
984
|
+
ensure_ascii=False,
|
|
985
|
+
indent=2,
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
return json.dumps(
|
|
989
|
+
{
|
|
990
|
+
"ok": True,
|
|
991
|
+
"evaluation_id": record.get("id"),
|
|
992
|
+
"task_id": task_id,
|
|
993
|
+
"goal": clean_goal,
|
|
994
|
+
"impact_level": clean_level,
|
|
995
|
+
"recommendation": recommended["name"],
|
|
996
|
+
"reasoning": reasoning,
|
|
997
|
+
"selected_choice": record.get("selected_choice"),
|
|
998
|
+
"selection_source": record.get("selection_source"),
|
|
999
|
+
"linked_outcome_id": record.get("linked_outcome_id"),
|
|
1000
|
+
"goal_profile": {
|
|
1001
|
+
"profile_id": resolved_goal_profile.get("profile_id", ""),
|
|
1002
|
+
"profile_name": resolved_goal_profile.get("profile_name", ""),
|
|
1003
|
+
"resolved_by": resolved_goal_profile.get("resolved_by", ""),
|
|
1004
|
+
"goal_labels": resolved_goal_profile.get("goal_labels", []),
|
|
1005
|
+
"weights": resolved_goal_profile.get("weights", {}),
|
|
1006
|
+
},
|
|
1007
|
+
"alternatives": parsed_alternatives,
|
|
1008
|
+
"scores": scored,
|
|
1009
|
+
"next_action": "Apply the recommended choice or call nexo_cortex_override if you intentionally choose another option.",
|
|
1010
|
+
},
|
|
1011
|
+
ensure_ascii=False,
|
|
1012
|
+
indent=2,
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def handle_cortex_review(evaluation_id: int = 0, task_id: str = "", session_id: str = "", limit: int = 10) -> str:
|
|
1017
|
+
"""Review stored Cortex alternative evaluations."""
|
|
1018
|
+
from db import get_cortex_evaluation, list_cortex_evaluations
|
|
1019
|
+
|
|
1020
|
+
if evaluation_id:
|
|
1021
|
+
item = get_cortex_evaluation(evaluation_id)
|
|
1022
|
+
if not item:
|
|
1023
|
+
return json.dumps({"ok": False, "error": f"Unknown evaluation_id: {evaluation_id}"}, ensure_ascii=False, indent=2)
|
|
1024
|
+
return json.dumps({"ok": True, "evaluation": item}, ensure_ascii=False, indent=2)
|
|
1025
|
+
|
|
1026
|
+
items = list_cortex_evaluations(session_id=session_id, task_id=task_id, limit=limit)
|
|
1027
|
+
return json.dumps({"ok": True, "evaluations": items}, ensure_ascii=False, indent=2)
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def handle_cortex_override(evaluation_id: int, chosen: str, reason: str) -> str:
|
|
1031
|
+
"""Override the Cortex recommendation while leaving the recommendation trail intact."""
|
|
1032
|
+
if not chosen.strip():
|
|
1033
|
+
return json.dumps({"ok": False, "error": "chosen is required"}, ensure_ascii=False, indent=2)
|
|
1034
|
+
if not reason.strip():
|
|
1035
|
+
return json.dumps({"ok": False, "error": "reason is required"}, ensure_ascii=False, indent=2)
|
|
1036
|
+
|
|
1037
|
+
from db import get_cortex_evaluation, override_cortex_evaluation
|
|
1038
|
+
|
|
1039
|
+
current = get_cortex_evaluation(evaluation_id)
|
|
1040
|
+
if not current:
|
|
1041
|
+
return json.dumps({"ok": False, "error": f"Unknown evaluation_id: {evaluation_id}"}, ensure_ascii=False, indent=2)
|
|
1042
|
+
|
|
1043
|
+
alternatives = _parse_json_list(current.get("alternatives") or "[]")
|
|
1044
|
+
valid_names = {str(item.get("name", "")).strip() for item in alternatives if isinstance(item, dict)}
|
|
1045
|
+
if chosen.strip() not in valid_names:
|
|
1046
|
+
return json.dumps(
|
|
1047
|
+
{
|
|
1048
|
+
"ok": False,
|
|
1049
|
+
"error": "chosen must match one of the stored alternative names",
|
|
1050
|
+
"valid_choices": sorted(valid_names),
|
|
1051
|
+
},
|
|
1052
|
+
ensure_ascii=False,
|
|
1053
|
+
indent=2,
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
updated = override_cortex_evaluation(
|
|
1057
|
+
evaluation_id,
|
|
1058
|
+
selected_choice=chosen,
|
|
1059
|
+
selection_reason=reason,
|
|
1060
|
+
)
|
|
1061
|
+
return json.dumps({"ok": True, "evaluation": updated}, ensure_ascii=False, indent=2)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
# v5.2.0: Cortex quality cache reader. The `nexo-cortex-cycle` cron
|
|
1065
|
+
# (src/scripts/nexo-cortex-cycle.py) writes a fresh quality snapshot to
|
|
1066
|
+
# $NEXO_HOME/operations/cortex-quality-latest.json every 6h. Until this
|
|
1067
|
+
# release the reader was missing — the snapshot was write-only and every
|
|
1068
|
+
# call to `nexo_cortex_quality` re-ran the SQL summary. Now the handler
|
|
1069
|
+
# reads the cache first for the 7d / 1d windows and falls back silently
|
|
1070
|
+
# to the live computation on any failure.
|
|
1071
|
+
_CORTEX_QUALITY_CACHE_PATH = (
|
|
1072
|
+
Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
1073
|
+
/ "operations"
|
|
1074
|
+
/ "cortex-quality-latest.json"
|
|
1075
|
+
)
|
|
1076
|
+
# 6h cron + 30 min slack so a slightly-late run still serves cache.
|
|
1077
|
+
_CORTEX_QUALITY_CACHE_MAX_AGE_SECONDS = 23400
|
|
1078
|
+
_CORTEX_QUALITY_CACHE_WINDOWS = {1: "window_1d", 7: "window_7d"}
|
|
1079
|
+
_CORTEX_QUALITY_CACHE_SCHEMA = 1
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _load_cortex_quality_cache(days: int) -> dict | None:
|
|
1083
|
+
"""Return cached summary dict for the requested window, or None if unusable.
|
|
1084
|
+
|
|
1085
|
+
Silent on any failure so the live path always wins on a corrupt cache.
|
|
1086
|
+
Respects the snapshot schema written by `_persist_quality_snapshot`
|
|
1087
|
+
in src/scripts/nexo-cortex-cycle.py — do NOT change the layout here
|
|
1088
|
+
without updating the writer in the same release.
|
|
1089
|
+
"""
|
|
1090
|
+
window_key = _CORTEX_QUALITY_CACHE_WINDOWS.get(days)
|
|
1091
|
+
if window_key is None:
|
|
1092
|
+
return None
|
|
1093
|
+
try:
|
|
1094
|
+
if not _CORTEX_QUALITY_CACHE_PATH.is_file():
|
|
1095
|
+
return None
|
|
1096
|
+
payload = json.loads(
|
|
1097
|
+
_CORTEX_QUALITY_CACHE_PATH.read_text(encoding="utf-8")
|
|
1098
|
+
)
|
|
1099
|
+
except Exception:
|
|
1100
|
+
return None
|
|
1101
|
+
if not isinstance(payload, dict):
|
|
1102
|
+
return None
|
|
1103
|
+
if payload.get("schema") != _CORTEX_QUALITY_CACHE_SCHEMA:
|
|
1104
|
+
return None
|
|
1105
|
+
captured_at = payload.get("captured_at") or ""
|
|
1106
|
+
if not isinstance(captured_at, str):
|
|
1107
|
+
return None
|
|
1108
|
+
try:
|
|
1109
|
+
captured = datetime.fromisoformat(captured_at)
|
|
1110
|
+
except Exception:
|
|
1111
|
+
return None
|
|
1112
|
+
age = time.time() - captured.timestamp()
|
|
1113
|
+
if age < 0 or age > _CORTEX_QUALITY_CACHE_MAX_AGE_SECONDS:
|
|
1114
|
+
return None
|
|
1115
|
+
window = payload.get(window_key)
|
|
1116
|
+
if not isinstance(window, dict):
|
|
1117
|
+
return None
|
|
1118
|
+
return window
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def handle_cortex_quality(days: int = 30) -> str:
|
|
1122
|
+
"""Summarise recommendation quality, overrides, and linked outcome results.
|
|
1123
|
+
|
|
1124
|
+
v5.2.0: Serves the snapshot written by `nexo-cortex-cycle` when the
|
|
1125
|
+
requested window is 7 or 1 days and the snapshot is fresh
|
|
1126
|
+
(< 6h30m old, schema == 1). Falls back silently to a live SQL
|
|
1127
|
+
summary on any failure, so the caller always gets a valid response.
|
|
1128
|
+
The returned JSON includes `"source": "cache" | "live"` so the
|
|
1129
|
+
path taken is observable from the outside.
|
|
1130
|
+
"""
|
|
1131
|
+
from db import cortex_evaluation_summary
|
|
1132
|
+
|
|
1133
|
+
cached = _load_cortex_quality_cache(days)
|
|
1134
|
+
if cached is not None:
|
|
1135
|
+
return json.dumps(
|
|
1136
|
+
{"ok": True, "summary": cached, "source": "cache"},
|
|
1137
|
+
ensure_ascii=False,
|
|
1138
|
+
indent=2,
|
|
1139
|
+
)
|
|
1140
|
+
summary = cortex_evaluation_summary(days=days)
|
|
1141
|
+
return json.dumps(
|
|
1142
|
+
{"ok": True, "summary": summary, "source": "live"},
|
|
1143
|
+
ensure_ascii=False,
|
|
1144
|
+
indent=2,
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
TOOLS = [
|
|
1149
|
+
(handle_cortex_check, "nexo_cortex_check", "Cognitive pre-action check. Validates reasoning and determines if you can act, should propose, or need to ask first. Call before significant actions."),
|
|
1150
|
+
(handle_cortex_decide, "nexo_cortex_decide", "Evaluate 2+ alternatives for a high-impact task and persist the recommendation on top of the existing Cortex."),
|
|
1151
|
+
(handle_cortex_review, "nexo_cortex_review", "Review persisted Cortex alternative evaluations by ID, task, or session."),
|
|
1152
|
+
(handle_cortex_override, "nexo_cortex_override", "Override a stored Cortex recommendation while preserving the recommendation trail."),
|
|
1153
|
+
(handle_cortex_quality, "nexo_cortex_quality", "Summarise recommendation accept rate, override rate, and linked outcome success for Cortex evaluations."),
|
|
1154
|
+
(handle_cortex_stats, "nexo_cortex_stats", "View Cortex activation statistics — modes, task types, inhibition rate."),
|
|
1155
|
+
]
|