nexo-brain 2.6.21 → 3.0.0
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 +72 -20
- package/hooks/hooks.json +79 -0
- package/package.json +1 -1
- package/src/agent_runner.py +296 -8
- package/src/cli.py +209 -4
- package/src/client_preferences.py +115 -0
- package/src/client_sync.py +202 -2
- package/src/cognitive/__init__.py +1 -1
- package/src/cognitive/_search.py +39 -19
- package/src/dashboard/app.py +264 -0
- package/src/dashboard/templates/base.html +4 -0
- package/src/dashboard/templates/dashboard.html +59 -1
- 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/runtime.py +1095 -3
- 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/script_registry.py +142 -0
- package/src/scripts/deep-sleep/apply_findings.py +482 -2
- 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/cli.py
CHANGED
|
@@ -41,6 +41,11 @@ from pathlib import Path
|
|
|
41
41
|
|
|
42
42
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
43
43
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
44
|
+
TERMINAL_CLIENT_LABELS = {
|
|
45
|
+
"claude_code": "Claude Code",
|
|
46
|
+
"codex": "Codex",
|
|
47
|
+
}
|
|
48
|
+
TERMINAL_CLIENT_ORDER = ("claude_code", "codex")
|
|
44
49
|
|
|
45
50
|
|
|
46
51
|
def _get_version() -> str:
|
|
@@ -844,8 +849,73 @@ def _dashboard(args):
|
|
|
844
849
|
return _service_control("dashboard", args.action)
|
|
845
850
|
|
|
846
851
|
|
|
852
|
+
def _terminal_client_label(client: str) -> str:
|
|
853
|
+
return TERMINAL_CLIENT_LABELS.get(client, client.replace("_", " ").title())
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _ordered_available_terminal_clients(preferences: dict, detected: dict) -> list[str]:
|
|
857
|
+
enabled = preferences.get("interactive_clients", {})
|
|
858
|
+
last_used = str(preferences.get("last_terminal_client", "")).strip()
|
|
859
|
+
preferred = str(preferences.get("default_terminal_client", "")).strip()
|
|
860
|
+
ordered: list[str] = []
|
|
861
|
+
|
|
862
|
+
for client in (last_used, preferred, *TERMINAL_CLIENT_ORDER):
|
|
863
|
+
if client in TERMINAL_CLIENT_ORDER and client not in ordered:
|
|
864
|
+
ordered.append(client)
|
|
865
|
+
|
|
866
|
+
return [
|
|
867
|
+
client
|
|
868
|
+
for client in ordered
|
|
869
|
+
if enabled.get(client, False) and detected.get(client, {}).get("installed", False)
|
|
870
|
+
]
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _preferred_terminal_client_label(preferences: dict, clients: list[str]) -> str:
|
|
874
|
+
last_used = str(preferences.get("last_terminal_client", "")).strip()
|
|
875
|
+
if clients and clients[0] == last_used:
|
|
876
|
+
return "last choice"
|
|
877
|
+
return "default"
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _prompt_for_terminal_client(
|
|
881
|
+
clients: list[str],
|
|
882
|
+
normalize_client_key,
|
|
883
|
+
*,
|
|
884
|
+
preferred_label: str = "default",
|
|
885
|
+
) -> str | None:
|
|
886
|
+
if not clients:
|
|
887
|
+
return None
|
|
888
|
+
if len(clients) == 1:
|
|
889
|
+
return clients[0]
|
|
890
|
+
|
|
891
|
+
while True:
|
|
892
|
+
print("Select terminal client for this chat:")
|
|
893
|
+
for index, client in enumerate(clients, start=1):
|
|
894
|
+
suffix = f" [{preferred_label}]" if index == 1 else ""
|
|
895
|
+
print(f" {index}. {_terminal_client_label(client)}{suffix}")
|
|
896
|
+
|
|
897
|
+
try:
|
|
898
|
+
response = input(f"Choose 1-{len(clients)} [1]: ").strip()
|
|
899
|
+
except EOFError:
|
|
900
|
+
return clients[0]
|
|
901
|
+
|
|
902
|
+
if not response:
|
|
903
|
+
return clients[0]
|
|
904
|
+
if response.isdigit():
|
|
905
|
+
choice = int(response)
|
|
906
|
+
if 1 <= choice <= len(clients):
|
|
907
|
+
return clients[choice - 1]
|
|
908
|
+
|
|
909
|
+
client_key = normalize_client_key(response)
|
|
910
|
+
if client_key in clients:
|
|
911
|
+
return client_key
|
|
912
|
+
|
|
913
|
+
print("Invalid choice. Try again.", file=sys.stderr)
|
|
914
|
+
|
|
915
|
+
|
|
847
916
|
def _chat(args):
|
|
848
917
|
target = args.path or "."
|
|
918
|
+
selected_client = getattr(args, "client", None)
|
|
849
919
|
|
|
850
920
|
try:
|
|
851
921
|
from auto_update import startup_preflight
|
|
@@ -867,20 +937,44 @@ def _chat(args):
|
|
|
867
937
|
pass
|
|
868
938
|
|
|
869
939
|
try:
|
|
940
|
+
from client_preferences import (
|
|
941
|
+
detect_installed_clients,
|
|
942
|
+
load_client_preferences,
|
|
943
|
+
normalize_client_key,
|
|
944
|
+
save_client_preferences,
|
|
945
|
+
)
|
|
870
946
|
from agent_runner import TerminalClientUnavailableError, launch_interactive_client
|
|
871
947
|
except ImportError:
|
|
872
948
|
print("Agent runner module not found. Ensure NEXO is properly installed.", file=sys.stderr)
|
|
873
949
|
return 1
|
|
874
950
|
|
|
951
|
+
if not selected_client:
|
|
952
|
+
try:
|
|
953
|
+
preferences = load_client_preferences()
|
|
954
|
+
detected = detect_installed_clients()
|
|
955
|
+
clients = _ordered_available_terminal_clients(preferences, detected)
|
|
956
|
+
selected_client = _prompt_for_terminal_client(
|
|
957
|
+
clients,
|
|
958
|
+
normalize_client_key,
|
|
959
|
+
preferred_label=_preferred_terminal_client_label(preferences, clients),
|
|
960
|
+
)
|
|
961
|
+
except Exception:
|
|
962
|
+
selected_client = None
|
|
963
|
+
|
|
875
964
|
try:
|
|
876
965
|
result = launch_interactive_client(
|
|
877
966
|
target=target,
|
|
878
|
-
client=
|
|
967
|
+
client=selected_client,
|
|
879
968
|
env=os.environ.copy(),
|
|
880
969
|
)
|
|
881
970
|
except TerminalClientUnavailableError as exc:
|
|
882
971
|
print(str(exc), file=sys.stderr)
|
|
883
972
|
return 1
|
|
973
|
+
if result.returncode == 0 and selected_client:
|
|
974
|
+
try:
|
|
975
|
+
save_client_preferences(last_terminal_client=normalize_client_key(selected_client))
|
|
976
|
+
except Exception:
|
|
977
|
+
pass
|
|
884
978
|
return int(result.returncode)
|
|
885
979
|
|
|
886
980
|
|
|
@@ -962,6 +1056,23 @@ def _skills_apply(args):
|
|
|
962
1056
|
return 0 if result.get("ok") else 1
|
|
963
1057
|
|
|
964
1058
|
|
|
1059
|
+
def _skills_test(args):
|
|
1060
|
+
from skills_runtime import test_skill
|
|
1061
|
+
|
|
1062
|
+
try:
|
|
1063
|
+
params = json.loads(args.params) if args.params else {}
|
|
1064
|
+
except json.JSONDecodeError as e:
|
|
1065
|
+
print(f"Invalid params JSON: {e}", file=sys.stderr)
|
|
1066
|
+
return 1
|
|
1067
|
+
|
|
1068
|
+
result = test_skill(args.id, params=params, mode=args.mode, context=args.context)
|
|
1069
|
+
if args.json:
|
|
1070
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1071
|
+
else:
|
|
1072
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1073
|
+
return 0 if result.get("ok") else 1
|
|
1074
|
+
|
|
1075
|
+
|
|
965
1076
|
def _skills_sync(args):
|
|
966
1077
|
from skills_runtime import sync_skills
|
|
967
1078
|
|
|
@@ -1009,12 +1120,68 @@ def _skills_evolution(args):
|
|
|
1009
1120
|
return 0
|
|
1010
1121
|
|
|
1011
1122
|
|
|
1123
|
+
def _skills_promote(args):
|
|
1124
|
+
from skills_runtime import promote_skill
|
|
1125
|
+
|
|
1126
|
+
result = promote_skill(args.id, target_level=args.target_level, reason=args.reason)
|
|
1127
|
+
if args.json:
|
|
1128
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1129
|
+
else:
|
|
1130
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1131
|
+
return 0 if result.get("ok") else 1
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _skills_retire(args):
|
|
1135
|
+
from skills_runtime import retire_skill
|
|
1136
|
+
|
|
1137
|
+
result = retire_skill(args.id, replacement_id=args.replacement_id, reason=args.reason)
|
|
1138
|
+
if args.json:
|
|
1139
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1140
|
+
else:
|
|
1141
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1142
|
+
return 0 if result.get("ok") else 1
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _skills_compose(args):
|
|
1146
|
+
from skills_runtime import compose_skills
|
|
1147
|
+
|
|
1148
|
+
try:
|
|
1149
|
+
component_ids = json.loads(args.component_ids) if args.component_ids.strip().startswith("[") else [
|
|
1150
|
+
item.strip() for item in args.component_ids.split(",") if item.strip()
|
|
1151
|
+
]
|
|
1152
|
+
tags = json.loads(args.tags) if args.tags.strip().startswith("[") else [
|
|
1153
|
+
item.strip() for item in args.tags.split(",") if item.strip()
|
|
1154
|
+
]
|
|
1155
|
+
trigger_patterns = json.loads(args.trigger_patterns) if args.trigger_patterns.strip().startswith("[") else [
|
|
1156
|
+
item.strip() for item in args.trigger_patterns.split(",") if item.strip()
|
|
1157
|
+
]
|
|
1158
|
+
except json.JSONDecodeError as e:
|
|
1159
|
+
print(f"Invalid JSON: {e}", file=sys.stderr)
|
|
1160
|
+
return 1
|
|
1161
|
+
|
|
1162
|
+
result = compose_skills(
|
|
1163
|
+
new_skill_id=args.new_id,
|
|
1164
|
+
name=args.name,
|
|
1165
|
+
component_ids=component_ids,
|
|
1166
|
+
description=args.description,
|
|
1167
|
+
level=args.level,
|
|
1168
|
+
mode=args.mode,
|
|
1169
|
+
tags=tags,
|
|
1170
|
+
trigger_patterns=trigger_patterns,
|
|
1171
|
+
)
|
|
1172
|
+
if args.json:
|
|
1173
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1174
|
+
else:
|
|
1175
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
1176
|
+
return 0 if result.get("ok") else 1
|
|
1177
|
+
|
|
1178
|
+
|
|
1012
1179
|
def _print_help():
|
|
1013
1180
|
v = _get_version()
|
|
1014
1181
|
print(f"""NEXO Runtime CLI v{v}
|
|
1015
1182
|
|
|
1016
1183
|
Commands:
|
|
1017
|
-
nexo chat [path] [--client claude_code|codex] Launch
|
|
1184
|
+
nexo chat [path] [--client claude_code|codex] Launch a NEXO terminal client
|
|
1018
1185
|
nexo doctor [--tier boot|runtime|deep|all] [--fix] System diagnostics
|
|
1019
1186
|
nexo scripts list|create|classify|sync|reconcile|ensure-schedules|schedules|run|doctor|call|unschedule|remove
|
|
1020
1187
|
Personal scripts
|
|
@@ -1036,12 +1203,12 @@ def main():
|
|
|
1036
1203
|
sub = parser.add_subparsers(dest="command")
|
|
1037
1204
|
|
|
1038
1205
|
# -- chat --
|
|
1039
|
-
chat_parser = sub.add_parser("chat", help="Launch
|
|
1206
|
+
chat_parser = sub.add_parser("chat", help="Launch a NEXO terminal client")
|
|
1040
1207
|
chat_parser.add_argument("path", nargs="?", default=".", help="Working directory (default: current directory)")
|
|
1041
1208
|
chat_parser.add_argument(
|
|
1042
1209
|
"--client",
|
|
1043
1210
|
choices=["claude_code", "codex"],
|
|
1044
|
-
help="Override the
|
|
1211
|
+
help="Override the chat picker and launch a specific terminal client",
|
|
1045
1212
|
)
|
|
1046
1213
|
|
|
1047
1214
|
# -- scripts --
|
|
@@ -1154,6 +1321,13 @@ def main():
|
|
|
1154
1321
|
skills_apply_p.add_argument("--context", default="", help="Usage context for feedback loop")
|
|
1155
1322
|
skills_apply_p.add_argument("--json", action="store_true", help="JSON output")
|
|
1156
1323
|
|
|
1324
|
+
skills_test_p = skills_sub.add_parser("test", help="Dry-run test a skill")
|
|
1325
|
+
skills_test_p.add_argument("id", help="Skill ID")
|
|
1326
|
+
skills_test_p.add_argument("--params", default="{}", help="JSON parameters")
|
|
1327
|
+
skills_test_p.add_argument("--mode", default="auto", choices=["auto", "guide", "execute", "hybrid"])
|
|
1328
|
+
skills_test_p.add_argument("--context", default="", help="Testing context")
|
|
1329
|
+
skills_test_p.add_argument("--json", action="store_true", help="JSON output")
|
|
1330
|
+
|
|
1157
1331
|
skills_sync_p = skills_sub.add_parser("sync", help="Sync filesystem skills")
|
|
1158
1332
|
skills_sync_p.add_argument("--json", action="store_true", help="JSON output")
|
|
1159
1333
|
|
|
@@ -1170,6 +1344,29 @@ def main():
|
|
|
1170
1344
|
skills_evolution_p = skills_sub.add_parser("evolution", help="Evolution candidates")
|
|
1171
1345
|
skills_evolution_p.add_argument("--json", action="store_true", help="JSON output")
|
|
1172
1346
|
|
|
1347
|
+
skills_promote_p = skills_sub.add_parser("promote", help="Promote a skill lifecycle level")
|
|
1348
|
+
skills_promote_p.add_argument("id", help="Skill ID")
|
|
1349
|
+
skills_promote_p.add_argument("--target-level", default="published", choices=["draft", "published", "stable"])
|
|
1350
|
+
skills_promote_p.add_argument("--reason", default="", help="Why promote this skill")
|
|
1351
|
+
skills_promote_p.add_argument("--json", action="store_true", help="JSON output")
|
|
1352
|
+
|
|
1353
|
+
skills_retire_p = skills_sub.add_parser("retire", help="Archive a skill")
|
|
1354
|
+
skills_retire_p.add_argument("id", help="Skill ID")
|
|
1355
|
+
skills_retire_p.add_argument("--replacement-id", default="", help="Optional replacement skill ID")
|
|
1356
|
+
skills_retire_p.add_argument("--reason", default="", help="Why retire this skill")
|
|
1357
|
+
skills_retire_p.add_argument("--json", action="store_true", help="JSON output")
|
|
1358
|
+
|
|
1359
|
+
skills_compose_p = skills_sub.add_parser("compose", help="Compose multiple skills into one")
|
|
1360
|
+
skills_compose_p.add_argument("new_id", help="New skill ID")
|
|
1361
|
+
skills_compose_p.add_argument("name", help="New skill name")
|
|
1362
|
+
skills_compose_p.add_argument("--component-ids", required=True, help="JSON array or comma-separated skill IDs")
|
|
1363
|
+
skills_compose_p.add_argument("--description", default="", help="Composite skill description")
|
|
1364
|
+
skills_compose_p.add_argument("--level", default="draft", choices=["trace", "draft", "published", "stable"])
|
|
1365
|
+
skills_compose_p.add_argument("--mode", default="guide", choices=["guide", "hybrid"])
|
|
1366
|
+
skills_compose_p.add_argument("--tags", default="[]", help="JSON array or comma-separated tags")
|
|
1367
|
+
skills_compose_p.add_argument("--trigger-patterns", default="[]", help="JSON array or comma-separated trigger patterns")
|
|
1368
|
+
skills_compose_p.add_argument("--json", action="store_true", help="JSON output")
|
|
1369
|
+
|
|
1173
1370
|
# -- dashboard --
|
|
1174
1371
|
dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
|
|
1175
1372
|
dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
|
|
@@ -1238,6 +1435,8 @@ def main():
|
|
|
1238
1435
|
return _skills_get(args)
|
|
1239
1436
|
elif args.skills_command == "apply":
|
|
1240
1437
|
return _skills_apply(args)
|
|
1438
|
+
elif args.skills_command == "test":
|
|
1439
|
+
return _skills_test(args)
|
|
1241
1440
|
elif args.skills_command == "sync":
|
|
1242
1441
|
return _skills_sync(args)
|
|
1243
1442
|
elif args.skills_command == "approve":
|
|
@@ -1246,6 +1445,12 @@ def main():
|
|
|
1246
1445
|
return _skills_featured(args)
|
|
1247
1446
|
elif args.skills_command == "evolution":
|
|
1248
1447
|
return _skills_evolution(args)
|
|
1448
|
+
elif args.skills_command == "promote":
|
|
1449
|
+
return _skills_promote(args)
|
|
1450
|
+
elif args.skills_command == "retire":
|
|
1451
|
+
return _skills_retire(args)
|
|
1452
|
+
elif args.skills_command == "compose":
|
|
1453
|
+
return _skills_compose(args)
|
|
1249
1454
|
else:
|
|
1250
1455
|
skills_parser.print_help()
|
|
1251
1456
|
return 0
|
|
@@ -30,6 +30,12 @@ AUTOMATION_BACKEND_KEYS = (
|
|
|
30
30
|
CLIENT_CLAUDE_CODE,
|
|
31
31
|
CLIENT_CODEX,
|
|
32
32
|
)
|
|
33
|
+
AUTOMATION_TASK_PROFILE_KEYS = (
|
|
34
|
+
"default",
|
|
35
|
+
"fast",
|
|
36
|
+
"balanced",
|
|
37
|
+
"deep",
|
|
38
|
+
)
|
|
33
39
|
INSTALL_PREFERENCE_KEYS = {
|
|
34
40
|
"ask",
|
|
35
41
|
"auto",
|
|
@@ -40,6 +46,10 @@ DEFAULT_CLAUDE_CODE_MODEL = "claude-opus-4-6[1m]"
|
|
|
40
46
|
DEFAULT_CLAUDE_CODE_REASONING_EFFORT = ""
|
|
41
47
|
DEFAULT_CODEX_MODEL = "gpt-5.4"
|
|
42
48
|
DEFAULT_CODEX_REASONING_EFFORT = "xhigh"
|
|
49
|
+
DEFAULT_FAST_MODEL = "gpt-5.4-mini"
|
|
50
|
+
DEFAULT_FAST_REASONING_EFFORT = "medium"
|
|
51
|
+
|
|
52
|
+
|
|
43
53
|
def _user_home() -> Path:
|
|
44
54
|
return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
|
|
45
55
|
|
|
@@ -73,9 +83,11 @@ def default_client_preferences() -> dict:
|
|
|
73
83
|
CLIENT_CLAUDE_DESKTOP: False,
|
|
74
84
|
},
|
|
75
85
|
"default_terminal_client": CLIENT_CLAUDE_CODE,
|
|
86
|
+
"last_terminal_client": "",
|
|
76
87
|
"automation_enabled": True,
|
|
77
88
|
"automation_backend": CLIENT_CLAUDE_CODE,
|
|
78
89
|
"client_runtime_profiles": default_client_runtime_profiles(),
|
|
90
|
+
"automation_task_profiles": default_automation_task_profiles(),
|
|
79
91
|
"client_install_preferences": {
|
|
80
92
|
CLIENT_CLAUDE_CODE: "ask",
|
|
81
93
|
CLIENT_CODEX: "ask",
|
|
@@ -189,6 +201,14 @@ def normalize_default_terminal_client(value, interactive_clients: dict[str, bool
|
|
|
189
201
|
return CLIENT_CLAUDE_CODE
|
|
190
202
|
|
|
191
203
|
|
|
204
|
+
def normalize_last_terminal_client(value, interactive_clients: dict[str, bool] | None = None) -> str:
|
|
205
|
+
interactive_clients = normalize_interactive_clients(interactive_clients or {})
|
|
206
|
+
candidate = normalize_client_key(value)
|
|
207
|
+
if candidate in TERMINAL_CLIENT_KEYS and interactive_clients.get(candidate, False):
|
|
208
|
+
return candidate
|
|
209
|
+
return ""
|
|
210
|
+
|
|
211
|
+
|
|
192
212
|
def normalize_automation_enabled(value) -> bool:
|
|
193
213
|
return _coerce_bool(value, True)
|
|
194
214
|
|
|
@@ -228,6 +248,31 @@ def default_client_runtime_profiles() -> dict[str, dict[str, str]]:
|
|
|
228
248
|
}
|
|
229
249
|
|
|
230
250
|
|
|
251
|
+
def default_automation_task_profiles() -> dict[str, dict[str, str]]:
|
|
252
|
+
return {
|
|
253
|
+
"default": {
|
|
254
|
+
"backend": "",
|
|
255
|
+
"model": "",
|
|
256
|
+
"reasoning_effort": "",
|
|
257
|
+
},
|
|
258
|
+
"fast": {
|
|
259
|
+
"backend": CLIENT_CODEX,
|
|
260
|
+
"model": DEFAULT_FAST_MODEL,
|
|
261
|
+
"reasoning_effort": DEFAULT_FAST_REASONING_EFFORT,
|
|
262
|
+
},
|
|
263
|
+
"balanced": {
|
|
264
|
+
"backend": "",
|
|
265
|
+
"model": "",
|
|
266
|
+
"reasoning_effort": "",
|
|
267
|
+
},
|
|
268
|
+
"deep": {
|
|
269
|
+
"backend": CLIENT_CLAUDE_CODE,
|
|
270
|
+
"model": DEFAULT_CLAUDE_CODE_MODEL,
|
|
271
|
+
"reasoning_effort": DEFAULT_CLAUDE_CODE_REASONING_EFFORT,
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
231
276
|
def _normalize_runtime_model(value, *, default: str) -> str:
|
|
232
277
|
candidate = str(value or "").strip()
|
|
233
278
|
return candidate or default
|
|
@@ -267,6 +312,31 @@ def normalize_client_runtime_profiles(value) -> dict[str, dict[str, str]]:
|
|
|
267
312
|
return normalized
|
|
268
313
|
|
|
269
314
|
|
|
315
|
+
def normalize_automation_task_profiles(value) -> dict[str, dict[str, str]]:
|
|
316
|
+
defaults = default_automation_task_profiles()
|
|
317
|
+
normalized = {key: dict(profile) for key, profile in defaults.items()}
|
|
318
|
+
if not isinstance(value, dict):
|
|
319
|
+
return normalized
|
|
320
|
+
|
|
321
|
+
for raw_profile, raw_value in value.items():
|
|
322
|
+
profile_key = str(raw_profile or "").strip().lower()
|
|
323
|
+
if profile_key not in AUTOMATION_TASK_PROFILE_KEYS:
|
|
324
|
+
continue
|
|
325
|
+
if not isinstance(raw_value, dict):
|
|
326
|
+
continue
|
|
327
|
+
backend = normalize_backend_key(raw_value.get("backend"))
|
|
328
|
+
if backend == BACKEND_NONE:
|
|
329
|
+
backend = ""
|
|
330
|
+
normalized[profile_key] = {
|
|
331
|
+
"backend": backend or defaults[profile_key]["backend"],
|
|
332
|
+
"model": str(raw_value.get("model") or defaults[profile_key]["model"]).strip(),
|
|
333
|
+
"reasoning_effort": str(
|
|
334
|
+
raw_value.get("reasoning_effort") or defaults[profile_key]["reasoning_effort"]
|
|
335
|
+
).strip().lower(),
|
|
336
|
+
}
|
|
337
|
+
return normalized
|
|
338
|
+
|
|
339
|
+
|
|
270
340
|
def normalize_client_preferences(
|
|
271
341
|
schedule: dict | None = None,
|
|
272
342
|
*,
|
|
@@ -282,6 +352,10 @@ def normalize_client_preferences(
|
|
|
282
352
|
schedule.get("default_terminal_client"),
|
|
283
353
|
interactive_clients=interactive_clients,
|
|
284
354
|
)
|
|
355
|
+
last_terminal_client = normalize_last_terminal_client(
|
|
356
|
+
schedule.get("last_terminal_client"),
|
|
357
|
+
interactive_clients=interactive_clients,
|
|
358
|
+
)
|
|
285
359
|
automation_backend = normalize_automation_backend(
|
|
286
360
|
schedule.get("automation_backend"),
|
|
287
361
|
automation_enabled=automation_enabled,
|
|
@@ -295,9 +369,13 @@ def normalize_client_preferences(
|
|
|
295
369
|
return {
|
|
296
370
|
"interactive_clients": interactive_clients,
|
|
297
371
|
"default_terminal_client": default_terminal_client,
|
|
372
|
+
"last_terminal_client": last_terminal_client,
|
|
298
373
|
"automation_enabled": automation_enabled,
|
|
299
374
|
"automation_backend": automation_backend,
|
|
300
375
|
"client_runtime_profiles": runtime_profiles,
|
|
376
|
+
"automation_task_profiles": normalize_automation_task_profiles(
|
|
377
|
+
schedule.get("automation_task_profiles")
|
|
378
|
+
),
|
|
301
379
|
"client_install_preferences": install_preferences,
|
|
302
380
|
}
|
|
303
381
|
|
|
@@ -307,9 +385,11 @@ def apply_client_preferences(
|
|
|
307
385
|
*,
|
|
308
386
|
interactive_clients: dict | None = None,
|
|
309
387
|
default_terminal_client: str | None = None,
|
|
388
|
+
last_terminal_client: str | None = None,
|
|
310
389
|
automation_enabled=None,
|
|
311
390
|
automation_backend: str | None = None,
|
|
312
391
|
client_runtime_profiles: dict | None = None,
|
|
392
|
+
automation_task_profiles: dict | None = None,
|
|
313
393
|
client_install_preferences: dict | None = None,
|
|
314
394
|
) -> dict:
|
|
315
395
|
merged = dict(schedule or {})
|
|
@@ -324,6 +404,10 @@ def apply_client_preferences(
|
|
|
324
404
|
default_terminal_client if default_terminal_client is not None else current["default_terminal_client"],
|
|
325
405
|
interactive_clients=merged["interactive_clients"],
|
|
326
406
|
)
|
|
407
|
+
merged["last_terminal_client"] = normalize_last_terminal_client(
|
|
408
|
+
last_terminal_client if last_terminal_client is not None else current.get("last_terminal_client", ""),
|
|
409
|
+
interactive_clients=merged["interactive_clients"],
|
|
410
|
+
)
|
|
327
411
|
merged["automation_backend"] = normalize_automation_backend(
|
|
328
412
|
automation_backend if automation_backend is not None else current["automation_backend"],
|
|
329
413
|
automation_enabled=merged["automation_enabled"],
|
|
@@ -333,6 +417,11 @@ def apply_client_preferences(
|
|
|
333
417
|
if client_runtime_profiles is not None
|
|
334
418
|
else current["client_runtime_profiles"]
|
|
335
419
|
)
|
|
420
|
+
merged["automation_task_profiles"] = normalize_automation_task_profiles(
|
|
421
|
+
automation_task_profiles
|
|
422
|
+
if automation_task_profiles is not None
|
|
423
|
+
else current["automation_task_profiles"]
|
|
424
|
+
)
|
|
336
425
|
merged["client_install_preferences"] = normalize_client_install_preferences(
|
|
337
426
|
client_install_preferences
|
|
338
427
|
if client_install_preferences is not None
|
|
@@ -349,18 +438,22 @@ def save_client_preferences(
|
|
|
349
438
|
*,
|
|
350
439
|
interactive_clients: dict | None = None,
|
|
351
440
|
default_terminal_client: str | None = None,
|
|
441
|
+
last_terminal_client: str | None = None,
|
|
352
442
|
automation_enabled=None,
|
|
353
443
|
automation_backend: str | None = None,
|
|
354
444
|
client_runtime_profiles: dict | None = None,
|
|
445
|
+
automation_task_profiles: dict | None = None,
|
|
355
446
|
client_install_preferences: dict | None = None,
|
|
356
447
|
) -> Path:
|
|
357
448
|
schedule = apply_client_preferences(
|
|
358
449
|
load_schedule_config(),
|
|
359
450
|
interactive_clients=interactive_clients,
|
|
360
451
|
default_terminal_client=default_terminal_client,
|
|
452
|
+
last_terminal_client=last_terminal_client,
|
|
361
453
|
automation_enabled=automation_enabled,
|
|
362
454
|
automation_backend=automation_backend,
|
|
363
455
|
client_runtime_profiles=client_runtime_profiles,
|
|
456
|
+
automation_task_profiles=automation_task_profiles,
|
|
364
457
|
client_install_preferences=client_install_preferences,
|
|
365
458
|
)
|
|
366
459
|
return save_schedule_config(schedule)
|
|
@@ -456,3 +549,25 @@ def resolve_client_runtime_profile(
|
|
|
456
549
|
default=defaults[client_key]["reasoning_effort"],
|
|
457
550
|
),
|
|
458
551
|
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def resolve_automation_task_profile(
|
|
555
|
+
profile: str | None,
|
|
556
|
+
*,
|
|
557
|
+
preferences: dict | None = None,
|
|
558
|
+
) -> dict[str, str]:
|
|
559
|
+
normalized = preferences or load_client_preferences()
|
|
560
|
+
defaults = default_automation_task_profiles()
|
|
561
|
+
profile_key = str(profile or "").strip().lower() or "default"
|
|
562
|
+
if profile_key not in AUTOMATION_TASK_PROFILE_KEYS:
|
|
563
|
+
profile_key = "default"
|
|
564
|
+
configured = normalize_automation_task_profiles(normalized.get("automation_task_profiles"))
|
|
565
|
+
selected = dict(configured.get(profile_key) or defaults[profile_key])
|
|
566
|
+
backend = selected.get("backend") or resolve_automation_backend(normalized)
|
|
567
|
+
runtime_profile = resolve_client_runtime_profile(backend, preferences=normalized)
|
|
568
|
+
return {
|
|
569
|
+
"name": profile_key,
|
|
570
|
+
"backend": backend,
|
|
571
|
+
"model": selected.get("model") or runtime_profile["model"],
|
|
572
|
+
"reasoning_effort": selected.get("reasoning_effort") or runtime_profile["reasoning_effort"],
|
|
573
|
+
}
|