Skip to main content

atelier_domain/
drafting.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Deterministic drafting flow (short path).
5//!
6//! Seeds -> Signals (research notes) -> Strategies (draft output)
7
8use converge_core::{AgentEffect, ContextKey, Suggestor};
9
10const DRAFT_RESEARCH_PREFIX: &str = "drafting_research:";
11const DRAFT_OUTPUT_PREFIX: &str = "drafting_output:";
12
13/// Drafting research agent (deterministic fallback).
14pub struct DraftingResearchAgent;
15
16#[async_trait::async_trait]
17impl Suggestor for DraftingResearchAgent {
18    fn name(&self) -> &str {
19        "DraftingResearchAgent"
20    }
21
22    fn dependencies(&self) -> &[ContextKey] {
23        &[ContextKey::Seeds]
24    }
25
26    fn accepts(&self, ctx: &dyn converge_core::Context) -> bool {
27        ctx.has(ContextKey::Seeds)
28            && !ctx
29                .get(ContextKey::Signals)
30                .iter()
31                .any(|fact| fact.id().as_str().starts_with(DRAFT_RESEARCH_PREFIX))
32    }
33
34    async fn execute(&self, ctx: &dyn converge_core::Context) -> AgentEffect {
35        let summary = ctx
36            .get(ContextKey::Seeds)
37            .iter()
38            .map(|seed| seed.content().to_string())
39            .collect::<Vec<_>>()
40            .join(" | ");
41
42        AgentEffect::with_proposal(crate::proposal(
43            self.name(),
44            ContextKey::Signals,
45            format!("{DRAFT_RESEARCH_PREFIX}notes"),
46            format!("Drafting research notes: {summary}"),
47        ))
48    }
49}
50
51/// Drafting composer agent (deterministic fallback).
52pub struct DraftingComposerAgent;
53
54#[async_trait::async_trait]
55impl Suggestor for DraftingComposerAgent {
56    fn name(&self) -> &str {
57        "DraftingComposerAgent"
58    }
59
60    fn dependencies(&self) -> &[ContextKey] {
61        &[ContextKey::Signals]
62    }
63
64    fn accepts(&self, ctx: &dyn converge_core::Context) -> bool {
65        ctx.get(ContextKey::Signals)
66            .iter()
67            .any(|fact| fact.id().as_str().starts_with(DRAFT_RESEARCH_PREFIX))
68            && !ctx
69                .get(ContextKey::Strategies)
70                .iter()
71                .any(|fact| fact.id().as_str().starts_with(DRAFT_OUTPUT_PREFIX))
72    }
73
74    async fn execute(&self, ctx: &dyn converge_core::Context) -> AgentEffect {
75        let notes = ctx
76            .get(ContextKey::Signals)
77            .iter()
78            .filter(|fact| fact.id().as_str().starts_with(DRAFT_RESEARCH_PREFIX))
79            .map(|fact| fact.content().to_string())
80            .collect::<Vec<_>>()
81            .join("\n");
82
83        AgentEffect::with_proposal(crate::proposal(
84            self.name(),
85            ContextKey::Strategies,
86            format!("{DRAFT_OUTPUT_PREFIX}v0"),
87            format!("Draft output (deterministic):\n{notes}"),
88        ))
89    }
90}