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.
@@ -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 ""}
@@ -283,6 +283,7 @@ def _llm_classify_signal(text: str) -> dict:
283
283
  try:
284
284
  result = run_automation_prompt(
285
285
  prompt,
286
+ caller="tools/drive_search",
286
287
  task_profile="fast",
287
288
  timeout=_LLM_TIMEOUT_SECONDS,
288
289
  output_format="text",