nexo-brain 5.8.2 → 5.9.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 +5 -1
- package/package.json +1 -1
- package/src/agent_runner.py +270 -33
- package/src/cli.py +122 -0
- package/src/client_preferences.py +5 -0
- package/src/db/_schema.py +43 -0
- package/src/desktop_bridge.py +28 -0
- package/src/resonance_map.py +295 -0
- package/src/scripts/check-context.py +1 -1
- package/src/scripts/deep-sleep/extract.py +2 -2
- package/src/scripts/deep-sleep/synthesize.py +1 -1
- package/src/scripts/nexo-agent-run.py +1 -0
- package/src/scripts/nexo-catchup.py +1 -1
- package/src/scripts/nexo-daily-self-audit.py +1 -1
- package/src/scripts/nexo-evolution-run.py +2 -2
- package/src/scripts/nexo-immune.py +1 -1
- package/src/scripts/nexo-learning-validator.py +1 -1
- package/src/scripts/nexo-postmortem-consolidator.py +1 -1
- package/src/scripts/nexo-sleep.py +1 -1
- package/src/scripts/nexo-synthesis.py +1 -1
- package/src/server.py +102 -0
- package/src/tools_automation_sessions.py +159 -0
- package/src/tools_drive.py +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""MCP tools for automation session logging.
|
|
2
|
+
|
|
3
|
+
Gives NEXO Desktop (which launches ``claude`` directly, not via
|
|
4
|
+
``agent_runner.run_automation_prompt``) a way to record its interactive
|
|
5
|
+
sessions in the same ``automation_runs`` table as every other backend call.
|
|
6
|
+
|
|
7
|
+
Two tools:
|
|
8
|
+
|
|
9
|
+
nexo_session_log_create → INSERT a row with ended_at=NULL, return id
|
|
10
|
+
nexo_session_log_close → UPDATE the row with exit + duration + tokens
|
|
11
|
+
|
|
12
|
+
The tools are intentionally thin wrappers over the helpers in
|
|
13
|
+
``agent_runner``. They exist as MCP tools so clients that don't embed the
|
|
14
|
+
Python runtime (Desktop's TypeScript/Electron process, any future
|
|
15
|
+
third-party agent) can still participate in the unified log.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle_session_log_create(payload: dict | None = None, **kwargs) -> dict:
|
|
23
|
+
"""Open an automation session row.
|
|
24
|
+
|
|
25
|
+
Expected arguments (all optional except ``caller`` and ``backend``):
|
|
26
|
+
caller — e.g. "desktop_new_session" (registered in
|
|
27
|
+
resonance_map.py).
|
|
28
|
+
backend — "claude_code" or "codex".
|
|
29
|
+
session_type — "interactive_chat" | "interactive_desktop"
|
|
30
|
+
(default: "interactive_desktop").
|
|
31
|
+
model — concrete model string, if the client already
|
|
32
|
+
resolved it.
|
|
33
|
+
reasoning_effort — concrete effort string.
|
|
34
|
+
resonance_tier — tier label for traceability.
|
|
35
|
+
cwd — working directory the session is anchored to.
|
|
36
|
+
pid — the child PID if the client already has it.
|
|
37
|
+
context_excerpt — optional short preview of the first prompt
|
|
38
|
+
(truncated to 2048 chars for prompt_chars).
|
|
39
|
+
"""
|
|
40
|
+
args = payload or kwargs or {}
|
|
41
|
+
caller = str(args.get("caller") or "").strip()
|
|
42
|
+
backend = str(args.get("backend") or "").strip()
|
|
43
|
+
if not caller or not backend:
|
|
44
|
+
return {
|
|
45
|
+
"ok": False,
|
|
46
|
+
"error": "caller and backend are required",
|
|
47
|
+
"session_id": None,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
session_type = str(args.get("session_type") or "interactive_desktop").strip()
|
|
51
|
+
model = str(args.get("model") or "").strip()
|
|
52
|
+
reasoning_effort = str(args.get("reasoning_effort") or "").strip()
|
|
53
|
+
resonance_tier = str(args.get("resonance_tier") or "").strip()
|
|
54
|
+
cwd = Path(str(args.get("cwd") or ".")).expanduser()
|
|
55
|
+
pid_raw = args.get("pid")
|
|
56
|
+
try:
|
|
57
|
+
pid = int(pid_raw) if pid_raw is not None and pid_raw != "" else None
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
pid = None
|
|
60
|
+
|
|
61
|
+
context_excerpt = str(args.get("context_excerpt") or "")[:2048]
|
|
62
|
+
|
|
63
|
+
# Resolve resonance_tier if client did not pre-compute it.
|
|
64
|
+
if not resonance_tier and caller:
|
|
65
|
+
try:
|
|
66
|
+
from resonance_map import (
|
|
67
|
+
resolve_tier_for_caller,
|
|
68
|
+
UnregisteredCallerError,
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
from client_preferences import load_client_preferences
|
|
72
|
+
prefs = load_client_preferences()
|
|
73
|
+
except Exception:
|
|
74
|
+
prefs = {}
|
|
75
|
+
user_default = ""
|
|
76
|
+
if isinstance(prefs, dict):
|
|
77
|
+
user_default = str(prefs.get("default_resonance") or "").strip()
|
|
78
|
+
try:
|
|
79
|
+
resonance_tier = resolve_tier_for_caller(
|
|
80
|
+
caller, user_default=user_default or None
|
|
81
|
+
)
|
|
82
|
+
except UnregisteredCallerError:
|
|
83
|
+
resonance_tier = ""
|
|
84
|
+
except Exception:
|
|
85
|
+
resonance_tier = ""
|
|
86
|
+
|
|
87
|
+
from agent_runner import _record_automation_start
|
|
88
|
+
|
|
89
|
+
row_id, err = _record_automation_start(
|
|
90
|
+
caller=caller,
|
|
91
|
+
backend=backend,
|
|
92
|
+
session_type=session_type,
|
|
93
|
+
task_profile="",
|
|
94
|
+
model=model,
|
|
95
|
+
reasoning_effort=reasoning_effort,
|
|
96
|
+
resonance_tier=resonance_tier,
|
|
97
|
+
cwd=cwd,
|
|
98
|
+
output_format="interactive",
|
|
99
|
+
prompt=context_excerpt,
|
|
100
|
+
pid=pid,
|
|
101
|
+
)
|
|
102
|
+
if row_id is None:
|
|
103
|
+
return {"ok": False, "error": err or "session log insert failed", "session_id": None}
|
|
104
|
+
return {
|
|
105
|
+
"ok": True,
|
|
106
|
+
"session_id": int(row_id),
|
|
107
|
+
"resonance_tier": resonance_tier,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def handle_session_log_close(payload: dict | None = None, **kwargs) -> dict:
|
|
112
|
+
"""Close an automation session row opened by nexo_session_log_create.
|
|
113
|
+
|
|
114
|
+
Expected arguments:
|
|
115
|
+
session_id — int returned by the create call.
|
|
116
|
+
returncode — exit code (default 0).
|
|
117
|
+
duration_ms — total wall-clock duration in ms.
|
|
118
|
+
input_tokens, cached_input_tokens, output_tokens — counters from
|
|
119
|
+
the client's own telemetry, all optional.
|
|
120
|
+
total_cost_usd — float, optional.
|
|
121
|
+
telemetry_source — short label ("desktop_stream", "codex_json", ...).
|
|
122
|
+
error — short error string if the session failed.
|
|
123
|
+
"""
|
|
124
|
+
args = payload or kwargs or {}
|
|
125
|
+
sid_raw = args.get("session_id")
|
|
126
|
+
try:
|
|
127
|
+
session_id = int(sid_raw) if sid_raw is not None else None
|
|
128
|
+
except (TypeError, ValueError):
|
|
129
|
+
session_id = None
|
|
130
|
+
if session_id is None:
|
|
131
|
+
return {"ok": False, "error": "session_id is required"}
|
|
132
|
+
|
|
133
|
+
returncode = int(args.get("returncode") or 0)
|
|
134
|
+
duration_ms = int(args.get("duration_ms") or 0)
|
|
135
|
+
telemetry = {
|
|
136
|
+
"usage": {
|
|
137
|
+
"input_tokens": int(args.get("input_tokens") or 0),
|
|
138
|
+
"cached_input_tokens": int(args.get("cached_input_tokens") or 0),
|
|
139
|
+
"output_tokens": int(args.get("output_tokens") or 0),
|
|
140
|
+
},
|
|
141
|
+
"total_cost_usd": args.get("total_cost_usd"),
|
|
142
|
+
"telemetry_source": str(args.get("telemetry_source") or "").strip(),
|
|
143
|
+
"cost_source": str(args.get("cost_source") or "").strip(),
|
|
144
|
+
"warnings": [],
|
|
145
|
+
"raw": {},
|
|
146
|
+
}
|
|
147
|
+
err_message = str(args.get("error") or "").strip()
|
|
148
|
+
if err_message:
|
|
149
|
+
telemetry["warnings"].append(err_message)
|
|
150
|
+
|
|
151
|
+
from agent_runner import _record_automation_end
|
|
152
|
+
|
|
153
|
+
ok, err = _record_automation_end(
|
|
154
|
+
row_id=session_id,
|
|
155
|
+
returncode=returncode,
|
|
156
|
+
duration_ms=duration_ms,
|
|
157
|
+
telemetry=telemetry,
|
|
158
|
+
)
|
|
159
|
+
return {"ok": bool(ok), "error": err or ""}
|
package/src/tools_drive.py
CHANGED