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:
2026-08-01 00:26:56 +02:00
parent 5efb026a80
commit 92b17e9a69
18 changed files with 467 additions and 83 deletions

View File

@ -14,8 +14,9 @@
mod usecases;
pub use usecases::{
AssignSkillToAgent, AssignSkillToAgentInput, AssignedSkillResolver, CreateSkill,
CreateSkillInput, CreateSkillOutput, DeleteSkill, DeleteSkillInput, ListSkills,
ListSkillsInput, ListSkillsOutput, ReadSkill, ReadSkillInput, UnassignSkillFromAgent,
UnassignSkillFromAgentInput, UpdateSkill, UpdateSkillInput, UpdateSkillOutput,
AgentCapability, AssignSkillToAgent, AssignSkillToAgentInput, AssignedSkillResolver,
CreateSkill, CreateSkillInput, CreateSkillOutput, DeleteSkill, DeleteSkillInput, ListSkills,
ListSkillsInput, ListSkillsOutput, ReadSkill, ReadSkillInput, ResolveAgentCapabilities,
ResolveAgentCapabilitiesInput, UnassignSkillFromAgent, UnassignSkillFromAgentInput,
UpdateSkill, UpdateSkillInput, UpdateSkillOutput,
};

View File

@ -11,8 +11,8 @@ use std::sync::Arc;
use domain::ports::{AgentContextStore, EventBus, IdGenerator, SkillStore, StoreError};
use domain::{
Agent, AgentId, AgentManifest, AssignedSkillSnapshot, ConversationParty, DomainEvent,
MarkdownDoc, OrchestrationCapabilitySnapshot, Project, ProjectPath, Skill, SkillId, SkillRef,
SkillScope,
MarkdownDoc, OrchestrationCapabilitySnapshot, Project, ProjectPath, Skill, SkillId, SkillKind,
SkillRef, SkillScope,
};
use crate::error::AppError;
@ -211,9 +211,80 @@ impl DeleteSkill {
}
// ---------------------------------------------------------------------------
// ReadSkill
// ResolveAgentCapabilities / ReadSkill
// ---------------------------------------------------------------------------
/// One discoverable capability carried by an agent.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapability {
/// Capability display name.
pub name: String,
/// One-line affordance description.
pub description: String,
/// Capability nature.
pub kind: SkillKind,
}
impl From<&AssignedSkillSnapshot> for AgentCapability {
fn from(skill: &AssignedSkillSnapshot) -> Self {
Self {
name: skill.name.clone(),
description: skill.description.clone(),
kind: skill.kind,
}
}
}
/// Input for [`ResolveAgentCapabilities::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolveAgentCapabilitiesInput {
/// Agent whose assigned skills must be resolved.
pub agent: Agent,
/// Active project root used to resolve project-scoped skills.
pub project_root: ProjectPath,
}
/// Resolves assigned skills into discoverable agent capabilities.
pub struct ResolveAgentCapabilities {
resolver: Arc<AssignedSkillResolver>,
}
impl ResolveAgentCapabilities {
/// Builds the use case from a skill store.
#[must_use]
pub fn new(skills: Arc<dyn SkillStore>) -> Self {
Self {
resolver: Arc::new(AssignedSkillResolver::new(skills)),
}
}
/// Builds the use case from a shared resolver.
#[must_use]
pub fn with_resolver(resolver: Arc<AssignedSkillResolver>) -> Self {
Self { resolver }
}
/// Resolves the compact capability list, skipping dangling skill refs.
///
/// # Errors
/// [`AppError::Store`] on store failures other than a missing skill.
pub async fn execute(
&self,
input: ResolveAgentCapabilitiesInput,
) -> Result<Vec<AgentCapability>, AppError> {
let snapshot = self
.resolver
.resolve_for_agent(&input.agent, &input.project_root)
.await?;
Ok(snapshot
.assigned_skills
.iter()
.map(AgentCapability::from)
.collect())
}
}
/// Resolves assigned skills into the runtime capability snapshot used by both
/// context rendering and `idea_skill_read` authorization.
pub struct AssignedSkillResolver {
@ -269,6 +340,7 @@ impl AssignedSkillResolver {
scope: skill.scope,
name: skill.name,
description,
kind: skill.kind,
assignment_index: index as u32,
};
assigned.push((snapshot, skill.content_md));
@ -691,6 +763,56 @@ mod tests {
.unwrap()
}
#[tokio::test]
async fn resolve_agent_capabilities_returns_compact_kind_aware_affordances() {
let store = Arc::new(FakeSkillStore::default());
store.push(
skill(1, "deploy", "# Deploy\n\nbody", SkillScope::Project)
.with_description(Some("Deploys safely".to_owned())),
);
store.push(
skill(2, "runbook", "# Runbook", SkillScope::Global).with_kind(SkillKind::Reference),
);
let requester = agent_id(10);
let agent = manifest_for(
requester,
vec![
SkillRef::new(
SkillId::from_uuid(uuid::Uuid::from_u128(1)),
SkillScope::Project,
),
SkillRef::new(
SkillId::from_uuid(uuid::Uuid::from_u128(99)),
SkillScope::Project,
),
SkillRef::new(
SkillId::from_uuid(uuid::Uuid::from_u128(2)),
SkillScope::Global,
),
],
)
.entries[0]
.to_agent()
.unwrap();
let uc = ResolveAgentCapabilities::new(store);
let capabilities = uc
.execute(ResolveAgentCapabilitiesInput {
agent,
project_root: root(),
})
.await
.unwrap();
assert_eq!(capabilities.len(), 2);
assert_eq!(capabilities[0].name, "deploy");
assert_eq!(capabilities[0].description, "Deploys safely");
assert_eq!(capabilities[0].kind, SkillKind::Workflow);
assert_eq!(capabilities[1].name, "runbook");
assert_eq!(capabilities[1].description, "Runbook");
assert_eq!(capabilities[1].kind, SkillKind::Reference);
}
fn read_skill_uc(
store: Arc<FakeSkillStore>,
manifest: AgentManifest,