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 runtime state
85/// (provider store, file system, etc.) own those references directly via the
86/// capability's constructor.
87#[derive(Debug, Clone)]
88pub struct CommandExecutionContext {
89 /// Session the command is being executed against.
90 pub session_id: SessionId,
91}
92
93/// Result of executing a system command
94#[derive(Debug, Clone, Serialize, Deserialize)]
95#[cfg_attr(feature = "openapi", derive(ToSchema))]
96pub struct CommandResult {
97 /// Whether the command succeeded
98 pub success: bool,
99 /// Human-readable message describing the result
100 pub message: String,
101 /// Stable error code when `success` is false. Mirrors the codes emitted on
102 /// chat error messages so the UI can localize the copy.
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub error_code: Option<String>,
105 /// Optional structured fields associated with `error_code` (provider,
106 /// model_id, retry_after, …).
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
109 pub error_fields: Option<UserFacingErrorFields>,
110}