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:
@ -31,7 +31,7 @@ use domain::{
|
||||
Handoff, HandoffStore, ManifestEntry, MarkdownDoc, MemoryIndexEntry, MemoryType, NetworkPolicy,
|
||||
NodeId, OrchestrationCapabilitySnapshot, PermissionProjector, Posture, ProfileId, Project,
|
||||
ProjectPath, ProjectedFile, ProjectionContext, ProjectorKey, ProviderSessionStore, PtySize,
|
||||
SessionId, SessionKind, SessionStatus, TerminalSession, HANDOFF_SUMMARY_MAX_CHARS,
|
||||
SessionId, SessionKind, SessionStatus, SkillKind, TerminalSession, HANDOFF_SUMMARY_MAX_CHARS,
|
||||
};
|
||||
|
||||
use domain::live_state::WorkStatus;
|
||||
@ -42,7 +42,7 @@ use crate::model_server::{
|
||||
EnsureLocalModelServer, EnsureLocalModelServerInput, ModelServerUseGuard,
|
||||
};
|
||||
use crate::project::project_context_path;
|
||||
use crate::skill::AssignedSkillResolver;
|
||||
use crate::skill::{AgentCapability, AssignedSkillResolver, ResolveAgentCapabilities};
|
||||
use crate::terminal::{StructuredSessions, TerminalSessions};
|
||||
use crate::workstate::GetLiveStateLean;
|
||||
|
||||
@ -138,6 +138,8 @@ pub struct EffectiveAgentContext {
|
||||
pub project_context: String,
|
||||
/// Provider-agnostic orchestration capabilities exposed to the agent.
|
||||
pub capabilities: OrchestrationCapabilitySnapshot,
|
||||
/// Compact agent capability affordances resolved by [`ResolveAgentCapabilities`].
|
||||
pub agent_capabilities: Vec<AgentCapability>,
|
||||
/// Resolved assigned skill bodies for fallback non-MCP injection.
|
||||
pub assigned_skills: Vec<ResolvedAssignedSkill>,
|
||||
/// Project-memory recall selected for this launch.
|
||||
@ -277,18 +279,79 @@ pub struct ListAgentsInput {
|
||||
pub struct ListAgentsOutput {
|
||||
/// The project's agents (reconstructed from the manifest).
|
||||
pub agents: Vec<Agent>,
|
||||
/// Resolved discoverable capabilities per agent.
|
||||
pub capabilities: Vec<ListedAgentCapabilities>,
|
||||
}
|
||||
|
||||
/// Resolved capabilities for one listed agent.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListedAgentCapabilities {
|
||||
/// Agent id.
|
||||
pub agent_id: AgentId,
|
||||
/// Compact capability list.
|
||||
pub capabilities: Vec<AgentCapability>,
|
||||
}
|
||||
|
||||
/// Agent list item for discovery surfaces.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentDiscoveryEntry {
|
||||
/// Raw agent manifest shape, including `skills` for compatibility.
|
||||
#[serde(flatten)]
|
||||
pub agent: Agent,
|
||||
/// Resolved capability affordances.
|
||||
pub capabilities: Vec<AgentCapability>,
|
||||
}
|
||||
|
||||
impl ListAgentsOutput {
|
||||
/// Returns the additive discovery shape used by inter-agent surfaces.
|
||||
#[must_use]
|
||||
pub fn discovery_entries(&self) -> Vec<AgentDiscoveryEntry> {
|
||||
self.agents
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|agent| {
|
||||
let capabilities = self
|
||||
.capabilities
|
||||
.iter()
|
||||
.find(|entry| entry.agent_id == agent.id)
|
||||
.map(|entry| entry.capabilities.clone())
|
||||
.unwrap_or_default();
|
||||
AgentDiscoveryEntry {
|
||||
agent,
|
||||
capabilities,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists a project's agents by reconstructing them from the manifest entries.
|
||||
pub struct ListAgents {
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
capabilities: Option<Arc<ResolveAgentCapabilities>>,
|
||||
}
|
||||
|
||||
impl ListAgents {
|
||||
/// Builds the use case from the [`AgentContextStore`] port.
|
||||
#[must_use]
|
||||
pub fn new(contexts: Arc<dyn AgentContextStore>) -> Self {
|
||||
Self { contexts }
|
||||
Self {
|
||||
contexts,
|
||||
capabilities: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the use case with resolved agent capabilities enabled.
|
||||
#[must_use]
|
||||
pub fn with_capabilities(
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
capabilities: Arc<ResolveAgentCapabilities>,
|
||||
) -> Self {
|
||||
Self {
|
||||
contexts,
|
||||
capabilities: Some(capabilities),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the manifest and folds each entry back into an [`Agent`].
|
||||
@ -306,7 +369,28 @@ impl ListAgents {
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(ListAgentsOutput { agents })
|
||||
let capabilities = match &self.capabilities {
|
||||
Some(resolver) => {
|
||||
let mut out = Vec::with_capacity(agents.len());
|
||||
for agent in &agents {
|
||||
out.push(ListedAgentCapabilities {
|
||||
agent_id: agent.id,
|
||||
capabilities: resolver
|
||||
.execute(crate::ResolveAgentCapabilitiesInput {
|
||||
agent: agent.clone(),
|
||||
project_root: input.project.root.clone(),
|
||||
})
|
||||
.await?,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
Ok(ListAgentsOutput {
|
||||
agents,
|
||||
capabilities,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1185,6 +1269,7 @@ pub struct LaunchAgent {
|
||||
fs: Arc<dyn FileSystem>,
|
||||
pty: Arc<dyn PtyPort>,
|
||||
assigned_skills: Arc<AssignedSkillResolver>,
|
||||
agent_capabilities: Arc<ResolveAgentCapabilities>,
|
||||
sessions: Arc<TerminalSessions>,
|
||||
events: Arc<dyn EventBus>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
@ -1268,13 +1353,18 @@ impl LaunchAgent {
|
||||
recall: Arc<dyn MemoryRecall>,
|
||||
embedder_suggestion: Option<Arc<crate::embedder::CheckEmbedderSuggestion>>,
|
||||
) -> Self {
|
||||
let assigned_skills = Arc::new(AssignedSkillResolver::new(skills));
|
||||
let agent_capabilities = Arc::new(ResolveAgentCapabilities::with_resolver(Arc::clone(
|
||||
&assigned_skills,
|
||||
)));
|
||||
Self {
|
||||
contexts,
|
||||
profiles,
|
||||
runtime,
|
||||
fs,
|
||||
pty,
|
||||
assigned_skills: Arc::new(AssignedSkillResolver::new(skills)),
|
||||
assigned_skills,
|
||||
agent_capabilities,
|
||||
sessions,
|
||||
events,
|
||||
ids,
|
||||
@ -1433,6 +1523,13 @@ impl LaunchAgent {
|
||||
let capabilities = OrchestrationCapabilitySnapshot::new(
|
||||
assigned_skills.iter().map(|s| s.snapshot.clone()).collect(),
|
||||
);
|
||||
let agent_capabilities = self
|
||||
.agent_capabilities
|
||||
.execute(crate::ResolveAgentCapabilitiesInput {
|
||||
agent: agent.clone(),
|
||||
project_root: project.root.clone(),
|
||||
})
|
||||
.await?;
|
||||
let project_context = self.resolve_project_context(project).await?;
|
||||
let memory = self.resolve_memory(&project.root, persona.as_str()).await;
|
||||
let handoff = self
|
||||
@ -1445,6 +1542,7 @@ impl LaunchAgent {
|
||||
persona,
|
||||
project_context,
|
||||
capabilities,
|
||||
agent_capabilities,
|
||||
assigned_skills,
|
||||
memory,
|
||||
handoff,
|
||||
@ -3320,7 +3418,7 @@ fn append_block(input: &str, block: &str) -> String {
|
||||
/// the given (manifest) order — making the output deterministic:
|
||||
/// - **MCP mode** (`mcp_enabled`): a high-altitude `# Skills disponibles` section,
|
||||
/// right after the orchestration block, listing each as
|
||||
/// `**<name>** — <effective description>` (affordances only, *no body*), with
|
||||
/// `**<name>** — <effective description> (<kind>)` (affordances only, *no body*), with
|
||||
/// prose pointing to `idea_skill_read` to load a body on demand. Respects the
|
||||
/// altitude: the capability is exposed, never the skill content.
|
||||
/// - **Non-MCP mode**: the legacy `# Skills` section dumping each body in full
|
||||
@ -3446,7 +3544,7 @@ pub(crate) fn compose_convention_file(
|
||||
// pour que l'agent sache qu'ils existent et charge le détail à la demande via
|
||||
// `idea_skill_read`. Réservé au mode MCP (le mode sans MCP conserve l'ancien dump
|
||||
// du corps complet en fin de fichier, plus bas). Omis si zéro skill.
|
||||
if mcp_enabled && !effective.capabilities.assigned_skills.is_empty() {
|
||||
if mcp_enabled && !effective.agent_capabilities.is_empty() {
|
||||
out.push_str("# Skills disponibles\n\n");
|
||||
out.push_str("Snapshot version: ");
|
||||
out.push_str(&effective.capabilities.version.to_string());
|
||||
@ -3456,11 +3554,17 @@ pub(crate) fn compose_convention_file(
|
||||
détail, appelle l'outil `idea_skill_read(name=…)` — n'improvise pas un \
|
||||
workflow déjà couvert par un skill, charge-le.\n\n",
|
||||
);
|
||||
for skill in &effective.capabilities.assigned_skills {
|
||||
for skill in &effective.agent_capabilities {
|
||||
out.push_str("**");
|
||||
out.push_str(&skill.name);
|
||||
out.push_str("** — ");
|
||||
out.push_str(&skill.description);
|
||||
out.push_str(" (");
|
||||
out.push_str(match skill.kind {
|
||||
SkillKind::Workflow => "workflow",
|
||||
SkillKind::Reference => "reference",
|
||||
});
|
||||
out.push(')');
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str("\n---\n\n");
|
||||
@ -3654,17 +3758,23 @@ mod tests {
|
||||
scope: skill.scope,
|
||||
name: skill.name.clone(),
|
||||
description: skill.effective_description(),
|
||||
kind: skill.kind,
|
||||
assignment_index: index as u32,
|
||||
},
|
||||
content: skill.content_md.clone(),
|
||||
})
|
||||
.collect();
|
||||
let agent_capabilities = assigned_skills
|
||||
.iter()
|
||||
.map(|skill| AgentCapability::from(&skill.snapshot))
|
||||
.collect();
|
||||
let effective = EffectiveAgentContext {
|
||||
persona: MarkdownDoc::new(agent_md),
|
||||
project_context: project_context.to_owned(),
|
||||
capabilities: OrchestrationCapabilitySnapshot::new(
|
||||
assigned_skills.iter().map(|s| s.snapshot.clone()).collect(),
|
||||
),
|
||||
agent_capabilities,
|
||||
assigned_skills,
|
||||
memory: memory.to_vec(),
|
||||
handoff: handoff.cloned(),
|
||||
@ -3936,9 +4046,9 @@ mod tests {
|
||||
"MCP skills section present"
|
||||
);
|
||||
assert!(doc.contains("idea_skill_read"), "points to the read tool");
|
||||
// Affordance lines: `**name** — <effective description>`.
|
||||
assert!(doc.contains("**refactor** — Refactors code"));
|
||||
assert!(doc.contains("**review** — Review skill"));
|
||||
// Affordance lines: `**name** — <effective description> (<kind>)`.
|
||||
assert!(doc.contains("**refactor** — Refactors code (workflow)"));
|
||||
assert!(doc.contains("**review** — Review skill (workflow)"));
|
||||
// The full bodies are NOT injected in MCP mode (loaded on demand instead).
|
||||
assert!(!doc.contains("REFAC_BODY"), "no full body in MCP mode");
|
||||
assert!(!doc.contains("REVIEW_BODY"), "no full body in MCP mode");
|
||||
|
||||
@ -34,9 +34,9 @@ pub use lifecycle::{
|
||||
resolve_opencode_mcp_timeout_ms, ChangeAgentProfile, ChangeAgentProfileInput,
|
||||
ChangeAgentProfileOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput,
|
||||
DeleteAgent, DeleteAgentInput, HandoffProvider, InjectedLiveRow, LaunchAgent, LaunchAgentInput,
|
||||
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, LiveStateLeanProvider,
|
||||
McpRuntime, PermissionProjectorRegistry, ProviderSessionProvider, ReadAgentContext,
|
||||
ReadAgentContextInput, ReadAgentContextOutput, StructuredRoutingMode,
|
||||
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, ListedAgentCapabilities,
|
||||
LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry, ProviderSessionProvider,
|
||||
ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, StructuredRoutingMode,
|
||||
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput,
|
||||
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||
};
|
||||
|
||||
@ -54,11 +54,11 @@ pub use agent::{
|
||||
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, ListClaudeModels,
|
||||
ListClaudeModelsOutput, ListCodexModels, ListCodexModelsOutput, ListOpenCodeProviders,
|
||||
ListOpenCodeProvidersOutput, ListProfiles, ListProfilesOutput, ListResumableAgents,
|
||||
ListResumableAgentsInput, ListResumableAgentsOutput, LiveStateLeanProvider, McpRuntime,
|
||||
OpenCodeProviderCatalogEntry, PermissionProjectorRegistry, ProfileAvailability,
|
||||
ProfileModelCatalogEntry, ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput,
|
||||
ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput, ResumableAgent,
|
||||
SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
||||
ListResumableAgentsInput, ListResumableAgentsOutput, ListedAgentCapabilities,
|
||||
LiveStateLeanProvider, McpRuntime, OpenCodeProviderCatalogEntry, PermissionProjectorRegistry,
|
||||
ProfileAvailability, ProfileModelCatalogEntry, ProviderSessionProvider, ReadAgentContext,
|
||||
ReadAgentContextInput, ReadAgentContextOutput, ReferenceProfiles, ReferenceProfilesOutput,
|
||||
ResumableAgent, SaveOpenCodeProviderProfile, SaveOpenCodeProviderProfileInput,
|
||||
SaveOpenCodeProviderProfileOutput, SaveProfile, SaveProfileInput, SaveProfileOutput,
|
||||
SessionLimitService, StructuredRoutingMode, StructuredSessionDescriptor, TurnOutcome,
|
||||
UpdateAgentContext, UpdateAgentContextInput, AGENT_MEMORY_RECALL_BUDGET, CODEX_SUBMIT_DELAY_MS,
|
||||
@ -169,10 +169,11 @@ pub use project::{
|
||||
};
|
||||
pub use remote::{ConnectRemote, ConnectRemoteInput, ConnectRemoteOutput};
|
||||
pub use skill::{
|
||||
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,
|
||||
};
|
||||
pub use sprints::{
|
||||
normalized_reorder, AssignTicketToSprint, AssignTicketToSprintInput,
|
||||
|
||||
@ -2687,8 +2687,8 @@ impl OrchestratorService {
|
||||
/// The list is serialised as a JSON array into [`OrchestratorOutcome::reply`]
|
||||
/// (the existing inline-payload channel, also used by `ask`), with a one-line
|
||||
/// count in [`OrchestratorOutcome::detail`]. Each element carries the agent's
|
||||
/// `id`, `name`, `contextPath`, `profileId`, `origin`, `synchronized` and
|
||||
/// `skills` (camelCase, the [`domain::Agent`] serde shape).
|
||||
/// `id`, `name`, `contextPath`, `profileId`, `origin`, `synchronized`,
|
||||
/// `skills` and the additive resolved `capabilities` list.
|
||||
///
|
||||
/// # Errors
|
||||
/// Propagates [`AppError`] from the use case (manifest load / invariant) or a
|
||||
@ -2701,11 +2701,12 @@ impl OrchestratorService {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let reply = serde_json::to_string(&listed.agents)
|
||||
let entries = listed.discovery_entries();
|
||||
let reply = serde_json::to_string(&entries)
|
||||
.map_err(|e| AppError::Invalid(format!("failed to serialise agent list: {e}")))?;
|
||||
|
||||
Ok(OrchestratorOutcome {
|
||||
detail: format!("listed {} agent(s)", listed.agents.len()),
|
||||
detail: format!("listed {} agent(s)", entries.len()),
|
||||
reply: Some(reply),
|
||||
})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -827,6 +827,43 @@ async fn list_reconstructs_agents_from_manifest() {
|
||||
assert_eq!(out.agents, vec![a]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_resolves_agent_capabilities_additively() {
|
||||
let mut a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let skill_id = SkillId::from_uuid(uuid::Uuid::from_u128(77));
|
||||
a.assign_skill(domain::SkillRef::new(skill_id, domain::SkillScope::Global));
|
||||
let contexts = FakeContexts::with_agent(&a, "ctx");
|
||||
let skills = FakeSkills::with(vec![Skill::new(
|
||||
skill_id,
|
||||
"review",
|
||||
MarkdownDoc::new("# Review workflow\n\nbody"),
|
||||
domain::SkillScope::Global,
|
||||
)
|
||||
.unwrap()
|
||||
.with_description(Some("Reviews changes".to_owned()))
|
||||
.with_kind(domain::SkillKind::Reference)]);
|
||||
let resolver = Arc::new(application::ResolveAgentCapabilities::new(Arc::new(skills)));
|
||||
let list = ListAgents::with_capabilities(Arc::new(contexts), resolver);
|
||||
|
||||
let out = list
|
||||
.execute(ListAgentsInput { project: project() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.agents, vec![a.clone()]);
|
||||
let entries = out.discovery_entries();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].agent.id, a.id);
|
||||
assert_eq!(entries[0].agent.skills.len(), 1);
|
||||
assert_eq!(entries[0].capabilities.len(), 1);
|
||||
assert_eq!(entries[0].capabilities[0].name, "review");
|
||||
assert_eq!(entries[0].capabilities[0].description, "Reviews changes");
|
||||
assert_eq!(
|
||||
entries[0].capabilities[0].kind,
|
||||
domain::SkillKind::Reference
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_then_update_context_roundtrips() {
|
||||
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
|
||||
Reference in New Issue
Block a user