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/agent_runner.py
CHANGED
|
@@ -8,6 +8,7 @@ import shlex
|
|
|
8
8
|
import shutil
|
|
9
9
|
import subprocess
|
|
10
10
|
import tempfile
|
|
11
|
+
import time
|
|
11
12
|
import tomllib
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
@@ -18,6 +19,7 @@ from client_preferences import (
|
|
|
18
19
|
TERMINAL_CLIENT_KEYS,
|
|
19
20
|
load_client_preferences,
|
|
20
21
|
resolve_automation_backend,
|
|
22
|
+
resolve_automation_task_profile,
|
|
21
23
|
resolve_client_runtime_profile,
|
|
22
24
|
resolve_terminal_client,
|
|
23
25
|
)
|
|
@@ -25,6 +27,12 @@ from client_preferences import (
|
|
|
25
27
|
|
|
26
28
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
27
29
|
CLAUDE_LEGACY_MODEL_HINTS = {"opus", "sonnet"}
|
|
30
|
+
MODEL_PRICING_USD_PER_1M = {
|
|
31
|
+
# Pricing snapshot used only when the backend does not return explicit cost.
|
|
32
|
+
# Codex model names map to the current GPT-5 family pricing.
|
|
33
|
+
"gpt-5.4": {"input": 1.25, "cached_input": 0.125, "output": 10.0},
|
|
34
|
+
"gpt-5.4-mini": {"input": 0.25, "cached_input": 0.025, "output": 2.0},
|
|
35
|
+
}
|
|
28
36
|
|
|
29
37
|
|
|
30
38
|
class AgentRunnerError(RuntimeError):
|
|
@@ -39,6 +47,192 @@ class AutomationBackendUnavailableError(AgentRunnerError):
|
|
|
39
47
|
"""Raised when the configured automation backend is unavailable."""
|
|
40
48
|
|
|
41
49
|
|
|
50
|
+
def _canonical_pricing_model(model: str) -> str:
|
|
51
|
+
lowered = str(model or "").strip().lower()
|
|
52
|
+
lowered = lowered.split("[", 1)[0]
|
|
53
|
+
aliases = {
|
|
54
|
+
"gpt-5": "gpt-5.4",
|
|
55
|
+
"gpt-5.4": "gpt-5.4",
|
|
56
|
+
"gpt-5-mini": "gpt-5.4-mini",
|
|
57
|
+
"gpt-5.4-mini": "gpt-5.4-mini",
|
|
58
|
+
}
|
|
59
|
+
return aliases.get(lowered, lowered)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _estimate_openai_cost_usd(model: str, *, input_tokens: int, cached_input_tokens: int, output_tokens: int) -> tuple[float | None, str]:
|
|
63
|
+
pricing = MODEL_PRICING_USD_PER_1M.get(_canonical_pricing_model(model))
|
|
64
|
+
if not pricing:
|
|
65
|
+
return None, "pricing_unavailable"
|
|
66
|
+
total = 0.0
|
|
67
|
+
total += (max(0, int(input_tokens or 0)) / 1_000_000.0) * pricing["input"]
|
|
68
|
+
total += (max(0, int(cached_input_tokens or 0)) / 1_000_000.0) * pricing["cached_input"]
|
|
69
|
+
total += (max(0, int(output_tokens or 0)) / 1_000_000.0) * pricing["output"]
|
|
70
|
+
return round(total, 6), "pricing_snapshot"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _safe_json_loads(raw: str) -> dict | list | None:
|
|
74
|
+
try:
|
|
75
|
+
return json.loads(raw)
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _extract_claude_telemetry(raw_stdout: str, *, requested_output_format: str) -> tuple[str, dict]:
|
|
81
|
+
payload = _safe_json_loads(raw_stdout) if str(raw_stdout or "").strip().startswith("{") else None
|
|
82
|
+
if not isinstance(payload, dict):
|
|
83
|
+
return raw_stdout or "", {
|
|
84
|
+
"telemetry_source": "missing",
|
|
85
|
+
"cost_source": "missing",
|
|
86
|
+
"usage": {},
|
|
87
|
+
"warnings": ["backend did not return parseable JSON telemetry"],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
result_payload = payload.get("result", "")
|
|
91
|
+
if requested_output_format and requested_output_format.lower() == "json" and not isinstance(result_payload, str):
|
|
92
|
+
final_stdout = json.dumps(result_payload, ensure_ascii=False)
|
|
93
|
+
else:
|
|
94
|
+
final_stdout = result_payload if isinstance(result_payload, str) else json.dumps(result_payload, ensure_ascii=False)
|
|
95
|
+
|
|
96
|
+
usage = payload.get("usage") or {}
|
|
97
|
+
model_usage = payload.get("modelUsage") or {}
|
|
98
|
+
explicit_cost = payload.get("total_cost_usd")
|
|
99
|
+
if explicit_cost is None and isinstance(model_usage, dict):
|
|
100
|
+
explicit_cost = sum(
|
|
101
|
+
float((item or {}).get("costUSD") or 0.0)
|
|
102
|
+
for item in model_usage.values()
|
|
103
|
+
if isinstance(item, dict)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return final_stdout, {
|
|
107
|
+
"telemetry_source": "claude_json",
|
|
108
|
+
"cost_source": "backend",
|
|
109
|
+
"usage": {
|
|
110
|
+
"input_tokens": int(usage.get("input_tokens") or 0),
|
|
111
|
+
"cached_input_tokens": int(usage.get("cache_read_input_tokens") or 0),
|
|
112
|
+
"output_tokens": int(usage.get("output_tokens") or 0),
|
|
113
|
+
},
|
|
114
|
+
"total_cost_usd": float(explicit_cost) if explicit_cost is not None else None,
|
|
115
|
+
"raw": payload,
|
|
116
|
+
"warnings": [],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _extract_codex_telemetry(stream_stdout: str, *, final_stdout: str, model: str) -> tuple[str, dict]:
|
|
121
|
+
usage_payload: dict = {}
|
|
122
|
+
raw_events: list[dict] = []
|
|
123
|
+
for line in str(stream_stdout or "").splitlines():
|
|
124
|
+
line = line.strip()
|
|
125
|
+
if not line.startswith("{"):
|
|
126
|
+
continue
|
|
127
|
+
payload = _safe_json_loads(line)
|
|
128
|
+
if not isinstance(payload, dict):
|
|
129
|
+
continue
|
|
130
|
+
raw_events.append(payload)
|
|
131
|
+
if payload.get("type") == "turn.completed" and isinstance(payload.get("usage"), dict):
|
|
132
|
+
usage_payload = payload["usage"]
|
|
133
|
+
|
|
134
|
+
usage = {
|
|
135
|
+
"input_tokens": int(usage_payload.get("input_tokens") or 0),
|
|
136
|
+
"cached_input_tokens": int(usage_payload.get("cached_input_tokens") or 0),
|
|
137
|
+
"output_tokens": int(usage_payload.get("output_tokens") or 0),
|
|
138
|
+
}
|
|
139
|
+
total_cost_usd = usage_payload.get("total_cost_usd")
|
|
140
|
+
cost_source = "backend" if total_cost_usd is not None else "missing"
|
|
141
|
+
warnings: list[str] = []
|
|
142
|
+
if total_cost_usd is None:
|
|
143
|
+
estimated_cost, estimated_source = _estimate_openai_cost_usd(
|
|
144
|
+
model,
|
|
145
|
+
input_tokens=usage["input_tokens"],
|
|
146
|
+
cached_input_tokens=usage["cached_input_tokens"],
|
|
147
|
+
output_tokens=usage["output_tokens"],
|
|
148
|
+
)
|
|
149
|
+
total_cost_usd = estimated_cost
|
|
150
|
+
cost_source = estimated_source
|
|
151
|
+
if estimated_cost is None:
|
|
152
|
+
warnings.append(f"no pricing snapshot available for model `{model}`")
|
|
153
|
+
|
|
154
|
+
if not usage_payload:
|
|
155
|
+
warnings.append("backend did not return usage telemetry")
|
|
156
|
+
|
|
157
|
+
return final_stdout, {
|
|
158
|
+
"telemetry_source": "codex_jsonl",
|
|
159
|
+
"cost_source": cost_source,
|
|
160
|
+
"usage": usage,
|
|
161
|
+
"total_cost_usd": float(total_cost_usd) if total_cost_usd is not None else None,
|
|
162
|
+
"raw": raw_events[-8:],
|
|
163
|
+
"warnings": warnings,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _append_stderr(stderr: str, message: str) -> str:
|
|
168
|
+
bits = [part for part in [str(stderr or "").rstrip(), str(message or "").strip()] if part]
|
|
169
|
+
if not bits:
|
|
170
|
+
return ""
|
|
171
|
+
return "\n".join(bits) + "\n"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _record_automation_run(
|
|
175
|
+
*,
|
|
176
|
+
backend: str,
|
|
177
|
+
task_profile: str,
|
|
178
|
+
model: str,
|
|
179
|
+
reasoning_effort: str,
|
|
180
|
+
cwd: Path,
|
|
181
|
+
output_format: str,
|
|
182
|
+
prompt: str,
|
|
183
|
+
returncode: int,
|
|
184
|
+
duration_ms: int,
|
|
185
|
+
telemetry: dict,
|
|
186
|
+
) -> tuple[bool, str]:
|
|
187
|
+
try:
|
|
188
|
+
from db._core import get_db
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
return False, f"automation telemetry unavailable: {exc}"
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
conn = get_db()
|
|
194
|
+
usage = telemetry.get("usage") or {}
|
|
195
|
+
conn.execute(
|
|
196
|
+
"""
|
|
197
|
+
INSERT INTO automation_runs (
|
|
198
|
+
backend, task_profile, model, reasoning_effort, cwd, output_format,
|
|
199
|
+
prompt_chars, returncode, duration_ms,
|
|
200
|
+
input_tokens, cached_input_tokens, output_tokens,
|
|
201
|
+
total_cost_usd, telemetry_source, cost_source, status, metadata
|
|
202
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
203
|
+
""",
|
|
204
|
+
(
|
|
205
|
+
backend,
|
|
206
|
+
task_profile or "default",
|
|
207
|
+
model,
|
|
208
|
+
reasoning_effort,
|
|
209
|
+
str(cwd),
|
|
210
|
+
output_format or "text",
|
|
211
|
+
len(prompt or ""),
|
|
212
|
+
int(returncode),
|
|
213
|
+
int(duration_ms),
|
|
214
|
+
int(usage.get("input_tokens") or 0),
|
|
215
|
+
int(usage.get("cached_input_tokens") or 0),
|
|
216
|
+
int(usage.get("output_tokens") or 0),
|
|
217
|
+
telemetry.get("total_cost_usd"),
|
|
218
|
+
telemetry.get("telemetry_source", ""),
|
|
219
|
+
telemetry.get("cost_source", ""),
|
|
220
|
+
"ok" if int(returncode) == 0 else "failed",
|
|
221
|
+
json.dumps(
|
|
222
|
+
{
|
|
223
|
+
"warnings": telemetry.get("warnings") or [],
|
|
224
|
+
"raw": telemetry.get("raw") or {},
|
|
225
|
+
},
|
|
226
|
+
ensure_ascii=False,
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
conn.commit()
|
|
231
|
+
return True, ""
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
return False, f"automation telemetry unavailable: {exc}"
|
|
234
|
+
|
|
235
|
+
|
|
42
236
|
def _resolve_claude_cli() -> str:
|
|
43
237
|
saved = NEXO_HOME / "config" / "claude-cli-path"
|
|
44
238
|
if saved.exists():
|
|
@@ -109,6 +303,10 @@ def _codex_initial_messages_config(prompt_text: str) -> str:
|
|
|
109
303
|
return f'initial_messages=[{{role="system",content={json.dumps(prompt_text, ensure_ascii=False)}}}]'
|
|
110
304
|
|
|
111
305
|
|
|
306
|
+
def _codex_interactive_launch_flags() -> list[str]:
|
|
307
|
+
return ["--sandbox", "danger-full-access", "--ask-for-approval", "never"]
|
|
308
|
+
|
|
309
|
+
|
|
112
310
|
def build_interactive_client_command(
|
|
113
311
|
*,
|
|
114
312
|
target: str | os.PathLike[str],
|
|
@@ -140,7 +338,7 @@ def build_interactive_client_command(
|
|
|
140
338
|
raise TerminalClientUnavailableError(
|
|
141
339
|
"Codex launcher not found in PATH. Install `codex` first or reconfigure NEXO."
|
|
142
340
|
)
|
|
143
|
-
cmd = [codex_bin]
|
|
341
|
+
cmd = [codex_bin, *_codex_interactive_launch_flags()]
|
|
144
342
|
bootstrap_prompt = _load_client_bootstrap_prompt(CLIENT_CODEX)
|
|
145
343
|
if bootstrap_prompt and not _codex_managed_initial_messages_enabled():
|
|
146
344
|
cmd.extend(["-c", _codex_initial_messages_config(bootstrap_prompt)])
|
|
@@ -201,7 +399,7 @@ def build_followup_terminal_shell_command(
|
|
|
201
399
|
"Codex launcher not found in PATH. Install `codex` first or reconfigure NEXO."
|
|
202
400
|
)
|
|
203
401
|
target_cwd = str(Path(cwd).expanduser()) if cwd else str(Path.home())
|
|
204
|
-
cmd = [codex_bin]
|
|
402
|
+
cmd = [codex_bin, *_codex_interactive_launch_flags()]
|
|
205
403
|
bootstrap_prompt = _load_client_bootstrap_prompt(CLIENT_CODEX)
|
|
206
404
|
if bootstrap_prompt and not _codex_managed_initial_messages_enabled():
|
|
207
405
|
cmd.extend(["-c", _codex_initial_messages_config(bootstrap_prompt)])
|
|
@@ -241,6 +439,27 @@ def _resolve_runtime_model_and_effort(
|
|
|
241
439
|
return requested_model, requested_effort
|
|
242
440
|
|
|
243
441
|
|
|
442
|
+
def _backend_is_available(backend: str) -> bool:
|
|
443
|
+
if backend == CLIENT_CLAUDE_CODE:
|
|
444
|
+
return bool(_resolve_claude_cli())
|
|
445
|
+
if backend == CLIENT_CODEX:
|
|
446
|
+
return bool(_resolve_codex_cli())
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _resolve_available_backend(selected_backend: str, *, preferences: dict | None = None) -> str:
|
|
451
|
+
if _backend_is_available(selected_backend):
|
|
452
|
+
return selected_backend
|
|
453
|
+
prefs = preferences or load_client_preferences()
|
|
454
|
+
preferred = resolve_automation_backend(preferences=prefs)
|
|
455
|
+
for candidate in (preferred, CLIENT_CLAUDE_CODE, CLIENT_CODEX):
|
|
456
|
+
if candidate == selected_backend or candidate == BACKEND_NONE:
|
|
457
|
+
continue
|
|
458
|
+
if _backend_is_available(candidate):
|
|
459
|
+
return candidate
|
|
460
|
+
return selected_backend
|
|
461
|
+
|
|
462
|
+
|
|
244
463
|
def _build_codex_prompt(
|
|
245
464
|
prompt: str,
|
|
246
465
|
*,
|
|
@@ -248,7 +467,18 @@ def _build_codex_prompt(
|
|
|
248
467
|
append_system_prompt: str = "",
|
|
249
468
|
allowed_tools: str = "",
|
|
250
469
|
) -> str:
|
|
470
|
+
protocol_contract = (
|
|
471
|
+
"NEXO PROTOCOL (MANDATORY):\n"
|
|
472
|
+
"- Before non-trivial analyze/edit/execute/delegate work, call `nexo_task_open(...)`. "
|
|
473
|
+
"If that tool is unavailable, call `nexo_guard_check(...)` and `nexo_cortex_check(...)` first.\n"
|
|
474
|
+
"- For long multi-step or cross-session work, call `nexo_workflow_open(...)` and keep it updated with "
|
|
475
|
+
"`nexo_workflow_update(...)` so resume/replay use durable state instead of guesswork.\n"
|
|
476
|
+
"- If a target file has conditioned learnings or blocking guard rules, review them before any read/edit/delete step, and acknowledge guard before any edit/delete step.\n"
|
|
477
|
+
"- Do not claim done without explicit verification evidence. Close with `nexo_task_close(...)`; if unavailable, capture the change log and state the evidence explicitly.\n"
|
|
478
|
+
"- When a correction changes the canonical rule, capture or supersede the learning instead of leaving contradictory active rules behind."
|
|
479
|
+
)
|
|
251
480
|
instructions: list[str] = []
|
|
481
|
+
instructions.append(protocol_contract)
|
|
252
482
|
if append_system_prompt:
|
|
253
483
|
instructions.append(f"SYSTEM INSTRUCTIONS:\n{append_system_prompt}")
|
|
254
484
|
if output_format and output_format.lower() == "text":
|
|
@@ -269,6 +499,7 @@ def run_automation_prompt(
|
|
|
269
499
|
prompt: str,
|
|
270
500
|
*,
|
|
271
501
|
backend: str | None = None,
|
|
502
|
+
task_profile: str = "",
|
|
272
503
|
cwd: str | os.PathLike[str] | None = None,
|
|
273
504
|
env: dict | None = None,
|
|
274
505
|
model: str = "",
|
|
@@ -284,15 +515,26 @@ def run_automation_prompt(
|
|
|
284
515
|
if selected_backend == BACKEND_NONE:
|
|
285
516
|
raise AutomationBackendUnavailableError("Automation backend is disabled in config.")
|
|
286
517
|
|
|
518
|
+
if task_profile:
|
|
519
|
+
profile = resolve_automation_task_profile(task_profile, preferences=prefs)
|
|
520
|
+
selected_backend = profile["backend"] or selected_backend
|
|
521
|
+
if not model:
|
|
522
|
+
model = profile["model"]
|
|
523
|
+
if not reasoning_effort:
|
|
524
|
+
reasoning_effort = profile["reasoning_effort"]
|
|
525
|
+
selected_backend = _resolve_available_backend(selected_backend, preferences=prefs)
|
|
526
|
+
|
|
287
527
|
cwd_path = Path(cwd).expanduser().resolve() if cwd else Path.cwd()
|
|
288
528
|
run_env = _headless_env(env)
|
|
289
529
|
extra_args = list(extra_args or [])
|
|
530
|
+
requested_output_format = output_format or "text"
|
|
290
531
|
resolved_model, resolved_effort = _resolve_runtime_model_and_effort(
|
|
291
532
|
selected_backend,
|
|
292
533
|
model=model,
|
|
293
534
|
reasoning_effort=reasoning_effort,
|
|
294
535
|
preferences=prefs,
|
|
295
536
|
)
|
|
537
|
+
started_at = time.perf_counter()
|
|
296
538
|
|
|
297
539
|
if selected_backend == CLIENT_CLAUDE_CODE:
|
|
298
540
|
claude_bin = _resolve_claude_cli()
|
|
@@ -305,14 +547,13 @@ def run_automation_prompt(
|
|
|
305
547
|
cmd.extend(["--model", resolved_model])
|
|
306
548
|
if resolved_effort:
|
|
307
549
|
cmd.extend(["--effort", resolved_effort])
|
|
308
|
-
|
|
309
|
-
cmd.extend(["--output-format", output_format])
|
|
550
|
+
cmd.extend(["--output-format", "json"])
|
|
310
551
|
if append_system_prompt:
|
|
311
552
|
cmd.extend(["--append-system-prompt", append_system_prompt])
|
|
312
553
|
if allowed_tools:
|
|
313
554
|
cmd.extend(["--allowedTools", allowed_tools])
|
|
314
555
|
cmd.extend(extra_args)
|
|
315
|
-
|
|
556
|
+
result = subprocess.run(
|
|
316
557
|
cmd,
|
|
317
558
|
cwd=str(cwd_path),
|
|
318
559
|
capture_output=True,
|
|
@@ -320,6 +561,31 @@ def run_automation_prompt(
|
|
|
320
561
|
timeout=timeout,
|
|
321
562
|
env=run_env,
|
|
322
563
|
)
|
|
564
|
+
final_stdout, telemetry = _extract_claude_telemetry(
|
|
565
|
+
result.stdout or "",
|
|
566
|
+
requested_output_format=requested_output_format,
|
|
567
|
+
)
|
|
568
|
+
recorded, record_error = _record_automation_run(
|
|
569
|
+
backend=selected_backend,
|
|
570
|
+
task_profile=task_profile,
|
|
571
|
+
model=resolved_model,
|
|
572
|
+
reasoning_effort=resolved_effort,
|
|
573
|
+
cwd=cwd_path,
|
|
574
|
+
output_format=requested_output_format,
|
|
575
|
+
prompt=prompt,
|
|
576
|
+
returncode=result.returncode,
|
|
577
|
+
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
|
578
|
+
telemetry=telemetry,
|
|
579
|
+
)
|
|
580
|
+
stderr = result.stderr or ""
|
|
581
|
+
if not recorded:
|
|
582
|
+
stderr = _append_stderr(stderr, record_error)
|
|
583
|
+
return subprocess.CompletedProcess(
|
|
584
|
+
cmd,
|
|
585
|
+
result.returncode,
|
|
586
|
+
final_stdout,
|
|
587
|
+
stderr,
|
|
588
|
+
)
|
|
323
589
|
|
|
324
590
|
if selected_backend == CLIENT_CODEX:
|
|
325
591
|
codex_bin = _resolve_codex_cli()
|
|
@@ -335,6 +601,7 @@ def run_automation_prompt(
|
|
|
335
601
|
"--skip-git-repo-check",
|
|
336
602
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
337
603
|
"--ephemeral",
|
|
604
|
+
"--json",
|
|
338
605
|
"-C",
|
|
339
606
|
str(cwd_path),
|
|
340
607
|
"-o",
|
|
@@ -364,12 +631,33 @@ def run_automation_prompt(
|
|
|
364
631
|
timeout=timeout,
|
|
365
632
|
env=run_env,
|
|
366
633
|
)
|
|
367
|
-
|
|
634
|
+
raw_stdout = result.stdout or ""
|
|
635
|
+
stdout = output_path.read_text() if output_path.exists() else raw_stdout
|
|
636
|
+
final_stdout, telemetry = _extract_codex_telemetry(
|
|
637
|
+
raw_stdout,
|
|
638
|
+
final_stdout=stdout,
|
|
639
|
+
model=resolved_model,
|
|
640
|
+
)
|
|
641
|
+
recorded, record_error = _record_automation_run(
|
|
642
|
+
backend=selected_backend,
|
|
643
|
+
task_profile=task_profile,
|
|
644
|
+
model=resolved_model,
|
|
645
|
+
reasoning_effort=resolved_effort,
|
|
646
|
+
cwd=cwd_path,
|
|
647
|
+
output_format=requested_output_format,
|
|
648
|
+
prompt=prompt,
|
|
649
|
+
returncode=result.returncode,
|
|
650
|
+
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
|
651
|
+
telemetry=telemetry,
|
|
652
|
+
)
|
|
653
|
+
stderr = result.stderr or ""
|
|
654
|
+
if not recorded:
|
|
655
|
+
stderr = _append_stderr(stderr, record_error)
|
|
368
656
|
return subprocess.CompletedProcess(
|
|
369
657
|
cmd,
|
|
370
658
|
result.returncode,
|
|
371
|
-
|
|
372
|
-
|
|
659
|
+
final_stdout,
|
|
660
|
+
stderr,
|
|
373
661
|
)
|
|
374
662
|
|
|
375
663
|
raise AutomationBackendUnavailableError(f"Unsupported automation backend: {selected_backend}")
|