Skip to main content

everruns_core/
command.rs

1// Custom Commands System
2//
3// Commands are user-invocable actions triggered via /slash syntax.
4// Two sources:
5// 1. System commands — from capabilities, execute through a dedicated handler
6//    without persisting a chat message
7// 2. Skill commands — from skills with user-invocable: true, expand to prompt
8//
9// Skills marked user-invocable appear in the command palette alongside
10// system commands. The UI fetches available commands and renders autocomplete.
11
12use crate::message::Controls;
13use crate::typed_id::SessionId;
14use crate::user_facing_error::UserFacingErrorFields;
15use serde::{Deserialize, Serialize};
16
17#[cfg(feature = "openapi")]
18use utoipa::ToSchema;
19
20/// Descriptor for a command available in a session
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[cfg_attr(feature = "openapi", derive(ToSchema))]
23pub struct CommandDescriptor {
24    /// Command name (used as /name)
25    pub name: String,
26    /// Human-readable description shown in autocomplete
27    pub description: String,
28    /// Where this command comes from
29    pub source: CommandSource,
30    /// Arguments this command accepts
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub args: Vec<CommandArg>,
33}
34
35/// Where a command originates from
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37#[cfg_attr(feature = "openapi", derive(ToSchema))]
38#[serde(rename_all = "snake_case")]
39pub enum CommandSource {
40    /// Built-in system command from a capability, executed out-of-band from the
41    /// main chat history. Handlers may call the model or do direct work.
42    System,
43    /// From a skill with user-invocable: true (expands to prompt, triggers LLM)
44    Skill,
45}
46
47/// Argument descriptor for a command
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[cfg_attr(feature = "openapi", derive(ToSchema))]
50pub struct CommandArg {
51    /// Argument name
52    pub name: String,
53    /// Description of the argument
54    pub description: String,
55    /// Whether the argument is required
56    #[serde(default)]
57    pub required: bool,
58    /// Static list of suggested values for this argument. Captured when
59    /// `Capability::commands()` is collected so renderers can surface
60    /// autocomplete entries without round-tripping back to the capability
61    /// on every keystroke. Empty means free-form input. Renderers should
62    /// treat the list as suggestions, not constraints — the capability's
63    /// `execute_command` is still the authority on what's accepted.
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub suggestions: Vec<String>,
66}
67
68/// Request payload for executing a system command
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[cfg_attr(feature = "openapi", derive(ToSchema))]
71pub struct ExecuteCommandRequest {
72    /// Command name without the leading slash
73    pub name: String,
74    /// Raw argument text after the command token
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub arguments: Option<String>,
77    /// Optional per-invocation runtime controls
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub controls: Option<Controls>,
80}
81
82/// Context handed to [`crate::capabilities::Capability::execute_command`] when a
83/// system command is dispatched. Carries only data that is safe to expose
84/// across the trait surface; capabilities that need handles to host-internal
85/// state (provider store, file system, etc.) own those references directly via
86/// the capability's constructor. Context-aware commands (out-of-band LLM call
87/// over the session's context, e.g. `/btw`) use the host facilities instead —
88/// see [`crate::command_host::CommandHost`].
89#[derive(Clone)]
90pub struct CommandExecutionContext {
91    /// Session the command is being executed against.
92    pub session_id: SessionId,
93    /// Host facilities: turn-context assembly and tool-less session
94    /// completions. Hosts that cannot provide them use
95    /// [`crate::command_host::DisabledCommandHost`].
96    pub host: std::sync::Arc<dyn crate::command_host::CommandHost>,
97}
98
99impl CommandExecutionContext {
100    pub fn new(
101        session_id: SessionId,
102        host: std::sync::Arc<dyn crate::command_host::CommandHost>,
103    ) -> Self {
104        Self { session_id, host }
105    }
106
107    /// Context for hosts that dispatch commands without turn-context/LLM
108    /// facilities; context-aware commands then fail with a clear error.
109    pub fn without_host(session_id: SessionId) -> Self {
110        Self {
111            session_id,
112            host: std::sync::Arc::new(crate::command_host::DisabledCommandHost),
113        }
114    }
115}
116
117impl std::fmt::Debug for CommandExecutionContext {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("CommandExecutionContext")
120            .field("session_id", &self.session_id)
121            .finish()
122    }
123}
124
125/// Result of executing a system command
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[cfg_attr(feature = "openapi", derive(ToSchema))]
128pub struct CommandResult {
129    /// Whether the command succeeded
130    pub success: bool,
131    /// Human-readable message describing the result
132    pub message: String,
133    /// Stable error code when `success` is false. Mirrors the codes emitted on
134    /// chat error messages so the UI can localize the copy.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub error_code: Option<String>,
137    /// Optional structured fields associated with `error_code` (provider,
138    /// model_id, retry_after, …).
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
141    pub error_fields: Option<UserFacingErrorFields>,
142}