fabro/lib/crates/fabro-hooks
fabro-sh-0530[bot] a42634cc54
Add event-sourced todo tools for OpenAI and Anthropic profiles (#353)
## Summary

Adds a shared todo/task engine behind two model-native tool surfaces,
with all mutations persisted as individual run events and replayed into
`RunProjection`.

### Plan Summary

- **New domain types** in `fabro-types`: `TodoStatus`, `TodoListKind`,
`TodoProjection`, `TodoListProjection`, and `todos_by_list` on
`RunProjection`.
- **New run events**: `todo.created`, `todo.updated`, `todo.deleted` —
mapped through Fabro's typed event pipeline and replayed by
`RunProjectionReducer`.
- **`TodoRuntime`** (`fabro-agent`): thread-safe in-memory projection
shared across tool closures within a profile instance; each mutation
emits the corresponding agent event.
- **`update_plan`** registered only in `OpenAiProfile`: reconciles
incoming steps by exact `step` text (sha256-derived ID), emitting
create/update/delete events to match the submitted plan.
- **`TaskCreate` / `TaskUpdate` / `TaskList`** registered only in
`AnthropicProfile`: numeric task IDs per list, metadata merge with
`null`-key deletion, `status: "deleted"` routes to `todo.deleted`.
- **Session identity threading**: `ToolContext` gains `session_id`,
`root_session_id`, `tool_call_id`, and `agent_event_emitter`;
`execute_tool_calls` threads these through to `execute_one_tool`;
`Session` tracks `root_session_id` and `spawn_agent` inherits it for
subagents.
- **Scoping**: OpenAI todos scope to `openai_plan:<session_id>`
(per-session); Anthropic todos scope to
`anthropic_tasks:<root_session_id>` (shared across subagents).
- **Web invalidation**: `todo.*` events invalidate `getRunState` and the
run events list; tested in `run-events.test.tsx`.

```mermaid
graph TB
    subgraph OpenAI
        UP[update_plan] -->|diff by step text| TR[TodoRuntime]
    end
    subgraph Anthropic
        TC[TaskCreate] --> TR
        TU[TaskUpdate] --> TR
        TL[TaskList] -->|read-only snapshot| TR
    end
    TR -->|emit todo.created/updated/deleted| SE[SessionBoundEmitter]
    SE --> EV[AgentEvent stream]
    EV --> RP[RunProjection\ntodos_by_list]
```

### Key design decisions

- **Step identity by text, not position** (`update_plan`): a
sha256-derived ID from `list_id + step` means reordering without
renaming emits an update rather than a delete+create. Duplicate step
strings are rejected with a model-visible error because text is the
identity.
- **No plan-replace event**: the engine emits only individual mutation
events; bulk replacement is expressed as a set of create/update/delete
events produced by diffing the incoming plan against the projection
snapshot.
- **`TodoRuntime` per profile instance**: tools inside a single profile
share one runtime. OpenAI subagents each have their own `session_id` so
their plans are isolated; Anthropic subagents inherit `root_session_id`
so tasks are shared — matching upstream Codex/Claude behavior.
- **`AgentEventEmitter` trait on `ToolContext`**: a narrow interface
that lets tools publish typed events without taking a dependency on the
full `Emitter`. `SessionBoundEmitter` wraps `Emitter` and stamps
`session_id` + `tool_call_id` on each event.


### Fabro Details

<details>
<summary>Ran 9 stages in 93m 23s for $56.46</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 20s | – | 0 |
| implement | 41m 30s | $38.21 | 0 |
| simplify_opus | 29m 2s | $11.78 | 0 |
| simplify_gpt | 14m 24s | $6.46 | 0 |
| verify | 3m 2s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **93m 23s** | **$56.46** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-22 13:44:42 -04:00
..
src Add event-sourced todo tools for OpenAI and Anthropic profiles (#353) 2026-05-22 13:44:42 -04:00
tests refactor(hooks): centralize run location handling (#325) 2026-05-20 09:31:46 -04:00
Cargo.toml refactor(llm): use credential sources and split run services 2026-04-23 18:54:27 -04:00