pub struct RunLogContext { /* private fields */ }Expand description
Context for managing per-run log directories and files.
This struct owns the run_id and provides path resolution for all logs
from a single Ralph invocation. All logs are grouped under a per-run
directory (.agent/logs-<run_id>/) for easy sharing and diagnosis.
§Design Rationale
Why per-run directories?
- Shareability: All logs from a run can be shared as a single tarball
- Resume continuity:
--resumecontinues logging to the same directory - Isolation: Multiple concurrent runs don’t interfere with each other
- Organization: Chronological sorting is natural (lexicographic sort)
Why not scatter logs across .agent/logs/, .agent/tmp/, etc?
- Hard to identify which logs belong to which run
- Difficult to share logs for debugging
- Resume would create fragmented log artifacts
- Log rotation and cleanup become complex
§Integration with Checkpoint/Resume
The run_id is stored in the checkpoint (.agent/checkpoint.json) so that
--resume can continue logging to the same directory. This ensures:
- Logs from the original run and resumed run are in one place
- Event loop sequence numbers continue from where they left off
- Pipeline log is appended (not overwritten)
§Architecture Compliance
This struct is created once per run in the impure layer (effect handlers)
and passed to all effect handlers via PhaseContext. It must never be used
in reducers or orchestrators (which are pure).
All filesystem operations go through the Workspace trait (never std::fs
in pipeline code) to support both WorkspaceFs (production) and
MemoryWorkspace (tests).
§Future Extensibility
The per-run directory structure includes reserved subdirectories for future use:
provider/: Provider streaming logs (infrastructure exists, not yet used)debug/: Future debug artifacts (e.g., memory dumps, profiling data)
§Examples
§Fresh run
use ralph_workflow::logging::RunLogContext;
use ralph_workflow::workspace::WorkspaceFs;
use std::path::PathBuf;
let workspace = WorkspaceFs::new(PathBuf::from("."));
let ctx = RunLogContext::new(&workspace)?;
// Get log paths
let pipeline_log = ctx.pipeline_log(); // .agent/logs-2026-02-06_14-03-27.123Z/pipeline.log
let agent_log = ctx.agent_log("planning", 1, None); // .agent/logs-.../agents/planning_1.log§Resume
use ralph_workflow::logging::RunLogContext;
use ralph_workflow::workspace::WorkspaceFs;
use std::path::PathBuf;
let workspace = WorkspaceFs::new(PathBuf::from("."));
let run_id = "2026-02-06_14-03-27.123Z"; // From checkpoint
let ctx = RunLogContext::from_checkpoint(run_id, &workspace)?;
// Logs will append to existing files in the same run directory
let pipeline_log = ctx.pipeline_log();Implementations§
Source§impl RunLogContext
impl RunLogContext
Sourcepub fn new(workspace: &dyn Workspace) -> Result<Self>
pub fn new(workspace: &dyn Workspace) -> Result<Self>
Create a new RunLogContext with collision handling.
Generates a new run_id and creates the run directory structure. If directory exists, tries collision counter variants (rare case of multiple runs starting in the same millisecond).
Creates subdirectories:
.agent/logs-<run_id>/agents/for per-agent logs.agent/logs-<run_id>/provider/for provider streaming logs.agent/logs-<run_id>/debug/for future debug artifacts
§Collision Handling
The collision handling loop tries counter values 0-99:
- Counter 0: Uses the base run_id (no suffix)
- Counter 1-99: Appends
-01through-99suffixes
§TOCTOU Race Condition Handling
To avoid the time-of-check-to-time-of-use race condition, we:
- First check if the directory exists (fast path for common case)
- If it doesn’t exist, try to create it
- If creation succeeds but the directory still doesn’t exist afterward, another process may have created it, so we try the next collision variant
- We use the presence of the “agents” subdirectory as our “created” marker
Note: If a base directory exists that was actually created as a collision directory (e.g., due to a bug), the system will still work correctly by creating the next collision variant. This is acceptable because the directory naming format is deterministic and we always check for existence before creating.
Sourcepub fn from_checkpoint(run_id: &str, workspace: &dyn Workspace) -> Result<Self>
pub fn from_checkpoint(run_id: &str, workspace: &dyn Workspace) -> Result<Self>
Create a RunLogContext from an existing checkpoint (for resume).
Uses the timestamp-based log run ID from the checkpoint (stored in
PipelineCheckpoint.log_run_id) to continue logging to the same run
directory. This is distinct from the UUID-based run_id field in the
checkpoint which identifies the execution session.
If the directory doesn’t exist (e.g., deleted), it is recreated.
Sourcepub fn for_testing(
base_run_id: RunId,
workspace: &dyn Workspace,
) -> Result<Self>
pub fn for_testing( base_run_id: RunId, workspace: &dyn Workspace, ) -> Result<Self>
Test-only helper to create a RunLogContext with a fixed run_id.
This allows testing the collision handling logic by providing a predictable run_id that can be pre-created on the filesystem to simulate collisions.
§Warning
This is intended for testing only. Using a fixed run_id in production
could lead to directory collisions. Always use RunLogContext::new
or RunLogContext::from_checkpoint in production code.
§Examples
use ralph_workflow::logging::{RunId, RunLogContext};
// Create a fixed run_id for testing
let fixed_id = RunId::for_test("2026-02-06_14-03-27.123Z");
let ctx = RunLogContext::for_testing(fixed_id, &workspace)?;Sourcepub fn run_id(&self) -> &RunId
pub fn run_id(&self) -> &RunId
Get a reference to the run ID.
This is the timestamp-based log run ID (format: YYYY-MM-DD_HH-mm-ss.SSSZ[-NN])
used for naming the per-run log directory. It is distinct from the UUID-based
run_id field stored in PipelineCheckpoint, which uniquely identifies the
execution session.
Sourcepub fn pipeline_log(&self) -> PathBuf
pub fn pipeline_log(&self) -> PathBuf
Get the path to the pipeline log file.
Sourcepub fn event_loop_log(&self) -> PathBuf
pub fn event_loop_log(&self) -> PathBuf
Get the path to the event loop log file.
Sourcepub fn event_loop_trace(&self) -> PathBuf
pub fn event_loop_trace(&self) -> PathBuf
Get the path to the event loop trace file (crash-only).
Sourcepub fn agent_log(
&self,
phase: &str,
index: u32,
attempt: Option<u32>,
) -> PathBuf
pub fn agent_log( &self, phase: &str, index: u32, attempt: Option<u32>, ) -> PathBuf
Get the path to an agent log file.
§Arguments
phase- Phase name (e.g., “planning”, “dev”, “reviewer”, “commit”)index- Invocation index within the phase (1-based)attempt- Optional retry attempt counter (1 for first retry, 2 for second retry, etc.; None for initial attempt with no retries)
§Returns
Path like .agent/logs-<run_id>/agents/planning_1.log or
.agent/logs-<run_id>/agents/dev_2_a1.log for retries.
Sourcepub fn provider_log(&self, name: &str) -> PathBuf
pub fn provider_log(&self, name: &str) -> PathBuf
Sourcepub fn run_metadata(&self) -> PathBuf
pub fn run_metadata(&self) -> PathBuf
Get the path to the run metadata file (run.json).
Sourcepub fn write_run_metadata(
&self,
workspace: &dyn Workspace,
metadata: &RunMetadata,
) -> Result<()>
pub fn write_run_metadata( &self, workspace: &dyn Workspace, metadata: &RunMetadata, ) -> Result<()>
Write run.json metadata file.
This should be called early in pipeline execution to record essential metadata for debugging and tooling.
Auto Trait Implementations§
impl Freeze for RunLogContext
impl RefUnwindSafe for RunLogContext
impl Send for RunLogContext
impl Sync for RunLogContext
impl Unpin for RunLogContext
impl UnwindSafe for RunLogContext
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more