Skip to main content

nexo_poller/
poller.rs

1//! Trait + context every Poller implements. Kept Laravel-style:
2//! the runner is a dumb scheduler — the trait knows nothing about
3//! channels, credentials, or outbound topics. Pollers reach the world
4//! through [`PollerHost`], which is the single point of egress.
5//!
6//! Compared to the pre-Phase-96 shape, this module no longer exports
7//! `OutboundDelivery` / `TickOutcome` / channel-typed bundle fields.
8//! Those concerns live with the poller now.
9
10use std::sync::Arc;
11use std::time::Duration;
12
13use async_trait::async_trait;
14use chrono::{DateTime, Utc};
15use nexo_llm::ToolDef;
16use serde_json::Value;
17use tokio_util::sync::CancellationToken;
18
19use crate::error::PollerError;
20use crate::host::{PollerHost, TickAck};
21use crate::PollerRunner;
22
23/// Implemented by every poller module (built-in or out-of-tree).
24/// `Send + Sync + 'static` because the runner stores `Arc<dyn Poller>`
25/// and spawns tasks against them.
26#[async_trait]
27pub trait Poller: Send + Sync + 'static {
28    /// Discriminator used in YAML (`kind: rss`) and metrics.
29    fn kind(&self) -> &'static str;
30
31    /// Human label for `agent pollers list`. Defaults to empty.
32    fn description(&self) -> &'static str {
33        ""
34    }
35
36    /// Validate the per-job `config` JSON at boot. Errors fail loading
37    /// of that job only; siblings keep going. Default: accept anything.
38    fn validate(&self, _config: &Value) -> Result<(), PollerError> {
39        Ok(())
40    }
41
42    /// Run one tick. The runner persists the returned cursor and
43    /// honors the interval hint; anything the poller wants to send
44    /// outbound (broker publish, LLM call, credential lookup) goes
45    /// through `ctx.host`.
46    async fn tick(&self, ctx: &PollContext) -> Result<TickAck, PollerError>;
47
48    /// Optional per-kind LLM tools registered alongside the generic
49    /// `pollers_*` tools. Pollers without custom tools leave this
50    /// empty (default).
51    fn custom_tools(&self) -> Vec<CustomToolSpec> {
52        Vec::new()
53    }
54}
55
56/// Per-kind LLM tool spec — adapted into an `nexo-core` `ToolHandler`
57/// by the `nexo-poller-tools` crate at registration time.
58pub struct CustomToolSpec {
59    pub def: ToolDef,
60    pub handler: Arc<dyn CustomToolHandler>,
61}
62
63/// Local handler trait — kept inside `nexo-poller` so the crate
64/// stays free of `nexo-core`. The adapter lives in `nexo-poller-tools`.
65#[async_trait]
66pub trait CustomToolHandler: Send + Sync + 'static {
67    async fn call(&self, runner: Arc<PollerRunner>, args: Value) -> anyhow::Result<Value>;
68}
69
70/// What the runner hands a poller on every tick. Compared to V1:
71/// dropped `credentials`, `bundle`, `broker`, `llm_registry`,
72/// `llm_config`. Added `host` — the single egress.
73pub struct PollContext {
74    pub job_id: String,
75    pub agent_id: String,
76    pub kind: &'static str,
77    /// The `config:` block from `pollers.yaml` for this job.
78    pub config: Value,
79    /// Opaque cursor returned by the previous successful tick. `None`
80    /// on first run, after `agent pollers reset <id>`, or after a
81    /// `Permanent` error that cleared state.
82    pub cursor: Option<Vec<u8>>,
83    pub now: DateTime<Utc>,
84    /// Schedule's nominal interval, before jitter. Useful for pollers
85    /// that pace internally (RSS feeds honoring `<ttl>`).
86    pub interval_hint: Duration,
87    /// Cancelled when the runner is shutting down OR when this job is
88    /// hot-reloaded. Pollers with long inner loops (HTTP retries,
89    /// pagination) should `tokio::select!` on this so reload doesn't
90    /// hang behind a stuck request.
91    pub cancel: CancellationToken,
92    /// Single egress for runtime-level concerns. Pollers use this for
93    /// broker publishes, credential resolution, LLM invocations,
94    /// logging, and metrics.
95    pub host: Arc<dyn PollerHost>,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn assert_send_sync<T: Send + Sync>() {}
103
104    #[test]
105    fn poll_context_is_send_sync() {
106        assert_send_sync::<PollContext>();
107    }
108}