agentyk_core/capability.rs
1//! Capabilities — composable extensions that give an agent behavior.
2//!
3//! A capability contributes system-prompt text and tools. It keeps the
4//! everruns `Capability` trait shape (`id`, `system_prompt_contribution`,
5//! `tools`), with one deliberate difference: capabilities are attached to an
6//! agent **by object** (`AgentBuilder::capability(FileSystem::new())`), not
7//! registered in a registry and then referenced by string id. The string
8//! `id()` remains for diagnostics and for hosts that layer config-driven
9//! wiring on top.
10
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16use crate::error::Result;
17use crate::id::SessionId;
18use crate::tool::{Tool, ToolOutput};
19
20/// Context available while assembling the system prompt.
21#[derive(Debug, Clone)]
22#[non_exhaustive]
23pub struct SystemPromptContext {
24 /// The session whose prompt is being assembled, so a capability can
25 /// contribute session-specific text.
26 pub session_id: SessionId,
27}
28
29impl SystemPromptContext {
30 /// Context for assembling one session's system prompt.
31 pub fn new(session_id: SessionId) -> Self {
32 Self { session_id }
33 }
34}
35
36/// One slash-command a capability exposes — everruns' `CommandDescriptor`,
37/// pared down to what a host needs to list and invoke it.
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[non_exhaustive]
40pub struct CommandDescriptor {
41 /// Invoked as `/{name}` (no leading slash here).
42 pub name: String,
43 /// One line describing what it does, for a host to list.
44 pub description: String,
45}
46
47impl CommandDescriptor {
48 /// Describe a command a capability answers to.
49 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
50 Self {
51 name: name.into(),
52 description: description.into(),
53 }
54 }
55}
56
57/// Context available while executing a command. Commands are host-invoked
58/// directly (e.g. a user typing `/goal set X`) and bypass the turn loop
59/// entirely, so there's no `turn_id` — only a session.
60#[derive(Debug, Clone)]
61#[non_exhaustive]
62pub struct CommandContext {
63 /// The session the command was invoked in.
64 pub session_id: SessionId,
65}
66
67impl CommandContext {
68 /// Context for running one command.
69 pub fn new(session_id: SessionId) -> Self {
70 Self { session_id }
71 }
72}
73
74/// A bundle of behavior attached to an agent: prompt text, tools, and
75/// slash commands.
76///
77/// Capabilities are attached **by object**, not registered and referenced by
78/// id, so a capability can hold whatever state it needs (a connection, a
79/// workspace handle) without a registry to look it up in.
80#[async_trait]
81pub trait Capability: Send + Sync {
82 /// Stable string id, e.g. `"file_system"` or `"mcp:github"`.
83 fn id(&self) -> &str;
84
85 /// Display name. Defaults to the id.
86 fn name(&self) -> &str {
87 self.id()
88 }
89
90 /// One line for a host to show alongside the name. Empty by default.
91 fn description(&self) -> &str {
92 ""
93 }
94
95 /// Generic, serializable extension hatch for host-side capability
96 /// metadata an adopting host owns — enabled/degraded `status`,
97 /// `category`, `icon`, and similar richness everruns exposes. Kept as an
98 /// opaque bag (default `Null`) rather than typed core methods, so the
99 /// contract doesn't grow per host concern; a host reads and interprets
100 /// the shape it defined.
101 fn metadata(&self) -> serde_json::Value {
102 serde_json::Value::Null
103 }
104
105 /// Text appended to the agent's system prompt, if any.
106 async fn system_prompt_contribution(&self, _context: &SystemPromptContext) -> Option<String> {
107 None
108 }
109
110 /// Tools this capability exposes to the model. Async so capabilities that
111 /// discover tools remotely (MCP) fit the same trait.
112 async fn tools(&self) -> Result<Vec<Arc<dyn Tool>>> {
113 Ok(Vec::new())
114 }
115
116 /// Slash commands this capability exposes for a host to list and route
117 /// — see `Session::commands`/`Session::execute_command` in the
118 /// framework crate. Default: none.
119 fn commands(&self) -> Vec<CommandDescriptor> {
120 Vec::new()
121 }
122
123 /// Run a command by name. Return `None` if this capability doesn't own
124 /// `name` (the host tries the next capability); `args` is the raw text
125 /// after the command name.
126 async fn execute_command(
127 &self,
128 _name: &str,
129 _args: &str,
130 _context: &CommandContext,
131 ) -> Option<ToolOutput> {
132 None
133 }
134}