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/evolution_cycle.py
CHANGED
|
@@ -390,6 +390,68 @@ Quality over quantity. One strong improvement is better than three weak ones.
|
|
|
390
390
|
"""
|
|
391
391
|
|
|
392
392
|
|
|
393
|
+
def build_public_pr_review_prompt(
|
|
394
|
+
*,
|
|
395
|
+
pr_number: int,
|
|
396
|
+
title: str,
|
|
397
|
+
author: str,
|
|
398
|
+
url: str,
|
|
399
|
+
body: str,
|
|
400
|
+
files: list[str],
|
|
401
|
+
diff_text: str,
|
|
402
|
+
) -> str:
|
|
403
|
+
"""Prompt for peer-reviewing another public evolution PR.
|
|
404
|
+
|
|
405
|
+
This is used only when this machine already has its own Draft PR open, so
|
|
406
|
+
Evolution can still add value without opening a second PR.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
rendered_files = "\n".join(f"- {path}" for path in files[:40]) if files else "- (no file list provided)"
|
|
410
|
+
trimmed_diff = (diff_text or "").strip()
|
|
411
|
+
if len(trimmed_diff) > 80000:
|
|
412
|
+
trimmed_diff = trimmed_diff[:80000] + "\n\n[diff truncated by NEXO]"
|
|
413
|
+
|
|
414
|
+
return f"""You are NEXO Public Evolution Review.
|
|
415
|
+
|
|
416
|
+
You are reviewing another opt-in public evolution PR. You must NOT merge, rebase,
|
|
417
|
+
push, or edit the PR. Your only job is to decide whether it deserves an approval
|
|
418
|
+
or whether it should receive a review comment without approval.
|
|
419
|
+
|
|
420
|
+
STRICT RULES:
|
|
421
|
+
- Review only this PR:
|
|
422
|
+
- Number: #{pr_number}
|
|
423
|
+
- Author: {author}
|
|
424
|
+
- URL: {url}
|
|
425
|
+
- Base the review only on the provided title, body, file list, and diff
|
|
426
|
+
- Do not assume hidden context
|
|
427
|
+
- If confidence is not strong, choose `comment`, not `approve`
|
|
428
|
+
- If the diff is too incomplete, too risky, or too ambiguous, choose `skip`
|
|
429
|
+
- Never suggest merge authority; maintainers decide that later
|
|
430
|
+
- Keep the review concise, technical, and useful
|
|
431
|
+
|
|
432
|
+
PR TITLE:
|
|
433
|
+
{title}
|
|
434
|
+
|
|
435
|
+
PR BODY:
|
|
436
|
+
{body or "(empty)"}
|
|
437
|
+
|
|
438
|
+
FILES CHANGED:
|
|
439
|
+
{rendered_files}
|
|
440
|
+
|
|
441
|
+
DIFF:
|
|
442
|
+
```diff
|
|
443
|
+
{trimmed_diff or "(empty diff)"}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Return ONLY valid JSON:
|
|
447
|
+
{{
|
|
448
|
+
"decision": "approve|comment|skip",
|
|
449
|
+
"summary": "one-line verdict",
|
|
450
|
+
"body": "the exact markdown text to post as the review body"
|
|
451
|
+
}}
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
|
|
393
455
|
def max_auto_changes(total_evolutions: int) -> int:
|
|
394
456
|
"""Progressive trust: 1 for first 4 cycles, 2 for next 4, then 3."""
|
|
395
457
|
if total_evolutions < 4:
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Post-tool guardrails for conditioned file learnings."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from db import create_protocol_debt, get_db
|
|
10
|
+
from plugins.guard import _load_conditioned_learnings, _normalize_path_token
|
|
11
|
+
|
|
12
|
+
READ_LIKE_TOOLS = {"Read"}
|
|
13
|
+
WRITE_LIKE_TOOLS = {"Edit", "MultiEdit", "Write"}
|
|
14
|
+
DELETE_LIKE_TOOLS = {"Delete"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _operation_kind(tool_name: str) -> str:
|
|
18
|
+
if tool_name in READ_LIKE_TOOLS:
|
|
19
|
+
return "read"
|
|
20
|
+
if tool_name in WRITE_LIKE_TOOLS:
|
|
21
|
+
return "write"
|
|
22
|
+
if tool_name in DELETE_LIKE_TOOLS:
|
|
23
|
+
return "delete"
|
|
24
|
+
return "other"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_file_path(path: str) -> str:
|
|
28
|
+
return _normalize_path_token(str(Path(path)))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _extract_touched_files(tool_input) -> list[str]:
|
|
32
|
+
files: list[str] = []
|
|
33
|
+
if not isinstance(tool_input, dict):
|
|
34
|
+
return files
|
|
35
|
+
|
|
36
|
+
def add(candidate) -> None:
|
|
37
|
+
if isinstance(candidate, str) and candidate.strip():
|
|
38
|
+
files.append(candidate.strip())
|
|
39
|
+
|
|
40
|
+
add(tool_input.get("file_path"))
|
|
41
|
+
add(tool_input.get("path"))
|
|
42
|
+
|
|
43
|
+
for key in ("paths", "file_paths", "files"):
|
|
44
|
+
value = tool_input.get(key)
|
|
45
|
+
if isinstance(value, list):
|
|
46
|
+
for item in value:
|
|
47
|
+
if isinstance(item, str):
|
|
48
|
+
add(item)
|
|
49
|
+
elif isinstance(item, dict):
|
|
50
|
+
add(item.get("file_path"))
|
|
51
|
+
add(item.get("path"))
|
|
52
|
+
|
|
53
|
+
unique: list[str] = []
|
|
54
|
+
seen = set()
|
|
55
|
+
for item in files:
|
|
56
|
+
normalized = _normalize_file_path(item)
|
|
57
|
+
if normalized and normalized not in seen:
|
|
58
|
+
seen.add(normalized)
|
|
59
|
+
unique.append(item)
|
|
60
|
+
return unique
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_nexo_sid(conn, external_session_id: str) -> str:
|
|
64
|
+
if not external_session_id.strip():
|
|
65
|
+
return ""
|
|
66
|
+
row = conn.execute(
|
|
67
|
+
"""SELECT sid
|
|
68
|
+
FROM sessions
|
|
69
|
+
WHERE external_session_id = ? OR claude_session_id = ?
|
|
70
|
+
ORDER BY last_update_epoch DESC
|
|
71
|
+
LIMIT 1""",
|
|
72
|
+
(external_session_id.strip(), external_session_id.strip()),
|
|
73
|
+
).fetchone()
|
|
74
|
+
return str(row["sid"]) if row else ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _find_open_task_for_file(conn, sid: str, filepath: str) -> dict | None:
|
|
78
|
+
target = _normalize_file_path(filepath)
|
|
79
|
+
rows = conn.execute(
|
|
80
|
+
"""SELECT task_id, files, guard_has_blocking
|
|
81
|
+
FROM protocol_tasks
|
|
82
|
+
WHERE session_id = ? AND status = 'open'
|
|
83
|
+
ORDER BY opened_at DESC""",
|
|
84
|
+
(sid,),
|
|
85
|
+
).fetchall()
|
|
86
|
+
for row in rows:
|
|
87
|
+
try:
|
|
88
|
+
files = json.loads(row["files"] or "[]")
|
|
89
|
+
except Exception:
|
|
90
|
+
files = []
|
|
91
|
+
for item in files if isinstance(files, list) else []:
|
|
92
|
+
if _normalize_file_path(str(item)) == target:
|
|
93
|
+
return dict(row)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _find_open_debt(conn, *, session_id: str, task_id: str, debt_type: str, file_token: str) -> dict | None:
|
|
98
|
+
row = conn.execute(
|
|
99
|
+
"""SELECT *
|
|
100
|
+
FROM protocol_debt
|
|
101
|
+
WHERE status = 'open'
|
|
102
|
+
AND session_id = ?
|
|
103
|
+
AND task_id = ?
|
|
104
|
+
AND debt_type = ?
|
|
105
|
+
AND INSTR(evidence, ?) > 0
|
|
106
|
+
ORDER BY id DESC
|
|
107
|
+
LIMIT 1""",
|
|
108
|
+
(session_id, task_id, debt_type, file_token),
|
|
109
|
+
).fetchone()
|
|
110
|
+
return dict(row) if row else None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _find_task_guard_blocking_debt(conn, task_id: str) -> dict | None:
|
|
114
|
+
row = conn.execute(
|
|
115
|
+
"""SELECT *
|
|
116
|
+
FROM protocol_debt
|
|
117
|
+
WHERE status = 'open'
|
|
118
|
+
AND task_id = ?
|
|
119
|
+
AND debt_type = 'unacknowledged_guard_blocking'
|
|
120
|
+
ORDER BY id DESC
|
|
121
|
+
LIMIT 1""",
|
|
122
|
+
(task_id,),
|
|
123
|
+
).fetchone()
|
|
124
|
+
return dict(row) if row else None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _ensure_protocol_debt(
|
|
128
|
+
conn,
|
|
129
|
+
*,
|
|
130
|
+
session_id: str,
|
|
131
|
+
task_id: str,
|
|
132
|
+
debt_type: str,
|
|
133
|
+
severity: str,
|
|
134
|
+
evidence: str,
|
|
135
|
+
file_token: str,
|
|
136
|
+
) -> dict:
|
|
137
|
+
existing = _find_open_debt(
|
|
138
|
+
conn,
|
|
139
|
+
session_id=session_id,
|
|
140
|
+
task_id=task_id,
|
|
141
|
+
debt_type=debt_type,
|
|
142
|
+
file_token=file_token,
|
|
143
|
+
)
|
|
144
|
+
if existing:
|
|
145
|
+
return existing
|
|
146
|
+
return create_protocol_debt(
|
|
147
|
+
session_id,
|
|
148
|
+
debt_type,
|
|
149
|
+
severity=severity,
|
|
150
|
+
task_id=task_id,
|
|
151
|
+
evidence=evidence,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def process_tool_event(payload: dict) -> dict:
|
|
156
|
+
tool_name = str(payload.get("tool_name", "")).strip()
|
|
157
|
+
op = _operation_kind(tool_name)
|
|
158
|
+
if op == "other":
|
|
159
|
+
return {"ok": True, "skipped": True, "reason": "tool not monitored"}
|
|
160
|
+
|
|
161
|
+
tool_input = payload.get("tool_input")
|
|
162
|
+
files = _extract_touched_files(tool_input)
|
|
163
|
+
if not files:
|
|
164
|
+
return {"ok": True, "skipped": True, "reason": "no touched files found"}
|
|
165
|
+
|
|
166
|
+
conn = get_db()
|
|
167
|
+
sid = _resolve_nexo_sid(conn, str(payload.get("session_id", "")))
|
|
168
|
+
if not sid:
|
|
169
|
+
return {"ok": True, "skipped": True, "reason": "session not mapped to nexo"}
|
|
170
|
+
|
|
171
|
+
conditioned = _load_conditioned_learnings(conn, files)
|
|
172
|
+
warnings: list[dict] = []
|
|
173
|
+
violations: list[dict] = []
|
|
174
|
+
|
|
175
|
+
for filepath in files:
|
|
176
|
+
hits = conditioned.get(filepath) or []
|
|
177
|
+
if not hits:
|
|
178
|
+
continue
|
|
179
|
+
learning_ids = [int(row["id"]) for row in hits]
|
|
180
|
+
task = _find_open_task_for_file(conn, sid, filepath)
|
|
181
|
+
|
|
182
|
+
if op == "read":
|
|
183
|
+
if not task:
|
|
184
|
+
evidence = (
|
|
185
|
+
f"{tool_name} read conditioned file {filepath} linked to learning IDs {learning_ids} "
|
|
186
|
+
"without an open protocol task."
|
|
187
|
+
)
|
|
188
|
+
debt = _ensure_protocol_debt(
|
|
189
|
+
conn,
|
|
190
|
+
session_id=sid,
|
|
191
|
+
task_id="",
|
|
192
|
+
debt_type="conditioned_file_read_without_protocol",
|
|
193
|
+
severity="warn",
|
|
194
|
+
evidence=evidence,
|
|
195
|
+
file_token=filepath,
|
|
196
|
+
)
|
|
197
|
+
warnings.append(
|
|
198
|
+
{
|
|
199
|
+
"file": filepath,
|
|
200
|
+
"learning_ids": learning_ids,
|
|
201
|
+
"debt_id": debt.get("id"),
|
|
202
|
+
"debt_type": "conditioned_file_read_without_protocol",
|
|
203
|
+
"message": "Read conditioned file outside protocol task; review the file rules before any write/delete step.",
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
if not task:
|
|
209
|
+
evidence = (
|
|
210
|
+
f"{tool_name} touched conditioned file {filepath} linked to learning IDs {learning_ids} "
|
|
211
|
+
f"without an open protocol task."
|
|
212
|
+
)
|
|
213
|
+
debt = _ensure_protocol_debt(
|
|
214
|
+
conn,
|
|
215
|
+
session_id=sid,
|
|
216
|
+
task_id="",
|
|
217
|
+
debt_type="conditioned_file_touch_without_protocol",
|
|
218
|
+
severity="error",
|
|
219
|
+
evidence=evidence,
|
|
220
|
+
file_token=filepath,
|
|
221
|
+
)
|
|
222
|
+
violations.append(
|
|
223
|
+
{
|
|
224
|
+
"file": filepath,
|
|
225
|
+
"learning_ids": learning_ids,
|
|
226
|
+
"task_id": "",
|
|
227
|
+
"debt_id": debt.get("id"),
|
|
228
|
+
"debt_type": "conditioned_file_touch_without_protocol",
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
guard_debt = _find_task_guard_blocking_debt(conn, task["task_id"])
|
|
234
|
+
if guard_debt:
|
|
235
|
+
evidence = (
|
|
236
|
+
f"{tool_name} touched conditioned file {filepath} linked to learning IDs {learning_ids} "
|
|
237
|
+
f"before acknowledging blocking guard debt for task {task['task_id']}."
|
|
238
|
+
)
|
|
239
|
+
debt = _ensure_protocol_debt(
|
|
240
|
+
conn,
|
|
241
|
+
session_id=sid,
|
|
242
|
+
task_id=task["task_id"],
|
|
243
|
+
debt_type="conditioned_file_touch_without_guard_ack",
|
|
244
|
+
severity="error",
|
|
245
|
+
evidence=evidence,
|
|
246
|
+
file_token=filepath,
|
|
247
|
+
)
|
|
248
|
+
violations.append(
|
|
249
|
+
{
|
|
250
|
+
"file": filepath,
|
|
251
|
+
"learning_ids": learning_ids,
|
|
252
|
+
"task_id": task["task_id"],
|
|
253
|
+
"debt_id": debt.get("id"),
|
|
254
|
+
"debt_type": "conditioned_file_touch_without_guard_ack",
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
"ok": True,
|
|
260
|
+
"session_id": sid,
|
|
261
|
+
"tool_name": tool_name,
|
|
262
|
+
"operation": op,
|
|
263
|
+
"warnings": warnings,
|
|
264
|
+
"violations": violations,
|
|
265
|
+
"status": "violation" if violations else ("warn" if warnings else "clean"),
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def format_hook_message(result: dict) -> str:
|
|
270
|
+
if not result.get("violations") and not result.get("warnings"):
|
|
271
|
+
return ""
|
|
272
|
+
lines = ["NEXO DISCIPLINE:"]
|
|
273
|
+
for item in result.get("warnings", []):
|
|
274
|
+
if item.get("debt_id"):
|
|
275
|
+
lines.append(
|
|
276
|
+
f"- REVIEW FILE RULES: {item['file']} -> learnings {item['learning_ids']}. "
|
|
277
|
+
f"{item['message']} (debt={item['debt_type']}, debt_id={item['debt_id']})"
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
lines.append(
|
|
281
|
+
f"- REVIEW FILE RULES: {item['file']} -> learnings {item['learning_ids']}. "
|
|
282
|
+
f"{item['message']}"
|
|
283
|
+
)
|
|
284
|
+
for item in result.get("violations", []):
|
|
285
|
+
lines.append(
|
|
286
|
+
f"- DEBT RECORDED: {item['debt_type']} on {item['file']} "
|
|
287
|
+
f"(task={item['task_id'] or 'none'}, debt_id={item['debt_id']}, learnings={item['learning_ids']})"
|
|
288
|
+
)
|
|
289
|
+
return "\n".join(lines)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def main() -> int:
|
|
293
|
+
raw = sys.stdin.read()
|
|
294
|
+
if not raw.strip():
|
|
295
|
+
return 0
|
|
296
|
+
try:
|
|
297
|
+
payload = json.loads(raw)
|
|
298
|
+
except Exception:
|
|
299
|
+
return 0
|
|
300
|
+
result = process_tool_event(payload)
|
|
301
|
+
message = format_hook_message(result)
|
|
302
|
+
if message:
|
|
303
|
+
print(message)
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# NEXO PostToolUse hook — conditioned file discipline guardrail
|
|
3
|
+
|
|
4
|
+
INPUT=$(cat || true)
|
|
5
|
+
[ -z "$INPUT" ] && exit 0
|
|
6
|
+
|
|
7
|
+
NEXO_CODE="${NEXO_CODE:-${HOME}/.nexo}"
|
|
8
|
+
python3 "$NEXO_CODE/hook_guardrails.py" <<< "$INPUT" 2>/dev/null || true
|
|
9
|
+
|
|
10
|
+
exit 0
|
package/src/nexo_sdk.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Minimal Python SDK for the public NEXO mental model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class NEXOClient:
|
|
12
|
+
"""Tiny Python wrapper around `nexo call` for common public operations."""
|
|
13
|
+
|
|
14
|
+
nexo_bin: str = "nexo"
|
|
15
|
+
|
|
16
|
+
def call(self, tool: str, payload: dict | None = None) -> dict | list | str:
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
[
|
|
19
|
+
self.nexo_bin,
|
|
20
|
+
"call",
|
|
21
|
+
tool,
|
|
22
|
+
"--input",
|
|
23
|
+
json.dumps(payload or {}, ensure_ascii=False),
|
|
24
|
+
"--json-output",
|
|
25
|
+
],
|
|
26
|
+
capture_output=True,
|
|
27
|
+
text=True,
|
|
28
|
+
check=False,
|
|
29
|
+
)
|
|
30
|
+
if result.returncode != 0:
|
|
31
|
+
raise RuntimeError((result.stderr or result.stdout or f"{tool} failed").strip())
|
|
32
|
+
text = (result.stdout or "").strip()
|
|
33
|
+
if not text:
|
|
34
|
+
return {}
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(text)
|
|
37
|
+
except json.JSONDecodeError:
|
|
38
|
+
return {"result": text}
|
|
39
|
+
|
|
40
|
+
def remember(
|
|
41
|
+
self,
|
|
42
|
+
content: str,
|
|
43
|
+
*,
|
|
44
|
+
title: str = "",
|
|
45
|
+
domain: str = "",
|
|
46
|
+
source_type: str = "note",
|
|
47
|
+
tags: str = "",
|
|
48
|
+
bypass_gate: bool = True,
|
|
49
|
+
) -> dict | list | str:
|
|
50
|
+
return self.call(
|
|
51
|
+
"nexo_remember",
|
|
52
|
+
{
|
|
53
|
+
"content": content,
|
|
54
|
+
"title": title,
|
|
55
|
+
"domain": domain,
|
|
56
|
+
"source_type": source_type,
|
|
57
|
+
"tags": tags,
|
|
58
|
+
"bypass_gate": bypass_gate,
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def recall(self, query: str, *, days: int = 30) -> dict | list | str:
|
|
63
|
+
return self.call("nexo_memory_recall", {"query": query, "days": days})
|
|
64
|
+
|
|
65
|
+
def consolidate(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
max_insights: int = 12,
|
|
69
|
+
threshold: float = 0.9,
|
|
70
|
+
dry_run: bool = False,
|
|
71
|
+
) -> dict | list | str:
|
|
72
|
+
return self.call(
|
|
73
|
+
"nexo_consolidate",
|
|
74
|
+
{
|
|
75
|
+
"max_insights": max_insights,
|
|
76
|
+
"threshold": threshold,
|
|
77
|
+
"dry_run": dry_run,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def run_workflow(
|
|
82
|
+
self,
|
|
83
|
+
sid: str,
|
|
84
|
+
goal: str,
|
|
85
|
+
*,
|
|
86
|
+
steps: list[dict] | str,
|
|
87
|
+
goal_id: str = "",
|
|
88
|
+
shared_state: dict | str | None = None,
|
|
89
|
+
owner: str = "",
|
|
90
|
+
idempotency_key: str = "",
|
|
91
|
+
) -> dict | list | str:
|
|
92
|
+
return self.call(
|
|
93
|
+
"nexo_run_workflow",
|
|
94
|
+
{
|
|
95
|
+
"sid": sid,
|
|
96
|
+
"goal": goal,
|
|
97
|
+
"steps": steps if isinstance(steps, str) else json.dumps(steps, ensure_ascii=False),
|
|
98
|
+
"goal_id": goal_id,
|
|
99
|
+
"shared_state": shared_state if isinstance(shared_state, str) else json.dumps(shared_state or {}, ensure_ascii=False),
|
|
100
|
+
"owner": owner,
|
|
101
|
+
"idempotency_key": idempotency_key,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
@@ -533,6 +533,23 @@ def handle_cognitive_trigger_check(text: str, use_semantic: bool = False) -> str
|
|
|
533
533
|
return "\n".join(lines)
|
|
534
534
|
|
|
535
535
|
|
|
536
|
+
def handle_cognitive_trigger_preview(text: str, use_semantic: bool = False) -> str:
|
|
537
|
+
"""Preview prospective trigger matches without firing them."""
|
|
538
|
+
matches = cognitive.preview_triggers(text, use_semantic=use_semantic)
|
|
539
|
+
if not matches:
|
|
540
|
+
return "No anticipatory warnings."
|
|
541
|
+
|
|
542
|
+
lines = [f"ANTICIPATORY WARNINGS: {len(matches)}", ""]
|
|
543
|
+
for match in matches:
|
|
544
|
+
lines.append(f" #{match['id']} [{match['match_type']}] pattern='{match['pattern']}'")
|
|
545
|
+
lines.append(f" ACTION: {match['action']}")
|
|
546
|
+
if match.get("context"):
|
|
547
|
+
lines.append(f" context: {match['context']}")
|
|
548
|
+
lines.append("")
|
|
549
|
+
|
|
550
|
+
return "\n".join(lines)
|
|
551
|
+
|
|
552
|
+
|
|
536
553
|
def handle_cognitive_trigger_delete(trigger_id: int) -> str:
|
|
537
554
|
"""Delete a prospective memory trigger.
|
|
538
555
|
|
|
@@ -570,6 +587,7 @@ TOOLS = [
|
|
|
570
587
|
(handle_cognitive_quarantine_process, "nexo_cognitive_quarantine_process", "Run quarantine promotion cycle — evaluate pending items against policy."),
|
|
571
588
|
(handle_cognitive_trigger_create, "nexo_cognitive_trigger_create", "Create a prospective memory trigger — 'when X is mentioned, remind about Y'."),
|
|
572
589
|
(handle_cognitive_trigger_list, "nexo_cognitive_trigger_list", "List prospective triggers by status (armed/fired/all)."),
|
|
590
|
+
(handle_cognitive_trigger_preview, "nexo_cognitive_trigger_preview", "Preview anticipatory trigger matches without firing them."),
|
|
573
591
|
(handle_cognitive_trigger_check, "nexo_cognitive_trigger_check", "Check text against armed triggers. Returns fired triggers with actions."),
|
|
574
592
|
(handle_cognitive_trigger_delete, "nexo_cognitive_trigger_delete", "Delete a prospective trigger by ID."),
|
|
575
593
|
(handle_cognitive_trigger_rearm, "nexo_cognitive_trigger_rearm", "Re-arm a fired trigger so it can fire again."),
|
package/src/plugins/cortex.py
CHANGED
|
@@ -15,6 +15,7 @@ v0.1: Single MCP tool + middleware validation.
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
import secrets
|
|
18
19
|
import time
|
|
19
20
|
|
|
20
21
|
|
|
@@ -135,6 +136,51 @@ def _tools_for_mode(mode: str) -> list[str]:
|
|
|
135
136
|
return ["all"]
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
def _parse_json_list(value) -> list:
|
|
140
|
+
try:
|
|
141
|
+
parsed = json.loads(value) if isinstance(value, str) else value
|
|
142
|
+
return parsed if isinstance(parsed, list) else []
|
|
143
|
+
except (json.JSONDecodeError, TypeError):
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def evaluate_cortex_state(state: dict) -> dict:
|
|
148
|
+
"""Return structured Cortex evaluation for internal callers."""
|
|
149
|
+
result = _validate_state(state)
|
|
150
|
+
result["check_id"] = f"CTX-{int(time.time())}-{secrets.randbelow(100000)}"
|
|
151
|
+
result["expires_at_epoch"] = int(time.time()) + 1200
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _log_cortex_activation(goal: str, task_type: str, result: dict):
|
|
156
|
+
try:
|
|
157
|
+
conn = _get_db()
|
|
158
|
+
conn.execute(
|
|
159
|
+
"""CREATE TABLE IF NOT EXISTS cortex_log (
|
|
160
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
161
|
+
goal TEXT,
|
|
162
|
+
task_type TEXT,
|
|
163
|
+
mode TEXT,
|
|
164
|
+
warnings TEXT,
|
|
165
|
+
trust_score INTEGER,
|
|
166
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
167
|
+
)"""
|
|
168
|
+
)
|
|
169
|
+
conn.execute(
|
|
170
|
+
"INSERT INTO cortex_log (goal, task_type, mode, warnings, trust_score) VALUES (?, ?, ?, ?, ?)",
|
|
171
|
+
(
|
|
172
|
+
goal[:200],
|
|
173
|
+
task_type,
|
|
174
|
+
result["mode"],
|
|
175
|
+
json.dumps(result["warnings"]),
|
|
176
|
+
result["trust_score"],
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
conn.commit()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
138
184
|
def handle_cortex_check(
|
|
139
185
|
goal: str,
|
|
140
186
|
task_type: str = "answer",
|
|
@@ -172,31 +218,25 @@ def handle_cortex_check(
|
|
|
172
218
|
Returns:
|
|
173
219
|
Mode (ask/propose/act), available tools, warnings, and relevant Core Rules
|
|
174
220
|
"""
|
|
175
|
-
# Parse JSON arrays safely
|
|
176
|
-
def _parse(s):
|
|
177
|
-
try:
|
|
178
|
-
v = json.loads(s) if isinstance(s, str) else s
|
|
179
|
-
return v if isinstance(v, list) else []
|
|
180
|
-
except (json.JSONDecodeError, TypeError):
|
|
181
|
-
return []
|
|
182
|
-
|
|
183
221
|
state = {
|
|
184
222
|
"goal": goal.strip() if goal else "",
|
|
185
223
|
"task_type": task_type if task_type in ("answer", "analyze", "edit", "execute", "delegate") else "answer",
|
|
186
|
-
"plan":
|
|
187
|
-
"known_facts":
|
|
188
|
-
"unknowns":
|
|
189
|
-
"constraints":
|
|
190
|
-
"evidence_refs":
|
|
224
|
+
"plan": _parse_json_list(plan),
|
|
225
|
+
"known_facts": _parse_json_list(known_facts),
|
|
226
|
+
"unknowns": _parse_json_list(unknowns),
|
|
227
|
+
"constraints": _parse_json_list(constraints),
|
|
228
|
+
"evidence_refs": _parse_json_list(evidence_refs),
|
|
191
229
|
"verification_step": verification_step.strip() if verification_step else "",
|
|
192
230
|
}
|
|
193
231
|
|
|
194
|
-
result =
|
|
232
|
+
result = evaluate_cortex_state(state)
|
|
195
233
|
|
|
196
234
|
# Format response
|
|
197
235
|
lines = [
|
|
198
236
|
f"CORTEX CHECK — mode: {result['mode'].upper()}",
|
|
199
237
|
f"Trust: {result['trust_score']}/100",
|
|
238
|
+
f"Check ID: {result['check_id']}",
|
|
239
|
+
f"Valid until epoch: {result['expires_at_epoch']}",
|
|
200
240
|
]
|
|
201
241
|
|
|
202
242
|
if result["mode"] == "act":
|
|
@@ -223,27 +263,7 @@ def handle_cortex_check(
|
|
|
223
263
|
lines.append("")
|
|
224
264
|
lines.append(f"Tools available: {', '.join(result['tools_available'])}")
|
|
225
265
|
|
|
226
|
-
|
|
227
|
-
try:
|
|
228
|
-
conn = _get_db()
|
|
229
|
-
conn.execute(
|
|
230
|
-
"""CREATE TABLE IF NOT EXISTS cortex_log (
|
|
231
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
232
|
-
goal TEXT,
|
|
233
|
-
task_type TEXT,
|
|
234
|
-
mode TEXT,
|
|
235
|
-
warnings TEXT,
|
|
236
|
-
trust_score INTEGER,
|
|
237
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
238
|
-
)"""
|
|
239
|
-
)
|
|
240
|
-
conn.execute(
|
|
241
|
-
"INSERT INTO cortex_log (goal, task_type, mode, warnings, trust_score) VALUES (?, ?, ?, ?, ?)",
|
|
242
|
-
(goal[:200], task_type, result["mode"], json.dumps(result["warnings"]), result["trust_score"])
|
|
243
|
-
)
|
|
244
|
-
conn.commit()
|
|
245
|
-
except Exception:
|
|
246
|
-
pass
|
|
266
|
+
_log_cortex_activation(goal, task_type, result)
|
|
247
267
|
|
|
248
268
|
return "\n".join(lines)
|
|
249
269
|
|