8.9 KiB
Plan: Events as Source of Truth
Make all run directory key data derivable from events in progress.jsonl.
Summary of changes
Clarified derivation rules
- Envelope fields count as event sources. Any file field may be sourced from event envelope metadata (
run_id,ts,node_id,node_label) as well asproperties. - When a file is reconstructed from multiple events, the derivation should be documented explicitly in
events.mdandrun-directory-keys.md; consumers should not need to infer cross-event joins. retro.jsonis reconstructed from multiple events:run.startedprovidesworkflow_nameandgoal,retro.completedprovides the parsedretropayload, and the retro event envelope provides the completion timestamp.conclusion.json.final_git_commit_shais normalized from the terminal run event:run.completed.properties.final_git_commit_shaon success/partial success,run.failed.properties.git_commit_shaon failure.
New events
| Event | Purpose |
|---|---|
run.created |
Full run definition (settings, graph, workflow source/config, labels, paths). Emitted at end of CREATE operation, before START. |
command.started |
Command node invocation: script, language, timeout_ms |
command.completed |
Command node result: stdout, stderr, exit_code, duration_ms, timed_out |
agent.cli.started |
CLI-backend LLM invocation: mode, provider, model, command |
agent.cli.completed |
CLI-backend LLM result: stdout, stderr |
Enriched events
| Event | New fields |
|---|---|
stage.started |
Remove script (moved to command.started), make handler_type non-optional |
stage.completed |
context_updates, jump_to_node, context_values, node_visits, loop_failure_signatures, restart_failure_signatures, response |
sandbox.initialized |
provider, identifier, host_working_directory, container_mount_point |
checkpoint.completed |
diff |
parallel.branch.completed |
head_sha |
agent.session.started |
mode, provider, model |
stage.prompt |
mode, provider, model |
retro.started |
prompt, provider, model |
retro.completed |
response, retro (full Retro struct) |
Already covered (no changes needed)
| Key | Event source |
|---|---|
start.json |
run.started |
nodes/{node_id}/prompt.md |
stage.prompt |
nodes/{node_id}/status.json |
stage.completed |
retro/status.json |
retro.completed / retro.failed |
conclusion.json |
run.completed / run.failed + aggregation from stage.completed |
checkpoints/*.json |
Same as checkpoint.json — derived from stage.completed replay |
Detailed decisions
1–2. _init.json + run.json — new run.created event
Decision: Emit a run.created event at the end of the CREATE operation (before START). Carries everything needed to persist the run:
- From
_init.json:created_at,db_prefix,run_dir - From
run.json:settings,graph,workflow_slug,working_directory,host_repo_path,base_branch,labels - From
workflow.fabro/workflow.toml:workflow_source(raw dot text),workflow_config(raw TOML text)
The existing run.started stays lightweight — it signals execution has begun. The CREATE→START boundary is: run.created persists the run definition, run.started marks execution start.
Derivation details:
_init.json.run_idandrun.json.run_idcome fromrun.createdenveloperun_id_init.json.created_atandrun.json.created_atcome fromrun.createdenvelopets
3. checkpoint.json — internal engine state
Decision: Add context_updates, jump_to_node, context_values, node_visits, loop_failure_signatures, and restart_failure_signatures to the stage.completed event properties. All checkpoint fields become derivable from replaying stage events.
New fields on stage.completed:
context_updates— map of context key → JSON value set by this stagejump_to_node— non-edge jump target (optional)context_values— full accumulated context map after this stagenode_visits— map of node id → visit count after this stageloop_failure_signatures— failure signature → count after this stagerestart_failure_signatures— failure signature → count after this stage
Derivation details:
checkpoint.json.timestampcomes from the correspondingcheckpoint.completedenvelopetscheckpoints/{seq:04}-{epoch_ms}.jsonuses the same derivation ascheckpoint.json, with snapshot timestamp taken from eachcheckpoint.completedenvelopets
4. retro.json — LLM-generated retro content
Decision: Embed the full Retro struct as a retro property on the retro.completed event.
Derivation details:
retro.json.run_idcomes from theretro.completedenveloperun_idretro.json.workflow_namecomes fromrun.started.properties.nameretro.json.goalcomes fromrun.started.properties.goalretro.json.timestampcomes from theretro.completedenvelopets- The remaining retro content comes from
retro.completed.properties.retro
5. sandbox.json — missing fields
Decision: Add host_working_directory, container_mount_point, provider, and identifier to sandbox.initialized so it alone is sufficient to reconstruct sandbox.json.
6. workflow.fabro + workflow.toml — raw source files
Decision: Add workflow_source (raw dot text) and workflow_config (raw TOML text) as string fields on run.created. Covered by gap 1–2.
7. nodes/{node_id}/response.md — LLM response text
Decision: Add a response string field to stage.completed for LLM stages.
8. nodes/{node_id}/prompt.md — already covered
No change needed. stage.prompt is emitted per handler invocation (including retries) with the full rendered prompt text.
9–10. stdout.log, stderr.log, script_timing.json — new command.completed event
Decision: Emit a command.completed event after a command node finishes. Fields:
stdout,stderr,exit_code,duration_ms,timed_out
The node_id is in the envelope.
11. nodes/{node_id}/script_invocation.json — new command.started event
Decision: Emit a command.started event with script, language, timeout_ms. Pairs with command.completed. Remove script from stage.started — it's handler-specific data that belongs on the handler-specific event (same pattern as agent.session.started and stage.prompt).
12. cli_stdout.log, cli_stderr.log, provider_used.json (CLI) — new agent.cli.started / agent.cli.completed events
Decision: Emit an agent.cli.started event before the CLI subprocess starts, with mode ("cli"), provider, model, command. Emit agent.cli.completed after it finishes, with stdout, stderr. The node_id is in the envelope.
13. nodes/{node_id}/diff.patch — git diff
Decision: Add a diff string field to checkpoint.completed.
14. nodes/{node_id}/provider_used.json — provider metadata
Decision: Add provider metadata to handler-specific events:
agent.session.startedgetsmode("agent"),provider,modelagent.cli.startedgetsmode("cli"),provider,model,commandstage.promptgetsmode("prompt"),provider,model
15. parallel_results.json — head_sha
Decision: Add an optional head_sha field to parallel.branch.completed.
16. retro/prompt.md + retro/response.md
Decision: Add prompt to retro.started. Add response to retro.completed (alongside the parsed retro struct).
17. retro/provider_used.json
Decision: Add provider and model to retro.started alongside the prompt.
Conclusion timestamps and final SHA normalization
Decision: Treat terminal run events as the authoritative source for conclusion.json.
Derivation details:
conclusion.json.timestampcomes from the terminal event envelopets(run.completedorrun.failed)conclusion.json.final_git_commit_shacomes fromrun.completed.properties.final_git_commit_shaon success/partial success- On failure,
conclusion.json.final_git_commit_shais reconstructed fromrun.failed.properties.git_commit_sha
Follow-up improvements
node_visits,loop_failure_signatures,restart_failure_signaturesonstage.completedare snapshots of accumulated state. In the future, these could be derived from event replay instead of being carried on each event — removing them would reduce event size but require replay logic in every consumer.context_valuesis the full accumulated context map. If context maps grow large, a future optimization could emit onlycontext_updates(the delta) and require consumers to accumulate. For now, shipping the full snapshot is simpler.stdout/stderroncommand.completedcould be large. If this becomes a problem, consider a size threshold with truncation or a separate blob store with a reference in the event.