-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix: Agent Skills gaps - approval scoping, progressive disclosure, observability, budget, safer defaults (fixes #1478) #1481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1867b61
e42e093
4bd60d3
6c44baa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| """Agent Skills activation protocol for progressive disclosure.""" | ||
|
|
||
| from typing import Protocol, Optional | ||
|
|
||
|
|
||
| class SkillActivationProtocol(Protocol): | ||
| """Protocol for progressive disclosure skill activation. | ||
|
|
||
| This protocol defines the interface for activating skills on demand, | ||
| supporting Claude Code-style progressive disclosure where only skill | ||
| descriptions are shown in the system prompt initially, with full | ||
| bodies loaded when needed. | ||
| """ | ||
|
|
||
| def activate(self, name: str, arguments: str = "", session_id: Optional[str] = None) -> str: | ||
| """Activate a skill and return its rendered body. | ||
|
|
||
| Args: | ||
| name: Name of the skill to activate | ||
| arguments: Arguments to substitute in the skill body | ||
| session_id: Optional session identifier for context | ||
|
|
||
| Returns: | ||
| Rendered skill body with arguments substituted | ||
|
|
||
| Raises: | ||
| ValueError: If skill is not found or not user-invocable | ||
| """ | ||
| ... | ||
|
Comment on lines
+6
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Move this protocol into The interface is correct, but extension-point protocols are expected to live in the module’s ♻️ Proposed structure-# src/praisonai-agents/praisonaiagents/skills/activation.py
-class SkillActivationProtocol(Protocol):
+// src/praisonai-agents/praisonaiagents/skills/protocols.py
+class SkillActivationProtocol(Protocol):
...Then update lazy exports to import As per coding guidelines, 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """Agent Skills prompt budget management. | ||
|
|
||
| Controls how many skills and how much content gets included in | ||
| the system prompt to prevent unbounded growth with large skill libraries. | ||
| """ | ||
|
|
||
| import html | ||
| from dataclasses import dataclass | ||
| from typing import Literal, List | ||
| from .models import SkillMetadata | ||
|
|
||
| # Maximum description chars before truncation (from prompt.py) | ||
| MAX_COMBINED_DESCRIPTION_CHARS = 1536 | ||
|
|
||
|
|
||
| def _truncate(text: str, limit: int) -> str: | ||
| """Truncate text to limit (same logic as prompt.py).""" | ||
| if text is None: | ||
| return "" | ||
| if len(text) <= limit: | ||
| return text | ||
| return text[: max(0, limit - 1)].rstrip() + "\u2026" | ||
|
|
||
|
|
||
| def _estimate_skill_xml_chars(skill: SkillMetadata) -> int: | ||
| """Estimate the rendered XML character count for a skill.""" | ||
| name = html.escape(skill.name) | ||
| description = html.escape(_truncate(skill.description, MAX_COMBINED_DESCRIPTION_CHARS)) | ||
| location = html.escape(skill.location or "") | ||
|
|
||
| # Exact XML format from format_skill_for_prompt | ||
| xml_content = f""" <skill> | ||
| <name>{name}</name> | ||
| <description>{description}</description> | ||
| <location>{location}</location> | ||
| </skill>""" | ||
|
|
||
| return len(xml_content) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class SkillPromptBudget: | ||
| """Budget constraints for skills included in system prompts. | ||
|
|
||
| Prevents unbounded system prompt growth when agents have access | ||
| to large skill libraries. | ||
| """ | ||
| max_chars: int = 4096 | ||
| max_skills: int = 50 | ||
| strategy: Literal["priority", "fifo", "alpha"] = "fifo" | ||
|
|
||
|
|
||
| def apply_budget(skills: List[SkillMetadata], budget: SkillPromptBudget) -> tuple[List[SkillMetadata], bool]: | ||
| """Apply budget constraints to a list of skills. | ||
|
|
||
| Args: | ||
| skills: List of skill metadata to potentially include | ||
| budget: Budget constraints to apply | ||
|
|
||
| Returns: | ||
| Tuple of (filtered_skills, was_truncated) | ||
| """ | ||
| if not skills: | ||
| return skills, False | ||
|
|
||
| # Apply ordering strategy first, before selecting the bounded subset | ||
| if budget.strategy == "alpha": | ||
| ordered_skills = sorted(skills, key=lambda s: s.name) | ||
| elif budget.strategy == "fifo": | ||
| ordered_skills = skills | ||
| else: | ||
| # priority strategy would require skill metadata to include priority field; | ||
| # for now, treat as fifo | ||
| ordered_skills = skills | ||
|
|
||
| # Apply skill count limit after ordering | ||
| limited_skills = ordered_skills[:budget.max_skills] | ||
| skill_count_truncated = len(limited_skills) < len(skills) | ||
|
|
||
| total_chars = 0 | ||
| filtered_skills = [] | ||
| char_truncated = False | ||
|
|
||
| for skill in limited_skills: | ||
| skill_chars = _estimate_skill_xml_chars(skill) | ||
| if total_chars + skill_chars > budget.max_chars: | ||
| char_truncated = True | ||
| break | ||
| filtered_skills.append(skill) | ||
| total_chars += skill_chars | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| was_truncated = skill_count_truncated or char_truncated | ||
| return filtered_skills, was_truncated | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| """Agent Skills observability events for telemetry and monitoring.""" | ||
|
|
||
| from dataclasses import dataclass | ||
| from typing import Literal | ||
|
|
||
|
|
||
| @dataclass | ||
| class SkillDiscoveredEvent: | ||
| """Event emitted when a skill is discovered during skill directory scanning. | ||
|
|
||
| This event provides visibility into which skills are found and from | ||
| what sources during the discovery phase. | ||
| """ | ||
| agent: str | ||
| skill_name: str | ||
| source: str # Directory path or source identifier | ||
| description_chars: int # Length of skill description | ||
|
|
||
|
|
||
| @dataclass | ||
| class SkillActivatedEvent: | ||
| """Event emitted when a skill is activated (full body loaded/rendered). | ||
|
|
||
| This event tracks skill usage patterns and performance metrics | ||
| for skill invocation. | ||
| """ | ||
| agent: str | ||
| skill_name: str | ||
| trigger: Literal["slash", "activate_tool", "auto"] | ||
| arguments: str | ||
| rendered_chars: int | ||
| session_id: str | None = None | ||
| activation_time_ms: float | None = None | ||
|
Comment on lines
+7
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add correlation metadata and avoid raw user data in telemetry events. These observability events are hard to join across traces without a correlation/session timestamp, and 🛡️ Possible shape for safer event payloads-from dataclasses import dataclass
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
from typing import Literal
+
+
+def _utc_now_iso() -> str:
+ return datetime.now(timezone.utc).isoformat()
`@dataclass`
class SkillDiscoveredEvent:
@@
agent: str
skill_name: str
- source: str # Directory path or source identifier
+ source_type: str # e.g. "project", "user", "package"
description_chars: int # Length of skill description
+ correlation_id: str | None = None
+ event_source: str = "skills"
+ timestamp: str = field(default_factory=_utc_now_iso)
@@
agent: str
skill_name: str
trigger: Literal["slash", "activate_tool", "auto"]
- arguments: str
+ argument_chars: int = 0
rendered_chars: int
session_id: str | None = None
activation_time_ms: float | None = None
+ correlation_id: str | None = None
+ event_source: str = "skills"
+ timestamp: str = field(default_factory=_utc_now_iso)Based on learnings, emit events via EventBus for all key operations to enable hooks and observability, with structured event objects containing correlationId, timestamp, and source information. 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use an
orfallback fordisplay_name.Line 1075 only falls back to
self.namewhendisplay_nameis missing; ifdisplay_nameexists but isNoneor"",agent_namestays falsey and skips all skill tool pre-approvals.🐛 Proposed fix
🤖 Prompt for AI Agents