feat(skills): capacités agent découvrables via idea_list_agents
Ajoute SkillKind (Workflow/Reference) sur Skill, extrait le use case ResolveAgentCapabilities à partir des SkillRef assignés, et enrichit idea_list_agents d'un champ additif capabilities (name, description, kind) — remplace l'exposition de SkillRef opaques par un inventaire de capacités interrogeable, source commune avec le bloc « Skills disponibles » injecté au lancement (#119). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -101,7 +101,7 @@ pub use background_task::{
|
||||
BACKGROUND_TASK_TEXT_MAX_BYTES,
|
||||
};
|
||||
|
||||
pub use skill::{Skill, SkillRef, SkillScope};
|
||||
pub use skill::{Skill, SkillKind, SkillRef, SkillScope};
|
||||
|
||||
pub use template::{AgentTemplate, TemplateVersion};
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ use crate::conversation::ConversationParty;
|
||||
use crate::ids::{AgentId, NodeId, SkillId};
|
||||
use crate::live_state::WorkStatus;
|
||||
use crate::mailbox::TicketId;
|
||||
use crate::skill::SkillScope;
|
||||
use crate::skill::{SkillKind, SkillScope};
|
||||
|
||||
/// Current schema version for orchestration capabilities injected into an
|
||||
/// agent's effective runtime context.
|
||||
@ -39,6 +39,9 @@ pub struct AssignedSkillSnapshot {
|
||||
pub name: String,
|
||||
/// One-line affordance description shown in the model context.
|
||||
pub description: String,
|
||||
/// Explicit nature of the assigned skill capability.
|
||||
#[serde(default)]
|
||||
pub kind: SkillKind,
|
||||
/// Position in the agent assignment list, preserving manifest order.
|
||||
pub assignment_index: u32,
|
||||
}
|
||||
|
||||
@ -27,6 +27,17 @@ pub enum SkillScope {
|
||||
Project,
|
||||
}
|
||||
|
||||
/// Nature of a skill when surfaced as an agent capability.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SkillKind {
|
||||
/// Procedural workflow the agent may execute on demand.
|
||||
#[default]
|
||||
Workflow,
|
||||
/// Reference material the agent may consult selectively.
|
||||
Reference,
|
||||
}
|
||||
|
||||
/// A reusable workflow assignable to one or more agents.
|
||||
///
|
||||
/// Invariants enforced here:
|
||||
@ -45,6 +56,10 @@ pub struct Skill {
|
||||
/// [`Skill::effective_description`] falls back to the first line of the body.
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
/// Explicit skill nature. Defaults to [`SkillKind::Workflow`] for legacy
|
||||
/// skill JSON/index rows that predate this field.
|
||||
#[serde(default)]
|
||||
pub kind: SkillKind,
|
||||
/// Markdown body — the workflow injected into an agent's convention file.
|
||||
pub content_md: MarkdownDoc,
|
||||
/// Scope (selects the backing store).
|
||||
@ -74,6 +89,7 @@ impl Skill {
|
||||
id,
|
||||
name,
|
||||
description: None,
|
||||
kind: SkillKind::Workflow,
|
||||
content_md,
|
||||
scope,
|
||||
})
|
||||
@ -90,6 +106,13 @@ impl Skill {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns this skill with an explicit [`SkillKind`] set (builder).
|
||||
#[must_use]
|
||||
pub const fn with_kind(mut self, kind: SkillKind) -> Self {
|
||||
self.kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// The description to surface for this skill, with a deterministic fallback.
|
||||
///
|
||||
/// Returns [`Skill::description`] when present and non-blank (trimmed);
|
||||
@ -122,7 +145,8 @@ impl Skill {
|
||||
pub fn with_content(&self, content_md: MarkdownDoc) -> Result<Self, DomainError> {
|
||||
Ok(
|
||||
Skill::new(self.id, self.name.clone(), content_md, self.scope)?
|
||||
.with_description(self.description.clone()),
|
||||
.with_description(self.description.clone())
|
||||
.with_kind(self.kind),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -230,22 +254,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn deserialize_legacy_skill_without_description_defaults_to_none() {
|
||||
// A skill JSON written before `description` existed must deserialise with
|
||||
// `description: None` thanks to `#[serde(default)]`.
|
||||
// A skill JSON written before `description`/`kind` existed must
|
||||
// deserialise with additive defaults.
|
||||
let id = uuid::Uuid::from_u128(7);
|
||||
let json =
|
||||
format!(r##"{{"id":"{id}","name":"legacy","contentMd":"# body","scope":"global"}}"##);
|
||||
let parsed: Skill = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.description, None);
|
||||
assert_eq!(parsed.kind, SkillKind::Workflow);
|
||||
assert_eq!(parsed.name, "legacy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip_with_description() {
|
||||
let original = skill("# body").with_description(Some("affordance".to_owned()));
|
||||
fn serde_round_trip_with_description_and_kind() {
|
||||
let original = skill("# body")
|
||||
.with_description(Some("affordance".to_owned()))
|
||||
.with_kind(SkillKind::Reference);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let back: Skill = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, original);
|
||||
assert_eq!(back.description.as_deref(), Some("affordance"));
|
||||
assert_eq!(back.kind, SkillKind::Reference);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user