Skip to content

Commit 7febc90

Browse files
committed
Add headless trace artifacts and runtime diagnostics
1 parent 0167689 commit 7febc90

3 files changed

Lines changed: 153 additions & 4 deletions

File tree

minicode/headless.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,40 @@
1616

1717
from __future__ import annotations
1818

19+
import json
1920
import os
2021
import sys
2122
from pathlib import Path
2223

2324

25+
def _write_headless_messages_trace(
26+
trace_path: str | None,
27+
*,
28+
cwd: str,
29+
prompt: str,
30+
runtime: dict | None,
31+
result_messages: list[dict] | None,
32+
response_text: str | None,
33+
error_text: str | None = None,
34+
) -> None:
35+
if not trace_path:
36+
return
37+
payload = {
38+
"cwd": cwd,
39+
"prompt": prompt,
40+
"model": (runtime or {}).get("model"),
41+
"messages": result_messages or [],
42+
"assistant_response": response_text,
43+
"error": error_text,
44+
}
45+
path = Path(trace_path)
46+
path.parent.mkdir(parents=True, exist_ok=True)
47+
path.write_text(
48+
json.dumps(payload, ensure_ascii=False, indent=2, default=str),
49+
encoding="utf-8",
50+
)
51+
52+
2453
def run_headless(prompt: str | None = None) -> str:
2554
"""Run a single agent turn in headless mode and return the response.
2655
@@ -91,6 +120,7 @@ def run_headless(prompt: str | None = None) -> str:
91120
]
92121

93122
logger.info("Headless run: %s", prompt[:80])
123+
trace_output_path = os.environ.get("MINI_CODE_HEADLESS_MESSAGES_OUT", "").strip() or None
94124

95125
try:
96126
result_messages = run_agent_turn(
@@ -107,11 +137,30 @@ def run_headless(prompt: str | None = None) -> str:
107137
(m for m in reversed(result_messages) if m["role"] == "assistant"),
108138
None,
109139
)
110-
return last_assistant["content"] if last_assistant else "(no response)"
140+
response_text = last_assistant["content"] if last_assistant else "(no response)"
141+
_write_headless_messages_trace(
142+
trace_output_path,
143+
cwd=cwd,
144+
prompt=prompt,
145+
runtime=runtime,
146+
result_messages=result_messages,
147+
response_text=response_text,
148+
)
149+
return response_text
111150

112151
except Exception as exc: # noqa: BLE001
113152
logger.error("Headless error: %s", exc)
114-
return f"Error: {exc}"
153+
response_text = f"Error: {exc}"
154+
_write_headless_messages_trace(
155+
trace_output_path,
156+
cwd=cwd,
157+
prompt=prompt,
158+
runtime=runtime,
159+
result_messages=[],
160+
response_text=response_text,
161+
error_text=str(exc),
162+
)
163+
return response_text
115164
finally:
116165
try:
117166
tools.dispose()

py-src/minicode/headless.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,40 @@
1616

1717
from __future__ import annotations
1818

19+
import json
1920
import os
2021
import sys
2122
from pathlib import Path
2223

2324

25+
def _write_headless_messages_trace(
26+
trace_path: str | None,
27+
*,
28+
cwd: str,
29+
prompt: str,
30+
runtime: dict | None,
31+
result_messages: list[dict] | None,
32+
response_text: str | None,
33+
error_text: str | None = None,
34+
) -> None:
35+
if not trace_path:
36+
return
37+
payload = {
38+
"cwd": cwd,
39+
"prompt": prompt,
40+
"model": (runtime or {}).get("model"),
41+
"messages": result_messages or [],
42+
"assistant_response": response_text,
43+
"error": error_text,
44+
}
45+
path = Path(trace_path)
46+
path.parent.mkdir(parents=True, exist_ok=True)
47+
path.write_text(
48+
json.dumps(payload, ensure_ascii=False, indent=2, default=str),
49+
encoding="utf-8",
50+
)
51+
52+
2453
def run_headless(prompt: str | None = None) -> str:
2554
"""Run a single agent turn in headless mode and return the response.
2655
@@ -103,6 +132,7 @@ def run_headless(prompt: str | None = None) -> str:
103132
max_steps = max(1, int(raw_max_steps))
104133
except ValueError:
105134
max_steps = 50
135+
trace_output_path = os.environ.get("MINI_CODE_HEADLESS_MESSAGES_OUT", "").strip() or None
106136

107137
try:
108138
result_messages = run_agent_turn(
@@ -112,18 +142,38 @@ def run_headless(prompt: str | None = None) -> str:
112142
cwd=cwd,
113143
permissions=permissions,
114144
max_steps=max_steps,
145+
runtime=runtime,
115146
)
116147

117148
# Extract last assistant message
118149
last_assistant = next(
119150
(m for m in reversed(result_messages) if m["role"] == "assistant"),
120151
None,
121152
)
122-
return last_assistant["content"] if last_assistant else "(no response)"
153+
response_text = last_assistant["content"] if last_assistant else "(no response)"
154+
_write_headless_messages_trace(
155+
trace_output_path,
156+
cwd=cwd,
157+
prompt=prompt,
158+
runtime=runtime,
159+
result_messages=result_messages,
160+
response_text=response_text,
161+
)
162+
return response_text
123163

124164
except Exception as exc: # noqa: BLE001
125165
logger.error("Headless error: %s", exc)
126-
return f"Error: {exc}"
166+
response_text = f"Error: {exc}"
167+
_write_headless_messages_trace(
168+
trace_output_path,
169+
cwd=cwd,
170+
prompt=prompt,
171+
runtime=runtime,
172+
result_messages=[],
173+
response_text=response_text,
174+
error_text=str(exc),
175+
)
176+
return response_text
127177
finally:
128178
try:
129179
tools.dispose()

tests/test_headless.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
from pathlib import Path
45

56
from minicode.tooling import ToolRegistry
@@ -121,3 +122,52 @@ def test_run_headless_provider_failure_uses_runtime_channel_details(
121122
assert "Active channel: anthropic-compatible via baseUrl/authToken." in response
122123
assert "Next step: Primary runtime is using a single anthropic-compatible channel from baseUrl/authToken." in response
123124

125+
126+
def test_run_headless_writes_messages_trace_when_requested(monkeypatch, tmp_path: Path) -> None:
127+
import minicode.headless
128+
129+
runtime = {
130+
"model": "deepseek-v4-pro[1m]",
131+
"baseUrl": "https://openai-proxy.example/v1",
132+
"authToken": "test-token",
133+
}
134+
trace_path = tmp_path / "artifacts" / "messages.json"
135+
136+
monkeypatch.chdir(tmp_path)
137+
monkeypatch.setenv("MINI_CODE_HEADLESS_MESSAGES_OUT", str(trace_path))
138+
monkeypatch.setattr(
139+
"minicode.config.load_runtime_config",
140+
lambda cwd: runtime,
141+
)
142+
monkeypatch.setattr(
143+
"minicode.tools.create_default_tool_registry",
144+
lambda cwd, runtime=None: ToolRegistry([]),
145+
)
146+
monkeypatch.setattr("minicode.permissions.PermissionManager", _DummyPermissions)
147+
monkeypatch.setattr("minicode.memory.MemoryManager", _DummyMemoryManager)
148+
monkeypatch.setattr(
149+
"minicode.prompt.build_system_prompt",
150+
lambda cwd, permissions, context: "sys",
151+
)
152+
monkeypatch.setattr(
153+
"minicode.model_registry.create_model_adapter",
154+
lambda model, tools, runtime=None: object(),
155+
)
156+
monkeypatch.setattr(
157+
"minicode.agent_loop.run_agent_turn",
158+
lambda **kwargs: [
159+
{"role": "assistant", "content": "traceable"},
160+
{"role": "tool", "content": "python -m unittest"},
161+
],
162+
)
163+
164+
response = minicode.headless.run_headless("Run the visible tests.")
165+
166+
assert response == "traceable"
167+
payload = json.loads(trace_path.read_text(encoding="utf-8"))
168+
assert payload["cwd"] == str(tmp_path)
169+
assert payload["prompt"] == "Run the visible tests."
170+
assert payload["model"] == "deepseek-v4-pro[1m]"
171+
assert payload["assistant_response"] == "traceable"
172+
assert payload["error"] is None
173+
assert payload["messages"][0]["role"] == "assistant"

0 commit comments

Comments
 (0)