Skip to main content

harness_core/
guide.rs

1use crate::{Context, Execution, World, error::GuideError};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5
6/// What kind of work this guide applies to. Determines when `apply` runs.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[non_exhaustive]
9pub enum GuideScope {
10    /// Always inject for every task.
11    Always,
12    /// Only when the task description matches one of the given globs / regexes.
13    TaskMatches(Vec<String>),
14    /// Only when the world's repo contains files matching `pattern`.
15    FilesMatch { pattern: String },
16}
17
18impl GuideScope {
19    /// True if this guide should run for the given task.
20    pub fn matches(&self, task: &crate::Task) -> bool {
21        match self {
22            GuideScope::Always => true,
23            GuideScope::TaskMatches(patterns) => {
24                patterns.iter().any(|p| task.description.contains(p))
25            }
26            GuideScope::FilesMatch { .. } => true,
27        }
28    }
29}
30
31pub type GuideId = String;
32
33#[async_trait]
34pub trait Guide: Send + Sync + 'static {
35    fn id(&self) -> &GuideId;
36    fn kind(&self) -> Execution;
37    fn scope(&self) -> &GuideScope;
38    /// Called ONCE per session, at the start, before the first model call.
39    /// Use for content that doesn't change across iterations (profile,
40    /// skills catalogue, static instructions).
41    async fn apply(&self, ctx: &mut Context, world: &World) -> Result<(), GuideError>;
42    /// Called BEFORE every `model.complete()` call within a session
43    /// (default: no-op). Override to inject content that should adapt to
44    /// the current conversation state — most useful for recall-style guides
45    /// that want to re-query an external store based on the last user
46    /// message.
47    ///
48    /// Implementations are responsible for cleaning up their previous
49    /// injection (typically by tagging their `Block::Text` with a unique
50    /// marker prefix and removing it before pushing a fresh one) — the
51    /// framework doesn't auto-roll-back between iterations.
52    async fn apply_before_iter(
53        &self,
54        _ctx: &mut Context,
55        _world: &World,
56    ) -> Result<(), GuideError> {
57        Ok(())
58    }
59}
60
61pub struct GuideEntry {
62    pub factory: fn() -> Arc<dyn Guide>,
63}
64
65inventory::collect!(GuideEntry);
66
67pub fn iter_macro_guides() -> impl Iterator<Item = Arc<dyn Guide>> {
68    inventory::iter::<GuideEntry>().map(|e| (e.factory)())
69}