diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2026-04-23 13:43:01 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-23 13:43:01 -0700 |
| commit | 461ef4c43589c6ca68176c180fd04f2755c9f036 (patch) | |
| tree | c646ea272d6016533c4941592f9a22baa2a54488 /crates/atuin-ai/src/fsm | |
| parent | feat: Send user-defined context with `TERMINAL.md` (#3443) (diff) | |
| download | atuin-461ef4c43589c6ca68176c180fd04f2755c9f036.zip | |
feat: Add skill discovery, loading, and invocation (#3444)
Adds a skills system that lets users define reusable LLM instructions as `SKILL.md` files with YAML frontmatter.
Diffstat (limited to 'crates/atuin-ai/src/fsm')
| -rw-r--r-- | crates/atuin-ai/src/fsm/effects.rs | 5 | ||||
| -rw-r--r-- | crates/atuin-ai/src/fsm/events.rs | 17 | ||||
| -rw-r--r-- | crates/atuin-ai/src/fsm/mod.rs | 50 |
3 files changed, 72 insertions, 0 deletions
diff --git a/crates/atuin-ai/src/fsm/effects.rs b/crates/atuin-ai/src/fsm/effects.rs index 306f1401..adc9628e 100644 --- a/crates/atuin-ai/src/fsm/effects.rs +++ b/crates/atuin-ai/src/fsm/effects.rs @@ -45,6 +45,11 @@ pub(crate) enum Effect { }, /// Kill a running tool (send interrupt to shell command). AbortTool { tool_id: String }, + /// Load a skill's content asynchronously (read + interpolate). + LoadSkill { + name: String, + arguments: Option<String>, + }, // ─── Persistence ──────────────────────────────────────────── /// Persist current conversation state to disk. diff --git a/crates/atuin-ai/src/fsm/events.rs b/crates/atuin-ai/src/fsm/events.rs index 6fecda08..e591db41 100644 --- a/crates/atuin-ai/src/fsm/events.rs +++ b/crates/atuin-ai/src/fsm/events.rs @@ -92,6 +92,23 @@ pub(crate) enum Event { /// The driver resolves known commands (like /help) and passes the /// rendered content; the FSM just pushes an OOB event. SlashCommand { command: String, content: String }, + + // ─── Skills ──────────────────────────────────────────────── + /// User invoked a skill via /skill-name. FSM emits a LoadSkill + /// effect; the driver loads the content asynchronously and sends + /// SkillLoaded when ready. + RequestSkillLoad { + name: String, + arguments: Option<String>, + }, + /// A skill's content has been loaded and interpolated. + /// Pushes skill content as OOB context and starts a turn so the + /// LLM sees the skill and acts on it. + SkillLoaded { + name: String, + arguments: Option<String>, + content: String, + }, } /// Result of the permission resolver check. diff --git a/crates/atuin-ai/src/fsm/mod.rs b/crates/atuin-ai/src/fsm/mod.rs index 25de41f3..3d72a3ae 100644 --- a/crates/atuin-ai/src/fsm/mod.rs +++ b/crates/atuin-ai/src/fsm/mod.rs @@ -309,6 +309,33 @@ impl AgentFsm { vec![] } + ( + AgentState::Idle { .. }, + Event::SkillLoaded { + name, + arguments, + content, + }, + ) => { + self.ctx.events.push(ConversationEvent::SkillInvocation { + name, + arguments, + content, + }); + self.ctx.current_response.clear(); + self.ctx.current_turn_tool_ids.clear(); + + let messages = self.build_messages(); + let session_id = self.ctx.session_id.clone(); + self.state = AgentState::Turn { + stream: StreamPhase::Connecting, + }; + vec![Effect::StartStream { + messages, + session_id, + }] + } + // ================================================================ // Turn — stream lifecycle // ================================================================ @@ -584,6 +611,29 @@ impl AgentFsm { vec![] } + // RequestSkillLoad during non-idle: still emit the effect + (_, Event::RequestSkillLoad { name, arguments }) => { + vec![Effect::LoadSkill { name, arguments }] + } + + // SkillLoaded during non-idle: queue so it's visible + // in context for the next turn. + ( + _, + Event::SkillLoaded { + name, + arguments, + content, + }, + ) => { + self.ctx.events.push(ConversationEvent::SkillInvocation { + name, + arguments, + content, + }); + vec![] + } + _ => vec![], } } |
