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,295 @@
|
|
|
1
|
+
"""Central resonance map — single source of truth for (backend, model, effort)
|
|
2
|
+
decisions across every automation caller.
|
|
3
|
+
|
|
4
|
+
Motivation
|
|
5
|
+
----------
|
|
6
|
+
Before v5.9.0 each caller that wanted to invoke Claude or Codex had to either
|
|
7
|
+
pass explicit model + reasoning_effort arguments or rely on the global defaults
|
|
8
|
+
in ``model_defaults.json``. Global defaults meant every background cron shared
|
|
9
|
+
the same max-effort configuration as the interactive ``nexo chat``, so batch
|
|
10
|
+
jobs (daily synthesis, postmortem consolidation, gbp posts) burned maximum
|
|
11
|
+
reasoning effort on tasks that didn't need it, while downgrading any single
|
|
12
|
+
default changed behaviour everywhere at once.
|
|
13
|
+
|
|
14
|
+
This module introduces four **resonance tiers** (``MAXIMO`` / ``ALTO`` /
|
|
15
|
+
``MEDIO`` / ``BAJO``) and maps each tier to a concrete ``(model, effort)`` pair
|
|
16
|
+
per backend. Every caller is labelled in one of two ways:
|
|
17
|
+
|
|
18
|
+
- **User-facing callers** (``nexo chat``, Desktop new session, interactive
|
|
19
|
+
``nexo update``) use the user's configured default resonance. When the
|
|
20
|
+
user changes the default via ``nexo preferences --resonance`` or through
|
|
21
|
+
the Desktop preferences pane, those three entry points adjust.
|
|
22
|
+
|
|
23
|
+
- **System-owned callers** (every cron, every background script, every
|
|
24
|
+
MCP-tool-triggered automation) use a fixed tier we pick per caller based
|
|
25
|
+
on what the task needs. A quarterly evolution pass that synthesizes
|
|
26
|
+
ten thousand lines into a new self-improvement plan is ``MAXIMO``. A
|
|
27
|
+
daily GBP post that needs to produce 200 characters of marketing copy
|
|
28
|
+
is ``BAJO``. That decision stays in this file and NEVER reads the user
|
|
29
|
+
default.
|
|
30
|
+
|
|
31
|
+
If a backend does not offer all four effort settings (e.g. a hypothetical
|
|
32
|
+
model with only ``max`` and ``low``), we collapse adjacent tiers — ``MAXIMO``
|
|
33
|
+
and ``ALTO`` both map to the backend's highest available effort, ``MEDIO``
|
|
34
|
+
and ``BAJO`` to the lowest. If a backend has no effort knob at all, the tier
|
|
35
|
+
still resolves to the same model with an empty effort string; the resonance
|
|
36
|
+
label is then informational only.
|
|
37
|
+
|
|
38
|
+
Contract
|
|
39
|
+
--------
|
|
40
|
+
Every call into ``run_automation_prompt`` and ``run_automation_interactive``
|
|
41
|
+
MUST pass a ``caller=`` string that is registered here. Callers not in the
|
|
42
|
+
registry raise ``UnregisteredCallerError`` — there is no silent default. This
|
|
43
|
+
forces the resonance decision to be explicit and auditable, and prevents
|
|
44
|
+
future scripts from silently inheriting the wrong tier.
|
|
45
|
+
"""
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from typing import Tuple
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Tier → (claude_model, claude_effort, codex_model, codex_effort)
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Keep this table in ONE place. When we promote a new Claude or Codex model,
|
|
55
|
+
# update only this dict — every caller rebalances automatically.
|
|
56
|
+
#
|
|
57
|
+
# If a future backend offers fewer tiers (e.g. only max + low), collapse
|
|
58
|
+
# adjacent tiers onto the closest available effort. MAXIMO + ALTO → highest,
|
|
59
|
+
# MEDIO + BAJO → lowest. If a backend has no effort setting at all, leave
|
|
60
|
+
# the effort string empty.
|
|
61
|
+
|
|
62
|
+
TIERS = ("maximo", "alto", "medio", "bajo")
|
|
63
|
+
|
|
64
|
+
_RESONANCE_TABLE: dict[str, dict[str, tuple[str, str]]] = {
|
|
65
|
+
"maximo": {
|
|
66
|
+
"claude_code": ("claude-opus-4-7[1m]", "max"),
|
|
67
|
+
"codex": ("gpt-5.4", "xhigh"),
|
|
68
|
+
},
|
|
69
|
+
"alto": {
|
|
70
|
+
"claude_code": ("claude-opus-4-7[1m]", "xhigh"),
|
|
71
|
+
"codex": ("gpt-5.4", "high"),
|
|
72
|
+
},
|
|
73
|
+
"medio": {
|
|
74
|
+
"claude_code": ("claude-opus-4-7[1m]", "high"),
|
|
75
|
+
"codex": ("gpt-5.4", "medium"),
|
|
76
|
+
},
|
|
77
|
+
"bajo": {
|
|
78
|
+
"claude_code": ("claude-opus-4-7[1m]", "medium"),
|
|
79
|
+
"codex": ("gpt-5.4", "low"),
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
DEFAULT_RESONANCE = "alto"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Caller registry
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Every script that calls the automation backend is registered here. Two
|
|
90
|
+
# categories, in two separate dicts so a reviewer can see at a glance which
|
|
91
|
+
# callers follow the user's preference and which are locked by us.
|
|
92
|
+
#
|
|
93
|
+
# USE_USER_DEFAULT → the caller reads the user's configured resonance at
|
|
94
|
+
# runtime. Only three callers should ever be in this list:
|
|
95
|
+
# the two interactive entry points (terminal chat, Desktop
|
|
96
|
+
# new conversation) and the interactive nexo update flow.
|
|
97
|
+
#
|
|
98
|
+
# SYSTEM_OWNED → the caller runs at whatever tier we deem appropriate
|
|
99
|
+
# for its workload, ignoring the user's default. Tier is
|
|
100
|
+
# picked for quality of output, not cost: batch jobs that
|
|
101
|
+
# synthesize across a lot of data lean ALTO/MAXIMO, jobs
|
|
102
|
+
# that apply a fixed transform or produce short copy lean
|
|
103
|
+
# MEDIO/BAJO.
|
|
104
|
+
|
|
105
|
+
USE_USER_DEFAULT_SENTINEL = "__USE_USER_DEFAULT__"
|
|
106
|
+
|
|
107
|
+
USER_FACING_CALLERS: dict[str, str] = {
|
|
108
|
+
"nexo_chat": USE_USER_DEFAULT_SENTINEL,
|
|
109
|
+
"desktop_new_session": USE_USER_DEFAULT_SENTINEL,
|
|
110
|
+
"nexo_update_interactive": USE_USER_DEFAULT_SENTINEL,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# System-owned callers. Grouped thematically for readability.
|
|
114
|
+
SYSTEM_OWNED_CALLERS: dict[str, str] = {
|
|
115
|
+
# ---- Evolution and introspection: highest-quality reasoning needed ----
|
|
116
|
+
"evolution/run": "maximo",
|
|
117
|
+
"reflection": "maximo",
|
|
118
|
+
|
|
119
|
+
# ---- Deep sleep: extraction and synthesis benefit from quality --------
|
|
120
|
+
"deep-sleep/extract": "alto",
|
|
121
|
+
"deep-sleep/synthesize": "maximo",
|
|
122
|
+
"deep-sleep/apply_findings": "alto",
|
|
123
|
+
"sleep/nightly": "alto",
|
|
124
|
+
"synthesis/daily": "alto",
|
|
125
|
+
|
|
126
|
+
# ---- User-facing outputs where quality is visible ---------------------
|
|
127
|
+
"catchup/morning": "alto",
|
|
128
|
+
"daily_self_audit": "alto",
|
|
129
|
+
"postmortem_consolidator": "alto",
|
|
130
|
+
"proactive_dashboard": "alto",
|
|
131
|
+
"followup_runner": "alto",
|
|
132
|
+
|
|
133
|
+
# ---- Defensive / consistency tasks ------------------------------------
|
|
134
|
+
"immune/scan": "medio",
|
|
135
|
+
"learning_validator": "medio",
|
|
136
|
+
"outcome_checker": "medio",
|
|
137
|
+
"check_context": "medio",
|
|
138
|
+
|
|
139
|
+
# ---- Agent orchestration ----------------------------------------------
|
|
140
|
+
"agent_run/generic": "alto",
|
|
141
|
+
|
|
142
|
+
# ---- Tooling helpers (short, structured outputs) ----------------------
|
|
143
|
+
"tools/drive_search": "medio",
|
|
144
|
+
|
|
145
|
+
# ---- Marketing automation ---------------------------------------------
|
|
146
|
+
# These produce short copy; we could run them at BAJO for speed, but the
|
|
147
|
+
# output is user-visible on a public surface, so we lean MEDIO for safety
|
|
148
|
+
# against embarrassing outputs.
|
|
149
|
+
"gbp/daily_post": "medio",
|
|
150
|
+
"gbp/post_wazion": "medio",
|
|
151
|
+
"gbp/post_psicologa": "medio",
|
|
152
|
+
"gbp/monthly_audit": "medio",
|
|
153
|
+
"gbp/reviews_watch": "medio",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
ALL_REGISTERED_CALLERS: frozenset[str] = frozenset(
|
|
157
|
+
list(USER_FACING_CALLERS.keys()) + list(SYSTEM_OWNED_CALLERS.keys())
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class UnregisteredCallerError(ValueError):
|
|
162
|
+
"""Raised when a caller string is not in the resonance registry.
|
|
163
|
+
|
|
164
|
+
Every caller that dispatches an automation subprocess MUST register here.
|
|
165
|
+
We do not fall back to a default tier silently — that would re-introduce
|
|
166
|
+
the pre-v5.9.0 problem where the wrong script could inherit the wrong
|
|
167
|
+
reasoning budget without anyone noticing. The fix for this error is:
|
|
168
|
+
add an entry to SYSTEM_OWNED_CALLERS (or USER_FACING_CALLERS if it is a
|
|
169
|
+
genuine interactive entry point) and pick the tier deliberately.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Resolution
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def _load_user_default_resonance() -> str:
|
|
178
|
+
"""Resolve the user's ``default_resonance`` preference.
|
|
179
|
+
|
|
180
|
+
Reads ``calibration.json`` first (``preferences.default_resonance``, the
|
|
181
|
+
location NEXO Desktop's preferences UI writes to) and falls back to
|
|
182
|
+
``schedule.json`` (``default_resonance``, the location the CLI used to
|
|
183
|
+
write to in v5.9.0). Returns an empty string if neither source has a
|
|
184
|
+
valid tier — callers should treat empty as "no preference".
|
|
185
|
+
"""
|
|
186
|
+
import json as _json
|
|
187
|
+
import os as _os
|
|
188
|
+
from pathlib import Path as _Path
|
|
189
|
+
|
|
190
|
+
home = _Path(_os.environ.get("NEXO_HOME", str(_Path.home() / ".nexo")))
|
|
191
|
+
|
|
192
|
+
# calibration.json (Desktop UI writes here)
|
|
193
|
+
cal_path = home / "brain" / "calibration.json"
|
|
194
|
+
try:
|
|
195
|
+
if cal_path.exists():
|
|
196
|
+
cal = _json.loads(cal_path.read_text())
|
|
197
|
+
prefs = cal.get("preferences") if isinstance(cal, dict) else None
|
|
198
|
+
if isinstance(prefs, dict):
|
|
199
|
+
tier = str(prefs.get("default_resonance") or "").strip().lower()
|
|
200
|
+
if tier in TIERS:
|
|
201
|
+
return tier
|
|
202
|
+
except (OSError, _json.JSONDecodeError):
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# schedule.json (CLI legacy)
|
|
206
|
+
sched_path = home / "config" / "schedule.json"
|
|
207
|
+
try:
|
|
208
|
+
if sched_path.exists():
|
|
209
|
+
sched = _json.loads(sched_path.read_text())
|
|
210
|
+
tier = str((sched or {}).get("default_resonance") or "").strip().lower()
|
|
211
|
+
if tier in TIERS:
|
|
212
|
+
return tier
|
|
213
|
+
except (OSError, _json.JSONDecodeError):
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return ""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def resolve_tier_for_caller(caller: str, user_default: str | None = None) -> str:
|
|
220
|
+
"""Return the resonance tier that should apply to ``caller``.
|
|
221
|
+
|
|
222
|
+
- User-facing callers resolve to ``user_default`` (or ``DEFAULT_RESONANCE``
|
|
223
|
+
if the user has no preference recorded).
|
|
224
|
+
- System-owned callers resolve to their fixed tier.
|
|
225
|
+
- Unknown callers raise ``UnregisteredCallerError``.
|
|
226
|
+
|
|
227
|
+
When ``user_default`` is not passed, the function looks it up from the
|
|
228
|
+
calibration.json preferences first and schedule.json second.
|
|
229
|
+
"""
|
|
230
|
+
if not caller:
|
|
231
|
+
raise UnregisteredCallerError(
|
|
232
|
+
"caller= is required. Every automation subprocess must be registered "
|
|
233
|
+
"in src/resonance_map.py so its reasoning budget is deliberate."
|
|
234
|
+
)
|
|
235
|
+
if caller in USER_FACING_CALLERS:
|
|
236
|
+
resolved_default = user_default
|
|
237
|
+
if resolved_default is None:
|
|
238
|
+
resolved_default = _load_user_default_resonance()
|
|
239
|
+
tier = (resolved_default or DEFAULT_RESONANCE).strip().lower()
|
|
240
|
+
if tier not in TIERS:
|
|
241
|
+
tier = DEFAULT_RESONANCE
|
|
242
|
+
return tier
|
|
243
|
+
if caller in SYSTEM_OWNED_CALLERS:
|
|
244
|
+
return SYSTEM_OWNED_CALLERS[caller]
|
|
245
|
+
raise UnregisteredCallerError(
|
|
246
|
+
f"caller {caller!r} is not registered in resonance_map.py. "
|
|
247
|
+
"Add it to SYSTEM_OWNED_CALLERS (or USER_FACING_CALLERS if it is an "
|
|
248
|
+
"interactive entry point) with a deliberate tier."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def resolve_model_and_effort(
|
|
253
|
+
caller: str,
|
|
254
|
+
backend: str,
|
|
255
|
+
user_default: str | None = None,
|
|
256
|
+
) -> Tuple[str, str]:
|
|
257
|
+
"""Return ``(model, reasoning_effort)`` for ``caller`` on ``backend``.
|
|
258
|
+
|
|
259
|
+
The ``backend`` key must match the entries in ``_RESONANCE_TABLE`` tier
|
|
260
|
+
dicts (``claude_code`` or ``codex``). Unknown backends fall back to an
|
|
261
|
+
empty pair; the caller is expected to handle that by raising or by
|
|
262
|
+
passing its own explicit model/effort arguments.
|
|
263
|
+
"""
|
|
264
|
+
tier = resolve_tier_for_caller(caller, user_default=user_default)
|
|
265
|
+
backend_entry = _RESONANCE_TABLE.get(tier, {}).get(backend)
|
|
266
|
+
if backend_entry is None:
|
|
267
|
+
return "", ""
|
|
268
|
+
return backend_entry
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def register_system_caller(caller: str, tier: str) -> None:
|
|
272
|
+
"""Test/debug helper: register a caller at runtime.
|
|
273
|
+
|
|
274
|
+
Production code must add callers statically to ``SYSTEM_OWNED_CALLERS``
|
|
275
|
+
at module level so the registry is reviewable. This helper exists so
|
|
276
|
+
unit tests can exercise ``resolve_*`` against synthetic caller names
|
|
277
|
+
without mutating the shipped table.
|
|
278
|
+
"""
|
|
279
|
+
if tier not in TIERS:
|
|
280
|
+
raise ValueError(f"tier {tier!r} not in {TIERS}")
|
|
281
|
+
SYSTEM_OWNED_CALLERS[caller] = tier
|
|
282
|
+
# Rebuild the frozen view so the guard below sees the new caller.
|
|
283
|
+
global ALL_REGISTERED_CALLERS
|
|
284
|
+
ALL_REGISTERED_CALLERS = frozenset(
|
|
285
|
+
list(USER_FACING_CALLERS.keys()) + list(SYSTEM_OWNED_CALLERS.keys())
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def unregister_system_caller(caller: str) -> None:
|
|
290
|
+
"""Mirror helper for tests that need to remove what they registered."""
|
|
291
|
+
SYSTEM_OWNED_CALLERS.pop(caller, None)
|
|
292
|
+
global ALL_REGISTERED_CALLERS
|
|
293
|
+
ALL_REGISTERED_CALLERS = frozenset(
|
|
294
|
+
list(USER_FACING_CALLERS.keys()) + list(SYSTEM_OWNED_CALLERS.keys())
|
|
295
|
+
)
|
|
@@ -193,7 +193,7 @@ def analyze_session(
|
|
|
193
193
|
|
|
194
194
|
result = run_automation_prompt(
|
|
195
195
|
prompt,
|
|
196
|
-
|
|
196
|
+
caller="deep-sleep/extract",
|
|
197
197
|
timeout=CLAUDE_TIMEOUT,
|
|
198
198
|
output_format="text",
|
|
199
199
|
append_system_prompt=JSON_SYSTEM_PROMPT,
|
|
@@ -224,7 +224,7 @@ def analyze_session(
|
|
|
224
224
|
)
|
|
225
225
|
convert_result = run_automation_prompt(
|
|
226
226
|
convert_prompt,
|
|
227
|
-
|
|
227
|
+
caller="deep-sleep/extract",
|
|
228
228
|
timeout=120,
|
|
229
229
|
output_format="text",
|
|
230
230
|
append_system_prompt=JSON_SYSTEM_PROMPT,
|
|
@@ -2050,7 +2050,7 @@ Also write the machine-readable summary to {LOG_DIR}/self-audit-summary.json.
|
|
|
2050
2050
|
try:
|
|
2051
2051
|
result = run_automation_prompt(
|
|
2052
2052
|
prompt,
|
|
2053
|
-
|
|
2053
|
+
caller="daily_self_audit",
|
|
2054
2054
|
timeout=AUTOMATION_SUBPROCESS_TIMEOUT,
|
|
2055
2055
|
output_format="text",
|
|
2056
2056
|
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
@@ -227,7 +227,7 @@ def call_claude_cli(prompt: str) -> str:
|
|
|
227
227
|
"""Call the configured automation backend for the managed evolution prompt."""
|
|
228
228
|
result = run_automation_prompt(
|
|
229
229
|
prompt,
|
|
230
|
-
|
|
230
|
+
caller="evolution/run",
|
|
231
231
|
timeout=CLI_TIMEOUT,
|
|
232
232
|
output_format="text",
|
|
233
233
|
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
@@ -241,9 +241,9 @@ def call_public_claude_cli(prompt: str, *, cwd: Path) -> str:
|
|
|
241
241
|
"""Run the configured automation backend in an isolated public repo checkout."""
|
|
242
242
|
result = run_automation_prompt(
|
|
243
243
|
prompt,
|
|
244
|
+
caller="evolution/run",
|
|
244
245
|
cwd=cwd,
|
|
245
246
|
env={"NEXO_PUBLIC_CONTRIBUTION": "1"},
|
|
246
|
-
model=_USER_MODEL,
|
|
247
247
|
timeout=CLI_TIMEOUT,
|
|
248
248
|
output_format="text",
|
|
249
249
|
allowed_tools="Read,Write,Edit,Glob,Grep,Bash",
|
|
@@ -916,7 +916,7 @@ Write the report. Be concise — max 40 lines."""
|
|
|
916
916
|
try:
|
|
917
917
|
result = run_automation_prompt(
|
|
918
918
|
prompt,
|
|
919
|
-
|
|
919
|
+
caller="immune/scan",
|
|
920
920
|
timeout=AUTOMATION_SUBPROCESS_TIMEOUT,
|
|
921
921
|
output_format="text",
|
|
922
922
|
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
@@ -255,7 +255,7 @@ Execute without asking."""
|
|
|
255
255
|
try:
|
|
256
256
|
result = run_automation_prompt(
|
|
257
257
|
prompt,
|
|
258
|
-
|
|
258
|
+
caller="postmortem_consolidator",
|
|
259
259
|
timeout=AUTOMATION_SUBPROCESS_TIMEOUT,
|
|
260
260
|
output_format="text",
|
|
261
261
|
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
@@ -446,7 +446,7 @@ Execute without asking."""
|
|
|
446
446
|
try:
|
|
447
447
|
result = run_automation_prompt(
|
|
448
448
|
prompt,
|
|
449
|
-
|
|
449
|
+
caller="sleep/nightly",
|
|
450
450
|
timeout=AUTOMATION_SUBPROCESS_TIMEOUT,
|
|
451
451
|
output_format="text",
|
|
452
452
|
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
@@ -348,7 +348,7 @@ Execute without asking."""
|
|
|
348
348
|
try:
|
|
349
349
|
result = run_automation_prompt(
|
|
350
350
|
prompt,
|
|
351
|
-
|
|
351
|
+
caller="synthesis/daily",
|
|
352
352
|
timeout=AUTOMATION_SUBPROCESS_TIMEOUT,
|
|
353
353
|
output_format="text",
|
|
354
354
|
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
package/src/server.py
CHANGED
|
@@ -63,6 +63,10 @@ from tools_credentials import (
|
|
|
63
63
|
from tools_task_history import (
|
|
64
64
|
handle_task_log, handle_task_list, handle_task_frequency,
|
|
65
65
|
)
|
|
66
|
+
from tools_automation_sessions import (
|
|
67
|
+
handle_session_log_create,
|
|
68
|
+
handle_session_log_close,
|
|
69
|
+
)
|
|
66
70
|
from plugin_loader import load_all_plugins, load_plugin, remove_plugin, list_plugins
|
|
67
71
|
|
|
68
72
|
|
|
@@ -1375,6 +1379,104 @@ def nexo_drive_dismiss(signal_id: int, reason: str) -> str:
|
|
|
1375
1379
|
return handle_drive_dismiss(signal_id, reason)
|
|
1376
1380
|
|
|
1377
1381
|
|
|
1382
|
+
@mcp.tool
|
|
1383
|
+
def nexo_session_log_create(
|
|
1384
|
+
caller: str,
|
|
1385
|
+
backend: str,
|
|
1386
|
+
session_type: str = "interactive_desktop",
|
|
1387
|
+
model: str = "",
|
|
1388
|
+
reasoning_effort: str = "",
|
|
1389
|
+
resonance_tier: str = "",
|
|
1390
|
+
cwd: str = "",
|
|
1391
|
+
pid: str = "",
|
|
1392
|
+
context_excerpt: str = "",
|
|
1393
|
+
) -> str:
|
|
1394
|
+
"""Open an automation_runs row for an interactive Claude/Codex session.
|
|
1395
|
+
|
|
1396
|
+
Designed for clients that spawn Claude/Codex directly (notably NEXO
|
|
1397
|
+
Desktop, which runs a TypeScript process that shells out to the CLI
|
|
1398
|
+
without going through run_automation_prompt). Call this BEFORE
|
|
1399
|
+
spawning the child, store the returned session_id, then call
|
|
1400
|
+
nexo_session_log_close when the session ends.
|
|
1401
|
+
|
|
1402
|
+
Args:
|
|
1403
|
+
caller: Registered caller id (see src/resonance_map.py). For
|
|
1404
|
+
Desktop's "new conversation" button, use
|
|
1405
|
+
"desktop_new_session".
|
|
1406
|
+
backend: "claude_code" or "codex".
|
|
1407
|
+
session_type: "interactive_chat" | "interactive_desktop" — how
|
|
1408
|
+
the session is shaped. Default "interactive_desktop".
|
|
1409
|
+
model: Concrete model the client resolved, e.g. "claude-opus-4-7[1m]".
|
|
1410
|
+
reasoning_effort: Concrete effort string, e.g. "xhigh".
|
|
1411
|
+
resonance_tier: Tier label ("maximo"/"alto"/"medio"/"bajo"). If
|
|
1412
|
+
left empty the Brain resolves it from caller.
|
|
1413
|
+
cwd: Working directory the session is anchored to.
|
|
1414
|
+
pid: Child process PID if available.
|
|
1415
|
+
context_excerpt: Optional first-prompt preview (used to size
|
|
1416
|
+
prompt_chars in telemetry).
|
|
1417
|
+
"""
|
|
1418
|
+
import json as _json
|
|
1419
|
+
result = handle_session_log_create(
|
|
1420
|
+
caller=caller,
|
|
1421
|
+
backend=backend,
|
|
1422
|
+
session_type=session_type,
|
|
1423
|
+
model=model,
|
|
1424
|
+
reasoning_effort=reasoning_effort,
|
|
1425
|
+
resonance_tier=resonance_tier,
|
|
1426
|
+
cwd=cwd,
|
|
1427
|
+
pid=pid,
|
|
1428
|
+
context_excerpt=context_excerpt,
|
|
1429
|
+
)
|
|
1430
|
+
return _json.dumps(result, ensure_ascii=False)
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
@mcp.tool
|
|
1434
|
+
def nexo_session_log_close(
|
|
1435
|
+
session_id: int,
|
|
1436
|
+
returncode: int = 0,
|
|
1437
|
+
duration_ms: int = 0,
|
|
1438
|
+
input_tokens: int = 0,
|
|
1439
|
+
cached_input_tokens: int = 0,
|
|
1440
|
+
output_tokens: int = 0,
|
|
1441
|
+
total_cost_usd: str = "",
|
|
1442
|
+
telemetry_source: str = "",
|
|
1443
|
+
cost_source: str = "",
|
|
1444
|
+
error: str = "",
|
|
1445
|
+
) -> str:
|
|
1446
|
+
"""Close an automation_runs row opened by nexo_session_log_create.
|
|
1447
|
+
|
|
1448
|
+
Args:
|
|
1449
|
+
session_id: id returned by the create call.
|
|
1450
|
+
returncode: child exit code (0 = ok).
|
|
1451
|
+
duration_ms: wall-clock duration in milliseconds.
|
|
1452
|
+
input_tokens / cached_input_tokens / output_tokens: client-side
|
|
1453
|
+
usage counters.
|
|
1454
|
+
total_cost_usd: cost in USD as a string (parsed to float).
|
|
1455
|
+
telemetry_source: short label identifying where the counts came
|
|
1456
|
+
from ("desktop_stream", "codex_json", ...).
|
|
1457
|
+
cost_source: short label for cost provenance.
|
|
1458
|
+
error: short error message if the session failed.
|
|
1459
|
+
"""
|
|
1460
|
+
import json as _json
|
|
1461
|
+
try:
|
|
1462
|
+
cost = float(total_cost_usd) if total_cost_usd else None
|
|
1463
|
+
except ValueError:
|
|
1464
|
+
cost = None
|
|
1465
|
+
result = handle_session_log_close(
|
|
1466
|
+
session_id=session_id,
|
|
1467
|
+
returncode=returncode,
|
|
1468
|
+
duration_ms=duration_ms,
|
|
1469
|
+
input_tokens=input_tokens,
|
|
1470
|
+
cached_input_tokens=cached_input_tokens,
|
|
1471
|
+
output_tokens=output_tokens,
|
|
1472
|
+
total_cost_usd=cost,
|
|
1473
|
+
telemetry_source=telemetry_source,
|
|
1474
|
+
cost_source=cost_source,
|
|
1475
|
+
error=error,
|
|
1476
|
+
)
|
|
1477
|
+
return _json.dumps(result, ensure_ascii=False)
|
|
1478
|
+
|
|
1479
|
+
|
|
1378
1480
|
if __name__ == "__main__":
|
|
1379
1481
|
_server_init()
|
|
1380
1482
|
mcp.run(**_run_kwargs_from_env())
|