feat(events): add control.directive.emitted event factory#478
feat(events): add control.directive.emitted event factory#478
Conversation
Add ouroboros.core.directive as an additive, caller-free vocabulary for control-plane decisions (continue / evaluate / evolve / unstuck / retry / compact / wait / cancel / converge). Decision sites are currently distributed across evaluation/, evolution/, resilience/, orchestrator/, and observability/, each with its own ad-hoc signals. This commit introduces only the shared type and its terminal set; no decision site is modified. Follow-up changes will migrate sites incrementally so each migration is independently reviewable. - src/ouroboros/core/directive.py: Directive StrEnum, per-member docstrings, and a module-level _TERMINAL_DIRECTIVES frozenset exposed through is_terminal (CANCEL, CONVERGE). - src/ouroboros/core/__init__.py: lazy re-export of Directive. - tests/unit/core/test_directive.py: value/terminality/membership/re-export coverage (10 tests). Verification: - uv run ruff check src/ouroboros/core/directive.py src/ouroboros/core/__init__.py tests/unit/core/test_directive.py - uv run ruff format --check <same paths> - uv run pytest tests/unit/core/ -q # 296 passed
There was a problem hiding this comment.
Review — ouroboros-agent[bot]
Verdict: APPROVE
Reviewing commit
62cbdacfor PR #478
Review record:
20cb6db7-b282-4885-b998-ab03dd57dc2f
Blocking Findings
No in-scope blocking findings remained after policy filtering.
Non-blocking Suggestions
None.
Design Notes
This is an additive, low-risk change: a shared Directive vocabulary plus a control-event factory with focused unit coverage for enum stability and payload shape. I did not find a scope-local contract mismatch or production bug in the touched files; note that I could not execute the tests here because pytest is not installed in the review environment.
Recovery Notes
First recoverable review artifact generated from codex analysis log.
Reviewed by ouroboros-agent[bot] via Codex deep analysis
|
Aligning with #476 (Phase 2 framing): this PR implements the observational-first stance per Q3 at #476#issuecomment-4294892689 — no emission site, no reactive consumer, payload denormalizes Reactive consumption (ControlBus subscription) is a follow-up, not this PR's concern. No code change needed. |
Expand the module docstring to connect Directive explicitly to the Agent OS layering introduced by the Phase 2 RFC (Q00#476): directives occupy the control layer and are the runtime-level syscalls through which decision sites express control flow. Also record two posture decisions that were implicit before: - Directives describe workflow control, not capability policy. Capability visibility/execution decisions stay in the policy layer and in policy.* events, never interleaved with directive semantics. - Existing local enums (e.g. StepAction) are mapped into this vocabulary incrementally rather than replaced in a single change. The docstring records the reference StepAction -> Directive mapping so callers and reviewers can see the migration path at the type's site of truth. No behavior or API change.
Add ouroboros.events.control as the first event category capturing decision causality. Existing categories (decomposition, evaluation, interview, lineage, ontology) record what was produced; this factory records why the run advanced from one step to the next. - src/ouroboros/events/control.py: create_control_directive_emitted_event factory emitting type 'control.directive.emitted' aggregated by execution id. Payload denormalizes directive.value and is_terminal so downstream consumers classify events without importing the Directive enum. Optional context_snapshot_id and extra fields stay out of the payload when unset to keep persisted rows compact. - tests/unit/events/test_control_events.py: 10 tests covering event type, aggregate shape, StrEnum serialization, terminality flag, optional fields, and coverage of every Directive member. No production emission is added in this change; emission sites are migrated in follow-up PRs. Depends on the Directive type added by the parent PR for the control-plane vocabulary. Verification: - uv run ruff check src/ouroboros/events/control.py tests/unit/events/test_control_events.py - uv run ruff format --check <same> - uv run pytest tests/unit/events/ tests/unit/core/ -q
Expand the module docstring to position this factory explicitly inside the Agent OS Event Journal layer introduced by the Phase 2 RFC (Q00#476): existing categories answer 'what was produced,' this category answers 'why the run moved,' and together they form a single replayable causal timeline. Also record the posture decisions that were implicit in the first draft: - Observational-first: the event is the source of truth; reactive consumers (TUI lineage renderer, future ControlBus subscribers) are projections of persisted events, never upstream of them. - Payload shape rationale: denormalizing directive.value and is_terminal lets downstream consumers classify events without importing the enum, keeping the projection layer decoupled from the vocabulary module. - extra is a forward-compatibility slot, not a dumping ground: the docstring now states the preference for named arguments as fields stabilize. No behavior or API change; existing tests continue to pass.
62cbdac to
acee166
Compare
|
Correction to my earlier alignment note — same reasoning as on #477. Bot APPROVAL verifies style, not whether the maintainer's Phase 2 framing is reflected in the primitive itself. Re-reading
Branch rebased on top of the updated |
There was a problem hiding this comment.
Review — ouroboros-agent[bot]
Verdict: APPROVE
Reviewing commit
acee166for PR #478
Review record:
1eb5a72d-cb7f-4cf6-8c3e-5d8b808e9f8c
Blocking Findings
No in-scope blocking findings remained after policy filtering.
Non-blocking Suggestions
| 1 | tests/unit/events/test_control_events.py:14 | nice-to-have tests | The new factory is only exercised at the in-memory BaseEvent level. A round-trip assertion through to_db_dict() / BaseEvent.from_db_row() would better lock in the stated JSON-safe payload contract for directive, is_terminal, and optional fields. |
Design Notes
The split is coherent: Directive establishes a small control vocabulary and events/control.py keeps the first integration observational-only. The new event payload is compact and forward-compatible, and the lazy re-export keeps the core package boundary consistent.
Recovery Notes
First recoverable review artifact generated from codex analysis log.
Reviewed by ouroboros-agent[bot] via Codex deep analysis
…00#476 review Q00 flagged on Q00#476 that the initial event shape (aggregate_type='control', aggregate_id=execution_id) stores decisions but does not let them surface inside existing lineage/execution reconstructions. A projector querying aggregate_type='lineage' never sees the interleaved directive stream, so the event is persisted but not reconstructable as part of the decision timeline. This change adopts Q00's target-oriented contract: create_control_directive_emitted_event( target_type: str, # 'session' | 'execution' | 'lineage' | 'agent_process' target_id: str, emitted_by: str, directive: Directive, reason: str, *, session_id: str | None = None, execution_id: str | None = None, lineage_id: str | None = None, generation_number: int | None = None, phase: str | None = None, context_snapshot_id: str | None = None, extra: dict | None = None, ) Stored event now has aggregate_type=target_type, aggregate_id=target_id, so a LineageProjector reading aggregate_type='lineage' naturally includes the directive stream alongside lineage.* state events. Correlation fields (session_id, execution_id, lineage_id, generation_number, phase) are optional and land in the payload only when provided, keeping rows compact and absence distinguishable from explicit None. target_type stays a free-form string (not an enum) so Phase 3's 'agent_process' target lands without a schema change. - src/ouroboros/events/control.py: signature rework, docstring updated to explain target-oriented aggregation and the canonical target types. - tests/unit/events/test_control_events.py: 17 tests covering target aggregation mirror, payload target duplication, optional correlation fields (session/execution/lineage/generation/phase/context_snapshot/ extra), and forward-compatible target types ('agent_process', 'session'). Verification: - uv run ruff check src/ouroboros/events/control.py tests/unit/events/test_control_events.py - uv run ruff format --check <same> - uv run pytest tests/unit/events/ tests/unit/core/ -q # 350 passed
|
@Q00 — addressed your #476 feedback on the event contract. Commit What changed in this commitNew signature (matches your proposal verbatim): create_control_directive_emitted_event(
target_type: str, # "session" | "execution" | "lineage" | later "agent_process"
target_id: str,
emitted_by: str,
directive: Directive,
reason: str,
*,
session_id: str | None = None,
execution_id: str | None = None,
lineage_id: str | None = None,
generation_number: int | None = None,
phase: str | None = None,
context_snapshot_id: str | None = None,
extra: dict[str, Any] | None = None,
)Storage change:
That is the key move. A Payload shape mirrors your example: {
"target_type": "lineage",
"target_id": "ralph-zepia-20260420-v3",
"lineage_id": "ralph-zepia-20260420-v3",
"generation_number": 2,
"phase": "reflecting",
"emitted_by": "evolver",
"directive": "retry",
"is_terminal": false,
"reason": "Reflect failed; retry budget remains."
}Optional correlation fields ( Target type is a free-form string rather than an enum. That preserves the invariant you called out: when Phase 3 introduces Reconstructable-timeline verificationThe decision lane you described now reconstructs by a single aggregate filter: yields, for a ralph run: — exactly the shape from your comment. The directive events are now first-class citizens of the lineage aggregate, so a future What this unlocks for later migrations
No event-type churn across those transitions. Test coverage17 unit tests across four classes:
Verification: Diff: +262 / -103 (net +159 over the previous commit). Ready for another look. |
There was a problem hiding this comment.
Review — ouroboros-agent[bot]
Verdict: APPROVE
Reviewing commit
ae8cb8cfor PR #478
Review record:
77da3f58-4142-4b80-a13e-2dcf233a24fe
Blocking Findings
No in-scope blocking findings remained after policy filtering.
Non-blocking Suggestions
None.
Design Notes
The PR is coherent: Directive is a small, explicit control vocabulary, and control.directive.emitted is modeled as an observational event aggregated onto the target object so existing aggregate replays can naturally include decision history. I did not find a diff-scoped contract break or runtime issue in the introduced enum, event factory, or their tests.
Recovery Notes
First recoverable review artifact generated from codex analysis log.
Reviewed by ouroboros-agent[bot] via Codex deep analysis
Summary
ouroboros.events.control.create_control_directive_emitted_event— a factory that persists control-plane decisions (continue / evaluate / evolve / unstuck / retry / compact / wait / cancel / converge) to the EventStore via the existingBaseEventcontract.Motivation
First step of #473 under the control-plane framing in #471. The repo's event-sourcing story is strong —
decomposition,evaluation,interview,lineage, andontologycategories together reconstruct prior state — but there is no category for the decisions themselves. Today, why a run moved fromevaluatetoevolve, or why aretrywas issued, is implied by downstream effects and visible only in logs.This is the reason the TUI
lineagescreen can show phase progress but not the decisions that produced it, and the reason theDirectivetype introduced in #472 would otherwise be ephemeral.The factory follows the same surgical-addition pattern as #436 (
feat(events): add event_version to BaseEvent payload) and #443 (feat(evaluate): add ouroboros_checklist_verify MCP tool): a new primitive introduced alongside its tests, with no existing caller modified.Changes
src/ouroboros/events/control.py— new module.create_control_directive_emitted_event(execution_id, emitted_by, directive, reason, context_snapshot_id=None, extra=None).BaseEventwithtype="control.directive.emitted",aggregate_type="control",aggregate_id=execution_id.directive.valueandis_terminalso downstream consumers (TUI, drift detector, future replay) classify events without importingDirective.context_snapshot_idandextrastay out of the payload when unset — matching the "compact payload" convention used byevents/evaluation.pyandevents/decomposition.py.tests/unit/events/test_control_events.py— 10 tests covering event type, aggregate shape, StrEnum serialization, terminality, optional-field compaction, and coverage of everyDirectivemember (guards future vocabulary growth).Not changed
feat(evaluation): emit control.directive.emitted from terminal branches).BaseEvent.to_db_dictalready stores arbitrary payloads. Schema-level work (a dedicatedcontrol_directivetable, if the maintainers prefer one over the genericeventstable) is deferred to a follow-up.Verification
Result:
343 passed(10 new), ruff clean, format clean.References
control_directiveevent category)