Skip to main content

cortex_sdk/
effects.rs

1use serde::{Deserialize, Serialize};
2
3use crate::InvocationContext;
4
5/// Stable categories for side effects a tool may perform.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ToolEffectKind {
9    ReadFile,
10    ReadSecret,
11    WriteFile,
12    DeleteFile,
13    RunProcess,
14    NetworkRequest,
15    SendMessage,
16    SpendMoney,
17    Deploy,
18    ModifyCredential,
19    PersistMemory,
20    PublishContent,
21    ScheduleTask,
22    GenerateMedia,
23    IntrospectRuntime,
24    DelegateWork,
25}
26
27/// Whether a declared effect can be undone after execution.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum EffectReversibility {
31    Reversible,
32    PartiallyReversible,
33    Irreversible,
34}
35
36/// When the runtime should ask for confirmation before executing an effect.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum EffectConfirmation {
40    Never,
41    OnRisk,
42    Always,
43}
44
45/// Whether a tool can preview its effect before committing it.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum DryRunSupport {
49    NotSupported,
50    Supported,
51    RequiredBeforeExecute,
52}
53
54/// Declarative hints about how a tool participates in the runtime.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct ToolEffect {
57    /// The stable effect category.
58    pub kind: ToolEffectKind,
59    /// Optional target, such as a path, host, channel, or resource name.
60    #[serde(default, skip_serializing_if = "String::is_empty")]
61    pub target: String,
62    /// Whether this effect can be undone after execution.
63    pub reversibility: EffectReversibility,
64    /// Confirmation preference declared by the tool author.
65    pub confirmation: EffectConfirmation,
66    /// Dry-run support declared by the tool author.
67    pub dry_run: DryRunSupport,
68}
69
70impl ToolEffect {
71    #[must_use]
72    pub const fn new(kind: ToolEffectKind) -> Self {
73        Self {
74            kind,
75            target: String::new(),
76            reversibility: kind.default_reversibility(),
77            confirmation: kind.default_confirmation(),
78            dry_run: DryRunSupport::NotSupported,
79        }
80    }
81
82    #[must_use]
83    pub fn with_target(mut self, target: impl Into<String>) -> Self {
84        self.target = target.into();
85        self
86    }
87
88    #[must_use]
89    pub const fn with_reversibility(mut self, reversibility: EffectReversibility) -> Self {
90        self.reversibility = reversibility;
91        self
92    }
93
94    #[must_use]
95    pub const fn with_confirmation(mut self, confirmation: EffectConfirmation) -> Self {
96        self.confirmation = confirmation;
97        self
98    }
99
100    #[must_use]
101    pub const fn with_dry_run(mut self, dry_run: DryRunSupport) -> Self {
102        self.dry_run = dry_run;
103        self
104    }
105
106    #[must_use]
107    pub const fn is_mutating(&self) -> bool {
108        self.kind.is_mutating()
109    }
110
111    #[must_use]
112    pub fn label(&self) -> String {
113        if self.target.is_empty() {
114            format!("{:?}", self.kind)
115        } else {
116            format!("{:?}:{}", self.kind, self.target)
117        }
118    }
119}
120
121impl ToolEffectKind {
122    #[must_use]
123    pub const fn is_mutating(self) -> bool {
124        !matches!(
125            self,
126            Self::ReadFile | Self::ReadSecret | Self::NetworkRequest | Self::IntrospectRuntime
127        )
128    }
129
130    const fn default_reversibility(self) -> EffectReversibility {
131        match self {
132            Self::ReadFile | Self::NetworkRequest | Self::IntrospectRuntime => {
133                EffectReversibility::Reversible
134            }
135            Self::WriteFile
136            | Self::RunProcess
137            | Self::PersistMemory
138            | Self::ScheduleTask
139            | Self::GenerateMedia
140            | Self::DelegateWork => EffectReversibility::PartiallyReversible,
141            Self::ReadSecret
142            | Self::DeleteFile
143            | Self::SendMessage
144            | Self::SpendMoney
145            | Self::Deploy
146            | Self::ModifyCredential
147            | Self::PublishContent => EffectReversibility::Irreversible,
148        }
149    }
150
151    const fn default_confirmation(self) -> EffectConfirmation {
152        match self {
153            Self::ReadFile | Self::NetworkRequest | Self::IntrospectRuntime => {
154                EffectConfirmation::OnRisk
155            }
156            _ => EffectConfirmation::Always,
157        }
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
162pub struct ToolCapabilities {
163    /// Tool emits intermediate progress updates.
164    pub emits_progress: bool,
165    /// Tool emits observer-lane notes for the parent turn.
166    pub emits_observer_text: bool,
167    /// Tool is safe to run in background maintenance contexts.
168    pub background_safe: bool,
169    /// Declarative effect surface used by risk policy and transaction tracing.
170    #[serde(default, skip_serializing_if = "Vec::is_empty")]
171    pub effects: Vec<ToolEffect>,
172}
173
174impl ToolCapabilities {
175    #[must_use]
176    pub fn with_effect(mut self, effect: ToolEffect) -> Self {
177        self.effects.push(effect);
178        self
179    }
180
181    #[must_use]
182    pub fn with_effects(mut self, effects: impl IntoIterator<Item = ToolEffect>) -> Self {
183        self.effects.extend(effects);
184        self
185    }
186}
187
188/// Runtime bridge presented to tools during execution.
189///
190/// This allows plugins to consume stable runtime context and emit bounded
191/// execution signals without depending on Cortex internals.
192pub trait ToolRuntime: Send + Sync {
193    /// Stable invocation metadata.
194    fn invocation(&self) -> &InvocationContext;
195
196    /// Emit an intermediate progress update for the current tool.
197    fn emit_progress(&self, message: &str);
198
199    /// Emit observer text for the parent turn. This never speaks directly to
200    /// the user-facing channel.
201    fn emit_observer(&self, source: Option<&str>, content: &str);
202}