feat(ergon): hook lifecycle + model wire types (BRO-997)#1152
Conversation
Lands the second slice of `ergon` per spec §12 work order: the eight-event
hook lifecycle and the wire types those hooks observe. Together these are
the mutation/extension surface that step.rs (BRO-998) will fire during the
autonomous loop.
## Why two modules in one PR
The Hook trait references `ModelRequest`, `ModelResponse`, `ToolCall`,
`ToolResult` directly in its signatures. Defining those types in this PR
(rather than re-exporting from `praxis_core` / `arcan_provider`) keeps
ergon's contract independent of any specific provider crate — step.rs
will translate, not couple. Bundling the types with the trait that uses
them avoids the alternative (placeholder types churned in BRO-998).
## What lands
- `model.rs` (~480 LOC) — provider-agnostic wire types:
- `Message` with `Vec<ContentBlock>` (block-structured content; flat
string drops the structure modern providers actually emit)
- `ContentBlock`: Text / Reasoning / ToolUse / ToolResult variants,
`#[non_exhaustive]`, `#[serde(tag = "type")]`
- `ToolCall`, `ToolResult`, `ToolDefinition` — provider-agnostic
`is_error` flag distinguishes model-visible errors from runtime
failures (which surface as `ErgonError::Tool`)
- `ModelRequest` + `ModelResponse` + `Usage`
- Convenience helpers: `Message::user_text`, `Message::assistant_text`,
`ModelResponse::extract_tool_calls`, `ModelResponse::text`
- 8 unit tests covering serde round-trips, content extraction, helpers
- `hook.rs` (~400 LOC) — eight-event lifecycle trait:
- `Hook` with 8 events (workflow_start/end, step_start/end,
pre/post_inference, pre/post_tool_use)
- `HookCtx<'a>` carries SessionId + workflow name + tracing span
- `HookOutcome` (Continue/Deny), `ToolHookOutcome` (Continue/Deny/Stub
-> JSON), `InferenceHookOutcome` (Continue/Deny/Stub -> Message)
- `HookRegistry` builder with `with` / `with_arc` / `iter` / `len`
- 10 unit tests: registry order, default Continue across all 8
events, Deny short-circuits, Stub variants carry payload, mutation
in pre_inference (`req.system = ...`) round-trips, Arc-shared
registry iteration
## Spec deviation: hook event defaults
Spec §3.7 only defaulted `on_workflow_start` to `Continue`; the other 7
events were abstract. This PR defaults **all 8 events** to
`Ok(_::Continue)`. Rationale: real-world hooks (e.g. `NousScoreHook`)
typically care about one event; forcing eight no-op implementations on
every hook is boilerplate without safety — the same `Continue` ships
either way. The original spec choice was a compile-time push to "force
the implementer to think about each event" — ergonomically
counterproductive once you have more than two hooks. Documented in
CHANGELOG and `crates/ergon/ergon/CLAUDE.md`.
## Validation
- `cargo fmt -p ergon -- --check` clean
- `cargo clippy -p ergon --all-targets -- -D warnings` clean
- `cargo clippy --workspace -- -D warnings -A clippy::too_many_arguments` clean
- `cargo test -p ergon --all-targets` — **43 passed; 0 failed** (was 25
after #1151; +18 new tests)
- No regression in workspace check
Linear: BRO-997. Tracker: BRO-994.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Caution Review failedFailed to post review comments 📝 WalkthroughWalkthroughThis PR lands two foundational ergon modules: a Hook trait system with eight lifecycle events and a HookRegistry for managing hook invocations, plus a Model module defining provider-agnostic wire-format types (MessageRole, ContentBlock, Message, ToolDefinition, ToolCall, ToolResult, ModelRequest, Usage, ModelResponse) with builder helpers. Both modules are exported in lib.rs and documented in CHANGELOG.md and CLAUDE.md. ChangesHook & Model Foundation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Poem🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… pattern (BRO-1000) (#1169) New sibling crate at `crates/ergon/ergon-life-hooks/` housing the four "auto-registered" hooks the spec §3.8 calls for. Each hook is paired with a small adapter trait the hook consumes via `Arc<dyn _>`. The arcan adapter (BRO-1001) implements those traits against the actual substrate; this crate stays substrate-free. - `crates/ergon/ergon-life-hooks/` — new crate, ~600 LOC + 15 tests - `capability.rs` — `CapabilityResolver` + `PraxisCapabilityHook` (fires `on_pre_tool_use`; `Err(reason)` → `ToolHookOutcome::Deny` with reason; on Deny, autonomous loop synthesizes model-visible is_error result so model can recover) - `budget.rs` — `BudgetGate` + `AutonomicBudgetHook` (fires `on_pre_inference`; gate operates on `&mut ModelRequest` so implementations can ALSO narrow the request — reduce max_tokens, filter tools — and return Continue) - `score.rs` — `ResponseScorer` + `NousScoreHook` (fires `on_post_inference`; observe-only in v0.1; failures non-fatal, surface via `tracing::warn!`) - `attestation.rs` — `SoulAttester` + `AnimaAttestHook` (fires `on_workflow_start` and `on_workflow_end`; failures non-fatal — refusing to run because attestation infra is offline is worse than running unsigned; observable via telemetry) The crate's Cargo.toml lists only `ergon`, `async-trait`, `serde`, `serde_json`, `tokio`, `tracing`. **No** `anima-core`, **no** `autonomic-core`, **no** `nous-core`, **no** `aios-protocol-extension`. Substrate dep lives in BRO-1001 (the arcan adapter), where each adapter trait gets implemented against the real substrate type (`PolicySet` for `CapabilityResolver`, `AutonomicGatingProfile` for `BudgetGate`, `NousEvaluator` for `ResponseScorer`, `AgentSoul` for `SoulAttester`). Result: hooks are mockable, ergon stays vendor-neutral, substrate API churn doesn't ripple through hooks or workflow authors. Three new deviations stacked on top of the five from earlier PRs: 6. **Auto-hooks live in their own crate** (not in ergon). Reason: ergon should be vendor-neutral; Life-specific governance hooks belong in their own crate so a future ergon consumer (TS port, alternate agent OS) can ship its own governance set. 7. **Hooks consume adapter traits, not substrate types**. Each hook takes `Arc<dyn AdapterTrait>` at construction. The adapter trait is owned by ergon-life-hooks (not by anima/autonomic/nous/aios). The arcan adapter (BRO-1001) provides the impls. 8. **Failure semantics differ by hook**: capability and budget denials are hard veto (`Deny`); score and attestation failures are `tracing::warn!` + `Continue`. Documented at length in the crate's CLAUDE.md "Failure semantics — three tiers" table. Adds two missing constructors to ergon's `model.rs`. Both types are `#[non_exhaustive]` (set in BRO-997), which means external crates can't struct-literal construct them. The constructors are required for testing in ergon-life-hooks; the omission was an oversight in BRO-997. Purely additive; ergon's 43 tests still pass. - `cargo fmt --all -- --check` clean - `cargo clippy -p ergon-life-hooks --all-targets -- -D warnings` clean - `cargo clippy --workspace -- -D warnings -A clippy::too_many_arguments` clean - `cargo test -p ergon-life-hooks --all-targets` — **15 passed; 0 failed** - `cargo test -p ergon --all-targets` — **43 passed; 0 failed** (no regression) This PR branches off main, parallel to #1165 (BRO-998 — autonomous loop body in step.rs). The two are strictly independent: ergon-life-hooks uses only the Hook trait + wire types (already in main from #1152), NOT Step / StepCtx / Workflow (which are in #1165 / future PRs). The substrate adapters in BRO-1001 will pull both together by constructing a HookRegistry from these four auto-hooks. Linear: BRO-1000. Tracker: BRO-994. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Second slice of ergon per spec §12 work order. Lands the eight-event hook lifecycle and the wire types those hooks observe.
model.rs— provider-agnostic wire types (Messagewith block-structured content,ContentBlockenum,ToolCall,ToolResult,ToolDefinition,ModelRequest,ModelResponse,Usage)hook.rs—Hooktrait with 8 events (workflow_start/end, step_start/end, pre/post_inference, pre/post_tool_use),HookCtx<'a>, three outcome enums (HookOutcome,ToolHookOutcome,InferenceHookOutcome), andHookRegistrybuilder.Spec:
core/life/docs/superpowers/specs/2026-05-05-ergon-v0.1.md(§3.7, §3.8)Linear: BRO-997. Umbrella: BRO-994.
Why two modules in one PR
The
Hooktrait referencesModelRequest,ModelResponse,ToolCall,ToolResultdirectly. Three options for where those types live:praxis_core/arcan_providerErgon owning the wire types is what makes step.rs a translator (not a coupling) — it converts between ergon's shapes and
arcan_provider/praxis_core::Toolat the boundary.Spec deviation: hook event defaults
Spec §3.7 only defaulted
on_workflow_starttoContinue; the other 7 events were abstract. This PR defaults all 8 events toOk(_::Continue).Rationale: a real-world hook (e.g. `NousScoreHook`) only cares about one event. Forcing eight no-op implementations on every hook is boilerplate without safety — the same `Continue` ships either way. The original spec choice was a compile-time push to "force the implementer to think about each event" — ergonomically counterproductive once you have more than two hooks. Documented in CHANGELOG and `crates/ergon/ergon/CLAUDE.md`.
Test plan
What's next (BRO-998, separate PR)
`step.rs` will:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation