fabro/docs/internal/plan-events-as-source-of-truth.md
2026-04-27 21:43:15 -07:00

163 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`, `working_directory`, `repo_cloned`, `clone_origin_url`, `clone_branch` |
| `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`, `source_directory`, `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 sandbox-native identity and clone metadata to `sandbox.initialized` so it alone is sufficient to reconstruct `sandbox.json`. Host paths and container mount points are not part of the durable event model; clone-based providers report their sandbox working directory plus repository clone state instead.
### 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.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.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.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.