feat(arcan-ergon): kernel-side adapter running ergon::Workflow as tick body (BRO-1001)#1192
Conversation
…k body (BRO-1001) New `arcan-ergon` crate at `crates/arcan/arcan-ergon/` that runs an `ergon::Workflow` as the body of one `aios_runtime::KernelRuntime` tick. Closes BRO-1001's "ergon tick-body adapter" deliverable per the corrected architectural framing in `docs/superpowers/specs/2026-05-08-bro-1001-ergon-tick-body.md` (§6, §10 of the original ergon spec are SUPERSEDED — see PR #1190). Kernel side (aios-runtime): - `TickKind` enum on `TickInput` — `Direct` (default, kernel-owned single model call) vs `Workflow { name, input }` (delegated). - `WorkflowTickDispatcher` trait + `WorkflowTickInvocation<'a>` + `WorkflowTickOutcome` types so workflow runners (arcan-ergon today, future shapes later) plug in without forcing the kernel to take on substrate dependencies. - `KernelRuntime::with_workflow_dispatcher` builder method; dispatch logic in `execute_turn` after the `Perceive`/`Deliberate`/`StateEstimated` boundary. - Existing `TickInput` call sites updated to `kind: TickKind::Direct` for behavior parity. arcan-ergon modules: - `dispatcher` — `ErgonWorkflowDispatcher` implements `WorkflowTickDispatcher` against a `WorkflowRegistry`. - `registry` — type-erased boxed-executor registry keyed by name (`BoxedWorkflowExecutor::run_json` does the JSON boundary). - `provider` — `ModelProviderAdapter` wraps `ModelProviderPort` as `ergon::Provider`, translating the kernel's flat shape and synthesizing `StreamEvent` sequences from the directives. - `tools` — `ToolHarnessAdapter` wraps `ToolHarnessPort` as `ergon::ToolRegistry`. No capability evaluation here — that lives on the dedicated capability hook to avoid double-firing. - `runtime_handle` — `ModeRuntimeHandle` exposes per-tick `OperatingMode` as `ergon::RuntimeHandle`. - `hooks` — `KernelCapabilityResolver` (real `PolicyGatePort`-backed adapter for `CapabilityResolver`, with `ToolCapabilityMap` declaring per-tool caps and fail-closed on unknown tools), plus `NoopBudgetGate` / `NoopResponseScorer` / `NoopSoulAttester` permissive stand-ins for the other three adapter traits — real autonomic / nous / anima impls land in follow-up. - `runner::run_workflow_as_tick` — composes a fully-built `ergon::StepCtx` from a `WorkflowTickInvocation`, drives the workflow body, returns `WorkflowTickOutcome` with JSON output + emitted-event count. arcan binary now installs an `ErgonWorkflowDispatcher` (with an empty registry by default) on the kernel runtime at startup. Adopting daemons override the registry to register their concrete `ergon::Workflow` impls before the runtime starts serving. Tests: 16 unit + 4 end-to-end integration tests (`tests/workflow_tick_e2e.rs`) verifying the workflow tick path against a real `KernelRuntime` over file-backed event storage: workflow runs, JSON output ends up in an `ergon.workflow_output` `Custom` event in the journal, direct ticks still work alongside the dispatcher, unknown workflows surface clear errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR introduces workflow tick support to the kernel runtime by adding a new ChangesWorkflow Tick Infrastructure
Integration and Updates
🎯 4 (Complex) | ⏱️ ~75 minutes Possibly Related Issues
Possibly Related PRs
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 unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
crates/arcan/arcan-ergon/src/hooks.rs (1)
185-319: ⚡ Quick winAdd coverage for the
requires_approvalbranch.This test module exercises allow/deny/unknown-tool flows, but the BRO-1001-specific path that turns
requires_approvalinto a fail-closed error is still untested. A regression there would silently weaken approval gating.As per coding guidelines "All new Rust code requires tests; cargo test --workspace must pass before commit".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/arcan/arcan-ergon/src/hooks.rs` around lines 185 - 319, Add a test covering the requires_approval branch: implement a test PolicyGatePort (e.g., AlwaysRequiresApproval) that returns a PolicyGateDecision with requires_approval populated, then construct a KernelCapabilityResolver (use KernelCapabilityResolver::new) with that port and a ToolCapabilityMap entry for the tool, call KernelCapabilityResolver::can_invoke("tool", &serde_json::Value::Null).await and assert it returns an Err (expect_err) and that the error message indicates an approval/blocked state (contains the tool name or "requires_approval") so the BRO-1001 fail-closed behavior is exercised.crates/arcan/arcan-ergon/src/lib.rs (1)
56-63: 💤 Low valueRe-exports are consistent with the crate's primary integration surface — LGTM.
Minor note:
WorkflowRunInputs(used alongsideErgonWorkflowDispatcher::newin every construction site) is not re-exported at the crate root. Callers currently reach it viaarcan_ergon::runner::WorkflowRunInputs. Adding it to the re-export list would make the construction API fully self-contained at the crate root, but it compiles correctly as-is.♻️ One-line re-export addition
pub use runner::run_workflow_as_tick; +pub use runner::WorkflowRunInputs; pub use runtime_handle::ModeRuntimeHandle;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/arcan/arcan-ergon/src/lib.rs` around lines 56 - 63, Add a re-export for WorkflowRunInputs at the crate root so callers can access it alongside ErgonWorkflowDispatcher::new without referencing the runner module; locate the runner module re-export (currently exposing run_workflow_as_tick) and add WorkflowRunInputs to the pub use list (so users can import WorkflowRunInputs directly from the crate root instead of arcan_ergon::runner::WorkflowRunInputs).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Cargo.toml`:
- Around line 19-23: The workspace.members list in Cargo.toml is not
alphabetized: "crates/arcan/arcan-ergon" is incorrectly placed between
"crates/arcan/arcan-commands" and "crates/arcan/arcan-console"; move
"crates/arcan/arcan-ergon" so the entries are sorted lexicographically (e.g.,
place "crates/arcan/arcan-ergon" after "crates/arcan/arcan-core") to restore
alphabetical order.
In `@crates/aios/aios-runtime/src/lib.rs`:
- Around line 50-61: The doc references non-existent convenience APIs; add them
to match the docs: implement Default for TickInput (impl Default for TickInput {
fn default() -> Self { Self { kind: TickKind::Direct, /* fill other fields with
sensible defaults */ } } }) and add a pub fn direct() -> Self that returns
Self::default() (or explicitly sets kind: TickKind::Direct) so callers can use
..Default::default() or TickInput::direct(); alternatively, if you prefer not to
add APIs, remove the misleading sentences from the TickInput doc that mention
Default and TickInput::direct.
- Around line 702-785: The workflow branch returns early after
dispatcher.dispatch() and emitting "ergon.workflow_output", skipping the normal
terminal run lifecycle and error bookkeeping; change the TickKind::Workflow
handling (around dispatch(), outcome, and the emitted ergon.workflow_output) so
that on success it emits the same terminal events and runs the same
commit/finalize logic as the Direct path (use append_event with
EventKind::RunCompleted/RunSucceeded as appropriate, then call emit_phase and
finalize_tick and set ctx.mode before returning to current_tick_output), and on
dispatch failure ensure you append EventKind::RunErrored and invoke the same
error-streak / circuit-breaker bookkeeping used elsewhere (i.e., don't return
early — forward errors through the existing RunErrored handling path so workflow
ticks match Direct ticks); touch symbols: workflow_dispatcher,
WorkflowTickInvocation, dispatch(), outcome, append_event(), emit_phase(),
finalize_tick(), current_tick_output().
In `@crates/arcan/arcan-ergon/src/provider.rs`:
- Around line 125-137: ModelDirective::Message currently emits
StreamEvent::TextStart/TextDelta/TextEnd with a constant id "message", causing
ID collisions when multiple Message directives are emitted; fix this by
introducing a local counter (e.g., message_idx) before the directive-processing
loop in provider.rs and incrementing it for each ModelDirective::Message, then
construct the id using that index (matching how TextDelta IDs are generated)
when emitting StreamEvent::TextStart, StreamEvent::TextDelta, and
StreamEvent::TextEnd and when pushing ContentBlock::text so each message gets a
unique id.
In `@crates/arcan/arcan-ergon/tests/workflow_tick_e2e.rs`:
- Around line 242-247: The current if-let chain using workflow_output_event and
EventKind::Custom can silently skip the inner assertions if the pattern stops
matching; change the code to explicitly unwrap or expect the Option (use
workflow_output_event.expect(...) or .unwrap()) and then match or destructure
the EventKind::Custom (e.g., let EventKind::Custom { data, .. } = event.kind
else { panic!(...) }) so the test fails loudly if the event is missing or the
kind/shape changed, then keep the assert_eq! checks on data["workflow"] and
data["output"]["message"] to ensure payload verification.
---
Nitpick comments:
In `@crates/arcan/arcan-ergon/src/hooks.rs`:
- Around line 185-319: Add a test covering the requires_approval branch:
implement a test PolicyGatePort (e.g., AlwaysRequiresApproval) that returns a
PolicyGateDecision with requires_approval populated, then construct a
KernelCapabilityResolver (use KernelCapabilityResolver::new) with that port and
a ToolCapabilityMap entry for the tool, call
KernelCapabilityResolver::can_invoke("tool", &serde_json::Value::Null).await and
assert it returns an Err (expect_err) and that the error message indicates an
approval/blocked state (contains the tool name or "requires_approval") so the
BRO-1001 fail-closed behavior is exercised.
In `@crates/arcan/arcan-ergon/src/lib.rs`:
- Around line 56-63: Add a re-export for WorkflowRunInputs at the crate root so
callers can access it alongside ErgonWorkflowDispatcher::new without referencing
the runner module; locate the runner module re-export (currently exposing
run_workflow_as_tick) and add WorkflowRunInputs to the pub use list (so users
can import WorkflowRunInputs directly from the crate root instead of
arcan_ergon::runner::WorkflowRunInputs).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 14163222-08a3-4935-b02f-089bfcb47e78
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
CHANGELOG.mdCargo.tomlcrates/aios/aios-kernel/src/lib.rscrates/aios/aios-runtime/src/lib.rscrates/arcan/arcan-ergon/CLAUDE.mdcrates/arcan/arcan-ergon/Cargo.tomlcrates/arcan/arcan-ergon/src/dispatcher.rscrates/arcan/arcan-ergon/src/error.rscrates/arcan/arcan-ergon/src/hooks.rscrates/arcan/arcan-ergon/src/lib.rscrates/arcan/arcan-ergon/src/provider.rscrates/arcan/arcan-ergon/src/registry.rscrates/arcan/arcan-ergon/src/runner.rscrates/arcan/arcan-ergon/src/runtime_handle.rscrates/arcan/arcan-ergon/src/tools.rscrates/arcan/arcan-ergon/tests/workflow_tick_e2e.rscrates/arcan/arcan/Cargo.tomlcrates/arcan/arcan/src/main.rscrates/arcan/arcand/src/canonical.rscrates/arcan/arcand/src/consciousness.rs
Five actionable items + two nitpicks from CodeRabbit's review: 1. **Workflow tick lifecycle parity** (`aios-runtime/src/lib.rs`): the workflow branch was returning early after dispatch, skipping the standard terminal lifecycle (StepFinished + RunFinished) and bubbling dispatch failures out as bare anyhow errors. Now mirrors the Direct path: emits StepStarted before dispatch, then on success appends ergon.workflow_output + StepFinished + RunFinished and runs Commit/Reflect/Sleep finalize; on failure appends RunErrored, increments error_streak / uncertainty / error_budget, drops the runtime into Recover mode, and still finalizes the tick so the journal is coherent. The integration test `unknown_workflow_routes_to_run_errored_and_recover_mode` (renamed + rewritten from `unknown_workflow_yields_error`) asserts the new shape: tick returns Ok, mode=Recover, error_streak >= 1, and an `RunErrored` event in the journal mentioning the workflow name. 2. **Workspace.members alphabetization** (`Cargo.toml`): `crates/arcan/arcan-ergon` was wedged between arcan-commands and arcan-console. Moved to the alphabetically correct slot after arcan-core. 3. **TickInput doc accuracy** (`aios-runtime/src/lib.rs`): the doc-comment referenced `Default::default()` and a `direct()` helper that don't exist. Removed the misleading sentence — the field is just a normal struct field today and call sites set `kind: TickKind::Direct` explicitly. 4. **Provider Message id collision** (`arcan-ergon/src/provider.rs`): `ModelDirective::Message` was emitting `StreamEvent::TextStart` / `TextDelta` / `TextEnd` with a constant `id="message"`, so a completion with multiple Message directives would fail downstream sink pairing. Now uses a directive-local `message_idx` counter so each Message gets a unique `message-N` id. 5. **Test assertion hardening** (`tests/workflow_tick_e2e.rs::workflow_tick_emits_output_event`): was using `if let Some && let Custom` chain that would silently pass if the event went missing or its kind changed. Now uses `.expect()` + `let-else` so a regression fails the test loudly. Nitpicks: - **`requires_approval` test** (`arcan-ergon/src/hooks.rs`): added `approval_required_tool_fails_until_flow_lands` — exercises the BRO-1001 fail-closed path when `PolicyGateDecision.requires_approval` is non-empty (the kernel approval flow isn't bridged into ergon hooks yet; this regression-tests the temporary fail-closed behavior so a future bridge implementation can update the test deliberately). - **`WorkflowRunInputs` re-export** (`arcan-ergon/src/lib.rs`): now re-exported at the crate root alongside `run_workflow_as_tick` so callers can construct `ErgonWorkflowDispatcher::new(registry, inputs)` purely through `arcan_ergon::*` without reaching into the `runner` module. Test counts: 17 unit (was 16) + 4 e2e (4) all green. Workspace: cargo clippy --all-targets -- -D warnings clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes BRO-1001 — ships
the kernel-side adapter that runs an
ergon::Workflowas the body ofone
aios_runtime::KernelRuntimetick, per the correctedarchitectural framing in
docs/superpowers/specs/2026-05-08-bro-1001-ergon-tick-body.md(§6 + §10 of the original ergon spec are SUPERSEDED, see PR #1190).
TickInput.kind: TickKind(Direct vs Workflow) +WorkflowTickDispatchertrait +KernelRuntime::with_workflow_dispatcherbuilder + dispatch logic in
execute_turn. Direct ticks behaveexactly as before;
TickKind::Workflowticks delegate through aregistered dispatcher.
WorkflowRegistry(string-keyed,type-erased),
ModelProviderAdapter(kernelModelProviderPort→ergon::Provider),ToolHarnessAdapter(ToolHarnessPort→ergon::ToolRegistry),ModeRuntimeHandle,KernelCapabilityResolver(realPolicyGatePort-backedcapability gate), 3 noop adapter-trait impls (budget / score /
attest — real wiring lands in follow-up), and
run_workflow_as_tickthat composes a fullergon::StepCtxanddrives the workflow body.
ErgonWorkflowDispatcherregisters onthe kernel.
startup; adopting daemons override to register concrete
ergon::Workflowimpls before the runtime serves.Test plan
cargo test -p arcan-ergon --lib(16 tests pass).
KernelRuntimeoverfile-backed event storage:
cargo test -p arcan-ergon --tests(4 tests pass — workflow runs end-to-end, output lands in
journal as
ergon.workflow_outputCustom event, direct ticksstill work alongside the dispatcher, unknown workflows error
cleanly).
(
cargo test -p arcand -p aios-kernel).cargo clippy -p arcan-ergon -p aios-runtime -p aios-kernel -p arcand -p arcan --all-targets -- -D warnings.cargo fmt --all.Spec / docs
docs/superpowers/specs/2026-05-08-bro-1001-ergon-tick-body.mddocs/architecture/agent-harness.mdcrates/arcan/arcan-ergon/CLAUDE.md(full deviationlist + invariants for follow-up agents)
Unreleased.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
TickKindclassification to distinguish direct ticks from workflow-routed ticks.Chores
arcan-ergonas a new workspace crate for workflow-kernel integration.