Skip to content

Commit cde7ee1

Browse files
committed
spec01: trace artifacts
1 parent bce3711 commit cde7ee1

10 files changed

Lines changed: 891 additions & 53 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
# Logs
55
logs/
6+
!visualizer/src/app/api/logs/
7+
!visualizer/src/app/api/logs/route.ts
68

79
# Byte-compiled / optimized / DLL files
810
__pycache__/

‎docs/specs/01-trace-artifacts.md‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Spec: Trace Artifacts (Notebook + Markdown)
2+
3+
## Summary
4+
Status: Implemented.
5+
6+
Provide two trace artifacts for each RLM run:
7+
- A file-based `.trace.ipynb` notebook in `logs/` that mirrors the JSONL trajectory.
8+
- A markdown trace exposed on `RLMChatCompletion.trace_markdown`.
9+
10+
## Goals
11+
- Keep a durable, replayable notebook artifact per run.
12+
- Provide a lightweight, notebook-friendly markdown trace for quick inspection.
13+
- Preserve trace availability outside Jupyter (plain markdown string).
14+
15+
## Non-Goals
16+
- No live rendering or auto-refresh in the notebook UI.
17+
- No in-notebook UI components or extensions.
18+
- No changes to iteration log format.
19+
20+
## Source of Truth
21+
- JSONL trajectory log written by `RLMLogger`.
22+
- Entry types used:
23+
- `metadata`
24+
- `run_context` (prompt + root_prompt + session_mode)
25+
- `iteration`
26+
27+
## Trace Markdown on Result
28+
### API Changes
29+
- `RLMChatCompletion` adds `trace_markdown: str`.
30+
- `RLM` maintains `trace_markdown_history: str` for each `completion()` run.
31+
- `RLMSession` accumulates `trace_markdown_history: str` across chat turns.
32+
33+
### Trace Format
34+
- Run header with prompt/root_prompt as JSON.
35+
- Iterations include model response, code blocks, stdout/stderr, and sub-LM calls.
36+
- Each run is separated by a markdown horizontal rule (`---`).
37+
38+
### Workflow
39+
1. On `RLM.completion()` start, capture the run context (prompt, root_prompt, session_mode, environment kwargs).
40+
2. Accumulate `RLMIteration` objects as the run proceeds.
41+
3. At completion, build a markdown trace for the run and set `trace_markdown_history` for that run.
42+
4. Return `RLMChatCompletion(trace_markdown=trace_markdown_history)`.
43+
44+
## Trace Notebook File
45+
### Notebook Structure
46+
1. Title cell (markdown)
47+
2. Resume cell (code)
48+
3. Optional metadata cell(s)
49+
4. Per-iteration cells:
50+
- Markdown cell with iteration header + model response
51+
- Code cell for each ```repl``` block
52+
- Outputs attached to code cell (stdout/stderr)
53+
- Optional markdown cell for nested `llm_query` summaries
54+
55+
### Cell Tagging
56+
Use tags for filtering:
57+
- Title: `["rlm", "rlm-title"]`
58+
- Resume: `["rlm", "rlm-resume"]`
59+
- Metadata: `["rlm", "rlm-metadata"]`
60+
- Iteration response: `["rlm", "iteration-N", "role-assistant"]`
61+
- Code cells: `["rlm", "iteration-N", "role-code"]`
62+
- Subcalls: `["rlm", "iteration-N", "role-llm-calls"]`
63+
64+
### Resume Cell Behavior
65+
- When `run_context` is logged, inject:
66+
- `completion_context` for completion runs, or `session_context_0` for session runs.
67+
- `root_prompt` rehydrated from JSON.
68+
- Session runs also initialize `context_history` and `session_history` in the resume cell.
69+
- If the prompt is not JSON-serializable, include a comment and fall back to `repr(...)`.
70+
- Replay support:
71+
- `llm_query` and `llm_query_batched` replay recorded subcalls by prompt match.
72+
- Raises if a prompt is not found in the replay map.
73+
- Live `llm_query` calls are not implemented in trace notebooks.
74+
75+
### Failure & Fallback
76+
- If `nbformat` is missing: raise a clear error.
77+
- If JSONL is malformed: skip that entry.
78+
- If multiple runs share a logger: each `run_context` updates the resume cell (or add a run boundary cell in the future).
79+
80+
## Session History Behavior
81+
- When using `RLMSession.chat()`, the context payload passed to the REPL is the current prompt (string or dict).
82+
- Prompt-level history is maintained in the LM message history.
83+
- If `persistent=True`, histories are stored in the REPL after each run as entries in `session_history`.
84+
- Assistant responses are always strings; `FINAL_VAR(...)` values are coerced to string.
85+
86+
## Public API
87+
- `enable_trace_notebook(log_dir, file_name="rlm", notebook_path=None, poll_interval=0.25) -> (RLMLogger, TraceWriterHandle)`
88+
- `start_trace_writer(log_path, notebook_path, poll_interval=0.25)`
89+
- `stop_trace_writer(handle)`
90+
91+
## Files
92+
- `rlm/utils/trace_notebook.py` (implementation)
93+
- `rlm/utils/trace_markdown.py` (trace builder)
94+
- `rlm/core/rlm.py` (trace accumulation + return)
95+
- `rlm/core/types.py` (trace_markdown field)

