diff --git a/s16code/events/engine.py b/s16code/events/engine.py index ab32753..7a180c9 100644 --- a/s16code/events/engine.py +++ b/s16code/events/engine.py @@ -80,8 +80,35 @@ async def process(self, event: EventEnvelope, *, llm: TextLLM, transport: Any = record, fresh = self.store.ingest(event) if not fresh: return {"accepted": False, "duplicate": True, "record": record} - matching = [item for item in self.store.subscriptions() - if self._matches(item, event) and not self._self_caused(item, event)] + matching: list[Subscription] = [] + ignored_now: list[dict[str, Any]] = [] + now = datetime.now(UTC) + for item in self.store.subscriptions(): + if not self._matches(item, event): + continue + if self._self_caused(item, event): + refusal = { + "subscription_id": item.id, + "relevant": False, + "reason": ( + f"ignored actor {event.actor!r}: listed on subscription " + f"{item.id!r} ignore_actors" + ), + "goal": "", + "ignored": True, + "decided_at": now.isoformat(), + } + self.store.record_refusal( + control="ignore_actors", + reason=refusal["reason"], + event=event.model_dump(mode="json"), + subscription_id=item.id, + detail={"actor": event.actor}, + ) + self.store.add_decision(event.source, event.id, refusal) + ignored_now.append(refusal) + continue + matching.append(item) async def decide(subscription: Subscription) -> dict[str, Any]: async with self._slots: @@ -168,4 +195,5 @@ async def decide(subscription: Subscription) -> dict[str, Any]: else: clean.append(outcome) return {"accepted": True, "duplicate": False, "sequence": record["sequence"], - "matched": len(matching), "decisions": clean} + "matched": len(matching), "ignored": len(ignored_now), + "decisions": ignored_now + clean} diff --git a/tests/test_autonomous_events.py b/tests/test_autonomous_events.py index 34a2fdd..a592926 100644 --- a/tests/test_autonomous_events.py +++ b/tests/test_autonomous_events.py @@ -73,6 +73,26 @@ async def irrelevant(_prompt, _system): assert runtime.calls == [] +@pytest.mark.asyncio +async def test_ignore_actors_are_recorded_and_do_not_start_agents(tmp_path): + store, runtime = EventStore(tmp_path), RecordingRuntime() + store.put_subscription(subscription(ignore_actors=["spam-bot"])) + engine = AutonomousEventEngine(store, runtime) + + async def should_not_run(_prompt, _system): + raise AssertionError("ignored actors must not reach the relevance gate") + + outcome = await engine.process(event(actor="spam-bot"), llm=should_not_run) + assert outcome["accepted"] is True + assert outcome["matched"] == 0 + assert outcome["ignored"] == 1 + assert outcome["decisions"][0]["relevant"] is False + assert "spam-bot" in outcome["decisions"][0]["reason"] + assert runtime.calls == [] + refusals = store.refusals() + assert refusals and refusals[0]["control"] == "ignore_actors" + + @pytest.mark.asyncio async def test_one_event_can_launch_independent_subscriptions_concurrently(tmp_path): store, runtime = EventStore(tmp_path), RecordingRuntime()