Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Clément Sirieix 2026-04-21 15:19:59 +02:00 committed by GitHub
parent 95336528f6
commit 04305bd77c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 3473 additions and 1764 deletions

View file

@ -5,7 +5,6 @@ from typing import ClassVar
from pydantic import BaseModel, Field
from vibe.core.skills.parser import SkillParseError, parse_frontmatter
from vibe.core.tools.base import (
BaseTool,
BaseToolConfig,
@ -14,14 +13,9 @@ from vibe.core.tools.base import (
ToolError,
ToolPermission,
)
from vibe.core.tools.permissions import (
PermissionContext,
PermissionScope,
RequiredPermission,
)
from vibe.core.tools.permissions import PermissionContext
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
from vibe.core.types import ToolResultEvent, ToolStreamEvent
from vibe.core.utils.io import read_safe
_MAX_LISTED_FILES = 10
@ -33,11 +27,13 @@ class SkillArgs(BaseModel):
class SkillResult(BaseModel):
name: str = Field(description="The name of the loaded skill")
content: str = Field(description="The full skill content block")
skill_dir: str = Field(description="Absolute path to the skill directory")
skill_dir: str | None = Field(
default=None, description="Absolute path to the skill directory when available"
)
class SkillToolConfig(BaseToolConfig):
permission: ToolPermission = ToolPermission.ASK
permission: ToolPermission = ToolPermission.ALWAYS
class Skill(
@ -71,17 +67,7 @@ class Skill(
return "Loading skill"
def resolve_permission(self, args: SkillArgs) -> PermissionContext | None:
return PermissionContext(
permission=self.config.permission,
required_permissions=[
RequiredPermission(
scope=PermissionScope.FILE_PATTERN,
invocation_pattern=args.name,
session_pattern=args.name,
label=f"Load skill: {args.name}",
)
],
)
return PermissionContext(permission=ToolPermission.ALWAYS)
async def run(
self, args: SkillArgs, ctx: InvokeContext | None = None
@ -98,36 +84,36 @@ class Skill(
f'Skill "{args.name}" not found. Available skills: {available or "none"}'
)
try:
raw = read_safe(skill_info.skill_path).text
_, body = parse_frontmatter(raw)
except (OSError, SkillParseError) as e:
raise ToolError(f"Cannot load skill file: {e}") from e
skill_dir = skill_info.skill_dir
files: list[str] = []
try:
for entry in sorted(skill_dir.rglob("*")):
if not entry.is_file():
continue
if entry.name == "SKILL.md":
continue
files.append(str(entry.relative_to(skill_dir)))
if len(files) >= _MAX_LISTED_FILES:
break
except OSError:
pass
if skill_dir is not None:
try:
for entry in sorted(skill_dir.rglob("*")):
if not entry.is_file():
continue
if entry.name == "SKILL.md":
continue
files.append(str(entry.relative_to(skill_dir)))
if len(files) >= _MAX_LISTED_FILES:
break
except OSError:
pass
file_lines = "\n".join(f"<file>{f}</file>" for f in files)
base_dir_lines: list[str] = []
if skill_dir is not None:
base_dir_lines = [
f"Base directory for this skill: {skill_dir}",
"Relative paths in this skill are relative to this base directory.",
]
output = "\n".join([
f'<skill_content name="{args.name}">',
f"# Skill: {args.name}",
"",
body.strip(),
skill_info.prompt.strip(),
"",
f"Base directory for this skill: {skill_dir}",
"Relative paths in this skill are relative to this base directory.",
*base_dir_lines,
"Note: file list is sampled.",
"",
"<skill_files>",
@ -136,4 +122,5 @@ class Skill(
"</skill_content>",
])
yield SkillResult(name=args.name, content=output, skill_dir=str(skill_dir))
resolved_skill_dir = None if skill_dir is None else str(skill_dir)
yield SkillResult(name=args.name, content=output, skill_dir=resolved_skill_dir)