‎rlm/core/rlm.py‎

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
build_user_prompt,
2929
)
3030
from rlm.utils.rlm_utils import filter_sensitive_keys
31+
from rlm.utils.trace_markdown import build_trace_markdown
3132

3233

3334
class RLM:
@@ -100,8 +101,7 @@ def __init__(
100101
self.system_prompt = self.system_prompt_session
101102
self.logger = logger
102103
self.verbose = VerbosePrinter(enabled=verbose)
103-
104-
# Persistence support
104+
self.trace_markdown_history = ""
105105
self.persistent = persistent
106106
self._persistent_env: SupportsPersistence | None = None
107107

@@ -226,12 +226,15 @@ def completion(
226226
Returns:
227227
A final answer as a string.
228228
"""
229-
completion, _ = self._run_completion(
229+
self.trace_markdown_history = ""
230+
completion, _, trace_history = self._run_completion(
230231
prompt=prompt,
231232
root_prompt=root_prompt,
232233
message_history=None,
234+
trace_history="",
233235
session_mode=False,
234236
)
237+
self.trace_markdown_history = trace_history
235238
return completion
236239

237240
def _run_completion(
@@ -240,19 +243,38 @@ def _run_completion(
240243
prompt: str | dict[str, Any],
241244
root_prompt: str | None,
242245
message_history: list[dict[str, Any]] | None,
246+
trace_history: str,
243247
session_mode: bool,
244-
) -> tuple[RLMChatCompletion, list[dict[str, Any]]]:
248+
) -> tuple[RLMChatCompletion, list[dict[str, Any]], str]:
245249
time_start = time.perf_counter()
246250

251+
run_context_entry = None
252+
if self.logger:
253+
run_context_entry = self.logger.log_run_context(
254+
prompt=prompt,
255+
root_prompt=root_prompt,
256+
environment_type=self.environment_type,
257+
environment_kwargs=filter_sensitive_keys(self.environment_kwargs),
258+
session_mode=session_mode,
259+
)
260+
261+
run_context = run_context_entry or {
262+
"prompt": prompt,
263+
"root_prompt": root_prompt,
264+
"environment_type": self.environment_type,
265+
"environment_kwargs": filter_sensitive_keys(self.environment_kwargs),
266+
"session_mode": session_mode,
267+
}
268+
247269
# If we're at max depth, the RLM is an LM, so we fallback to the regular LM.
248270
if self.depth >= self.max_depth:
249271
client: BaseLM = get_client(self.backend, self.backend_kwargs)
250272
fallback_start = time.perf_counter()
251273
response = client.completion(prompt)
252274
fallback_end = time.perf_counter()
253275
usage = client.get_last_usage()
254-
if message_history is None:
255-
message_history = self._setup_prompt(prompt, session_mode=session_mode)
276+
run_trace = build_trace_markdown(run_context, [])
277+
trace_history = append_trace_history(trace_history, run_trace)
256278
completion = RLMChatCompletion(
257279
root_model=self.backend_kwargs.get("model_name", "unknown")
258280
if self.backend_kwargs
@@ -261,15 +283,19 @@ def _run_completion(
261283
response=response,
262284
usage_summary=usage,
263285
execution_time=fallback_end - fallback_start,
286+
trace_markdown=trace_history,
264287
)
265-
return completion, message_history
288+
if message_history is None:
289+
message_history = self._setup_prompt(prompt, session_mode=session_mode)
290+
return completion, message_history, trace_history
266291

267292
with self._spawn_completion_context(
268293
prompt,
269294
session_mode=session_mode,
270295
) as (lm_handler, environment):
271296
if message_history is None:
272297
message_history = self._setup_prompt(prompt, session_mode=session_mode)
298+
iterations: list[RLMIteration] = []
273299

274300
for i in range(self.max_iterations):
275301
# Current prompt = message history + additional prompt suffix
@@ -289,10 +315,13 @@ def _run_completion(
289315
prompt=current_prompt,
290316
lm_handler=lm_handler,
291317
environment=environment,
318+
iteration_index=i + 1,
292319
)
320+
iterations.append(iteration)
293321

294322
# Check if RLM is done and has a final answer.
295-
final_answer = find_final_answer(iteration.response, environment=environment)
323+
raw_final_answer = find_final_answer(iteration.response, environment=environment)
324+
final_answer = raw_final_answer or None
296325
iteration.final_answer = final_answer
297326

298327
# If logger is used, log the iteration.
@@ -316,6 +345,8 @@ def _run_completion(
316345
):
317346
environment.add_history(message_history)
318347

348+
run_trace = build_trace_markdown(run_context, iterations)
349+
trace_history = append_trace_history(trace_history, run_trace)
319350
completion = RLMChatCompletion(
320351
root_model=self.backend_kwargs.get("model_name", "unknown")
321352
if self.backend_kwargs
@@ -324,8 +355,9 @@ def _run_completion(
324355
response=final_answer,
325356
usage_summary=usage,
326357
execution_time=time_end - time_start,
358+
trace_markdown=trace_history,
327359
)
328-
return completion, message_history
360+
return completion, message_history, trace_history
329361

330362
# Format the iteration for the next prompt.
331363
new_messages = format_iteration(iteration)
@@ -335,15 +367,19 @@ def _run_completion(
335367

336368
# Default behavior: we run out of iterations, provide one final answer
337369
time_end = time.perf_counter()
338-
final_answer = self._default_answer(message_history, lm_handler)
370+
default_iteration = self._default_answer(message_history, lm_handler)
371+
iterations.append(default_iteration)
372+
message_history.extend(format_iteration(default_iteration))
373+
final_answer = default_iteration.final_answer or ""
339374
usage = lm_handler.get_usage_summary()
340375
self.verbose.print_final_answer(final_answer)
341376
self.verbose.print_summary(self.max_iterations, time_end - time_start, usage.to_dict())
342-
343377
# Store message history in persistent environment
344378
if session_mode and self.persistent and isinstance(environment, SupportsPersistence):
345379
environment.add_history(message_history)
346380

381+
run_trace = build_trace_markdown(run_context, iterations)
382+
trace_history = append_trace_history(trace_history, run_trace)
347383
completion = RLMChatCompletion(
348384
root_model=self.backend_kwargs.get("model_name", "unknown")
349385
if self.backend_kwargs
@@ -352,14 +388,16 @@ def _run_completion(
352388
response=final_answer,
353389
usage_summary=usage,
354390
execution_time=time_end - time_start,
391+
trace_markdown=trace_history,
355392
)
356-
return completion, message_history
393+
return completion, message_history, trace_history
357394

358395
def _completion_turn(
359396
self,
360397
prompt: str | dict[str, Any],
361398
lm_handler: LMHandler,
362399
environment: BaseEnv,
400+
iteration_index: int,
363401
) -> RLMIteration:
364402
"""
365403
Perform a single iteration of the RLM, including prompting the model
@@ -382,7 +420,9 @@ def _completion_turn(
382420
iteration_time=iteration_time,
383421
)
384422

385-
def _default_answer(self, message_history: list[dict[str, Any]], lm_handler: LMHandler) -> str:
423+
def _default_answer(
424+
self, message_history: list[dict[str, Any]], lm_handler: LMHandler
425+
) -> RLMIteration:
386426
"""
387427
Default behavior if the RLM runs out of iterations and does not find a final answer.
388428
It will take the message history, and try to generate a final answer from it.
@@ -394,26 +434,17 @@ def _default_answer(self, message_history: list[dict[str, Any]], lm_handler: LMH
394434
}
395435
]
396436
response = lm_handler.completion(current_prompt)
437+
iteration = RLMIteration(
438+
prompt=current_prompt,
439+
response=response,
440+
final_answer=response,
441+
code_blocks=[],
442+
)
397443

398444
if self.logger:
399-
self.logger.log(
400-
RLMIteration(
401-
prompt=current_prompt,
402-
response=response,
403-
final_answer=response,
404-
code_blocks=[],
405-
)
406-
)
445+
self.logger.log(iteration)
407446

408-
return response
409-
410-
def _fallback_answer(self, message: str | dict[str, Any]) -> str:
411-
"""
412-
Fallback behavior if the RLM is actually at max depth, and should be treated as an LM.
413-
"""
414-
client: BaseLM = get_client(self.backend, self.backend_kwargs)
415-
response = client.completion(message)
416-
return response
447+
return iteration
417448

418449
def start_session(self) -> "RLMSession":
419450
return RLMSession(self)
@@ -468,13 +499,21 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
468499
return False
469500

470501

502+
def append_trace_history(existing: str, new_trace: str) -> str:
503+
if not existing:
504+
return new_trace
505+
return f"{existing}\n\n---\n\n{new_trace}"
506+
507+
471508
class RLMSession:
472509
def __init__(self, rlm: RLM):
473510
self.rlm = rlm
474511
self.message_history: list[dict[str, Any]] | None = None
512+
self.trace_markdown_history = ""
475513

476514
def reset(self) -> None:
477515
self.message_history = None
516+
self.trace_markdown_history = ""
478517

479518
def chat(
480519
self, prompt: str | dict[str, Any], root_prompt: str | None = None
@@ -489,11 +528,13 @@ def chat(
489528
)[1]
490529
self.message_history.append(metadata_prompt)
491530

492-
completion, message_history = self.rlm._run_completion(
531+
completion, message_history, trace_history = self.rlm._run_completion(
493532
prompt=prompt_payload,
494533
root_prompt=root_prompt,
495534
message_history=self.message_history,
535+
trace_history=self.trace_markdown_history,
496536
session_mode=True,
497537
)
498538
self.message_history = message_history
539+
self.trace_markdown_history = trace_history
499540
return completion

0 commit comments

Comments
 (0)