converge_pack/agent.rs
1// Copyright 2024-2026 Reflective Labs
2
3// SPDX-License-Identifier: MIT
4
5//! The Suggestor trait — the contract all suggestor implementations satisfy.
6//!
7//! # Why this shape?
8//!
9//! Suggestors in Converge are not actors, not services, not workflow steps.
10//! They are pure functions over context: given the current state of shared
11//! context, a suggestor decides whether to act (`accepts`) and what to
12//! contribute (`execute`).
13//!
14//! This design makes suggestors:
15//! - **Deterministic**: same context → same decision.
16//! - **Composable**: suggestors don't know about each other, only about context.
17//! - **Testable**: mock the context, assert the effect.
18//!
19//! # Critical rules
20//!
21//! - `accepts()` must be **pure** — no side effects, no I/O, no mutations.
22//! - `execute()` is **read-only** — it reads context and returns an effect.
23//! - Suggestors **never call other suggestors** — all communication via shared context.
24//! - **Idempotency is context-based** — check for existing contributions in
25//! context, not internal state. Internal `has_run` flags violate the
26//! "context is the only shared state" axiom.
27
28use crate::context::{Context, ContextKey};
29use crate::effect::AgentEffect;
30
31/// The core suggestor contract.
32///
33/// Every suggestor in the Converge ecosystem implements this trait — whether
34/// it wraps an LLM, a policy engine, an optimizer, or a simple rule.
35///
36/// The engine calls `accepts()` to determine eligibility, then `execute()`
37/// to collect effects. Effects are merged by the engine in deterministic
38/// order (sorted by suggestor name).
39///
40/// # Async
41///
42/// `execute()` is async, allowing suggestors to call LLM providers, search
43/// backends, and other I/O without blocking. The engine awaits each
44/// suggestor and controls concurrency — suggestors don't need to manage
45/// their own parallelism.
46///
47/// # Thread Safety
48///
49/// Suggestors must be `Send + Sync` because the engine may execute eligible
50/// suggestors concurrently in the future.
51#[async_trait::async_trait]
52pub trait Suggestor: Send + Sync {
53 /// Human-readable name, used for ordering, logging, and provenance.
54 ///
55 /// Must be unique within a convergence run. The engine sorts suggestors
56 /// by name to ensure deterministic merge order.
57 fn name(&self) -> &str;
58
59 /// Context keys this suggestor reads from.
60 ///
61 /// The engine uses this to determine when a suggestor becomes eligible:
62 /// a suggestor is a candidate when at least one of its dependency keys
63 /// has been modified since the last cycle.
64 fn dependencies(&self) -> &[ContextKey];
65
66 /// Pure predicate: should this suggestor execute given the current context?
67 ///
68 /// # Contract
69 ///
70 /// - Must be **pure**: no side effects, no I/O, no state mutation.
71 /// - Must be **deterministic**: same context → same answer.
72 /// - Must check **idempotency via context**: look for your own
73 /// contributions in context (both `Proposals` and target key),
74 /// not internal flags.
75 fn accepts(&self, ctx: &dyn Context) -> bool;
76
77 /// Produce effects given the current context.
78 ///
79 /// # Contract
80 ///
81 /// - **Read-only**: do not mutate context. Return effects instead.
82 /// - Effects are collected by the engine and merged after all
83 /// eligible suggestors have executed.
84 /// - For LLM suggestors: emit `ProposedFact` to `ContextKey::Proposals`,
85 /// not directly to the target key.
86 async fn execute(&self, ctx: &dyn Context) -> AgentEffect;
87}