fabro/docs-internal/plan-events-as-source-of-truth.md
Bryan Helmkamp ee173403e9 docs
2026-04-01 16:25:41 -04:00

8.9 KiB
Raw Blame History

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 as properties.
  • When a file is reconstructed from multiple events, the derivation should be documented explicitly in events.md and run-directory-keys.md; consumers should not need to infer cross-event joins.
  • retro.json is reconstructed from multiple events: run.started provides workflow_name and goal, retro.completed provides the parsed retro payload, and the retro event envelope provides the completion timestamp.
  • conclusion.json.final_git_commit_sha is normalized from the terminal run event: run.completed.properties.final_git_commit_sha on success/partial success, run.failed.properties.git_commit_sha on 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

12. _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_id and run.json.run_id come from run.created envelope run_id
  • _init.json.created_at and run.json.created_at come from run.created envelope ts

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 stage
  • jump_to_node — non-edge jump target (optional)
  • context_values — full accumulated context map after this stage
  • node_visits — map of node id → visit count after this stage
  • loop_failure_signatures — failure signature → count after this stage
  • restart_failure_signatures — failure signature → count after this stage

Derivation details:

  • checkpoint.json.timestamp comes from the corresponding checkpoint.completed envelope ts
  • checkpoints/{seq:04}-{epoch_ms}.json uses the same derivation as checkpoint.json, with snapshot timestamp taken from each checkpoint.completed envelope ts

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_id comes from the retro.completed envelope run_id
  • retro.json.workflow_name comes from run.started.properties.name
  • retro.json.goal comes from run.started.properties.goal
  • retro.json.timestamp comes from the retro.completed envelope ts
  • 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 12.

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.

910. 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.started gets mode ("agent"), provider, model
  • agent.cli.started gets mode ("cli"), provider, model, command
  • stage.prompt gets mode ("prompt"), provider, model

15. parallel_results.jsonhead_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.timestamp comes from the terminal event envelope ts (run.completed or run.failed)
  • conclusion.json.final_git_commit_sha comes from run.completed.properties.final_git_commit_sha on success/partial success
  • On failure, conclusion.json.final_git_commit_sha is reconstructed from run.failed.properties.git_commit_sha

Follow-up improvements

  • node_visits, loop_failure_signatures, restart_failure_signatures on stage.completed are 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_values is the full accumulated context map. If context maps grow large, a future optimization could emit only context_updates (the delta) and require consumers to accumulate. For now, shipping the full snapshot is simpler.
  • stdout/stderr on command.completed could be large. If this becomes a problem, consider a size threshold with truncation or a separate blob store with a reference in the event.