Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions api/oss/src/core/workflows/static_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
GETTING_STARTED_WITH_AGENTA_SKILL,
GETTING_STARTED_WITH_AGENTA_SLUG,
)
from agenta.sdk.agents.skills.models import SkillConfig
from agenta.sdk.agents.skills.models import SkillTemplate
from agenta.sdk.engines.running.utils import (
AGENTA_BUILTIN_SKILL_URI,
infer_flags_from_data,
Expand Down Expand Up @@ -63,17 +63,17 @@
# snippet carrying uri + parameters).


def _skill_revision(skill_config: SkillConfig) -> WorkflowRevision:
def _skill_revision(skill_template: SkillTemplate) -> WorkflowRevision:
"""A static skill as a full WorkflowRevision. The skill content is canonical in the SDK
(agenta_builtins), imported here so the embed path (this catalogue) and the forced path
(AgentaHarness) stay one source. Structural fields (ids / slug / version) and flags are filled
by the catalogue on resolution."""
return WorkflowRevision(
name=skill_config.name,
description=skill_config.description,
name=skill_template.name,
description=skill_template.description,
data=WorkflowRevisionData(
uri=AGENTA_BUILTIN_SKILL_URI,
parameters={"skill": skill_config.model_dump(mode="json")},
parameters={"skill": skill_template.model_dump(mode="json")},
),
)

Expand Down
16 changes: 8 additions & 8 deletions sdks/python/agenta/sdk/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@
ResolvedMCPServer,
)
from .skills import (
SkillConfig,
SkillConfigurationError,
SkillTemplate,
SkillValidationError,
SkillError,
SkillFile,
parse_skill_config,
parse_skill_configs,
parse_skill_template,
parse_skill_templates,
skill_to_wire,
skills_to_wire,
)
Expand Down Expand Up @@ -219,14 +219,14 @@
"MCPDisabledError",
"MissingMCPSecretError",
# Skills are a sibling subsystem
"SkillConfig",
"SkillTemplate",
"SkillFile",
"parse_skill_config",
"parse_skill_configs",
"parse_skill_template",
"parse_skill_templates",
"skill_to_wire",
"skills_to_wire",
"SkillError",
"SkillConfigurationError",
"SkillValidationError",
# Connections are a sibling subsystem (provider / model / auth)
"ModelRef",
"Connection",
Expand Down
14 changes: 7 additions & 7 deletions sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

from typing import List, Optional

from ..skills import SkillConfig
from ..skills import SkillTemplate

# The base AGENTS.md preamble. The author's own ``instructions`` are appended after this, so
# the final AGENTS.md is ``AGENTA_PREAMBLE`` + the author's project conventions.
Expand Down Expand Up @@ -63,7 +63,7 @@

# Reserved slug of the platform default skill. The default agent config template embeds the
# skill by this slug; the server-side StaticWorkflowCatalog resolves the slug to the
# SkillConfig below. Kept here so the catalogue and the forced path share one slug constant.
# SkillTemplate below. Kept here so the catalogue and the forced path share one slug constant.
GETTING_STARTED_WITH_AGENTA_SLUG = "__ag__getting_started_with_agenta"

# Canonical SKILL.md body for the platform "getting started" skill. Single source of the body
Expand All @@ -89,8 +89,8 @@
)

# The platform default skill as a concrete inline package. This is the canonical content; the
# server-side catalogue serves the same SkillConfig for the reserved slug above.
GETTING_STARTED_WITH_AGENTA_SKILL = SkillConfig(
# server-side catalogue serves the same SkillTemplate for the reserved slug above.
GETTING_STARTED_WITH_AGENTA_SKILL = SkillTemplate(
name="agenta-getting-started",
description=(
"Getting started on the Agenta platform: how an Agenta agent should behave, ask for "
Expand All @@ -101,7 +101,7 @@

# Platform skills every pi_agenta run carries, regardless of the author's config. These are the
# actually-forced skills (see module docstring); unioned in by `force_skills`.
AGENTA_FORCED_SKILLS: List[SkillConfig] = [GETTING_STARTED_WITH_AGENTA_SKILL]
AGENTA_FORCED_SKILLS: List[SkillTemplate] = [GETTING_STARTED_WITH_AGENTA_SKILL]


def _join(*parts: Optional[str]) -> Optional[str]:
Expand Down Expand Up @@ -136,15 +136,15 @@ def force_tools(builtin_tools: List[str]) -> List[str]:
return out


def force_skills(skills: List[SkillConfig]) -> List[SkillConfig]:
def force_skills(skills: List[SkillTemplate]) -> List[SkillTemplate]:
"""Union the author's skills with the forced platform skills, de-duplicated by name.

The author's skills come first and win on a name clash (a config that already carries the
resolved platform skill — e.g. via the default template's embed — is not doubled), then any
forced platform skill not already present is appended. This is what makes the ``_agenta``
platform skill actually forced on a custom ``pi_agenta`` config that drops the embed."""
seen = {skill.name for skill in skills}
out: List[SkillConfig] = list(skills)
out: List[SkillTemplate] = list(skills)
for forced in AGENTA_FORCED_SKILLS:
if forced.name not in seen:
seen.add(forced.name)
Expand Down
18 changes: 10 additions & 8 deletions sdks/python/agenta/sdk/agents/dtos.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
mcp_servers_to_wire,
parse_mcp_server_configs,
)
from .skills import SkillConfig, parse_skill_configs, skills_to_wire
from .skills import SkillTemplate, parse_skill_templates, skills_to_wire
from .tools import ToolCallback, ToolConfig, ToolSpec, coerce_tool_configs
from .tools.models import coerce_tool_spec

Expand Down Expand Up @@ -526,7 +526,7 @@ class AgentConfig(BaseModel):
model_ref: Optional[ModelRef] = None
tools: List[ToolConfig] = Field(default_factory=list)
mcp_servers: List[MCPServerConfig] = Field(default_factory=list)
skills: List[SkillConfig] = Field(default_factory=list)
skills: List[SkillTemplate] = Field(default_factory=list)
harness_kwargs: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
sandbox_permission: Optional[SandboxPermission] = None
# The run-selection fields (formerly the separate ``RunSelection``): the coding agent to
Expand Down Expand Up @@ -554,8 +554,8 @@ def _coerce_mcp_servers(cls, value: Any) -> List[MCPServerConfig]:

@field_validator("skills", mode="before")
@classmethod
def _coerce_skills(cls, value: Any) -> List[SkillConfig]:
return parse_skill_configs(_as_list(value))
def _coerce_skills(cls, value: Any) -> List[SkillTemplate]:
return parse_skill_templates(_as_list(value))

@classmethod
def from_params(
Expand Down Expand Up @@ -625,7 +625,7 @@ class HarnessAgentConfig(BaseModel):
resolved_connection: Optional[ResolvedConnection] = None
tool_callback: Optional[ToolCallback] = None
mcp_servers: List[ResolvedMCPServer] = Field(default_factory=list)
skills: List[SkillConfig] = Field(default_factory=list)
skills: List[SkillTemplate] = Field(default_factory=list)
sandbox_permission: Optional[SandboxPermission] = None
# The neutral per-harness options bag (a map keyed by harness name), carried verbatim from
# ``AgentConfig.harness_kwargs`` by the harness adapter. The active harness's CONFIG translates
Expand All @@ -650,9 +650,11 @@ def _coerce_resolved_mcp_servers(cls, value: Any) -> List[ResolvedMCPServer]:

@field_validator("skills", mode="before")
@classmethod
def _coerce_skills(cls, value: Any) -> List[SkillConfig]:
def _coerce_skills(cls, value: Any) -> List[SkillTemplate]:
return [
item if isinstance(item, SkillConfig) else SkillConfig.model_validate(item)
item
if isinstance(item, SkillTemplate)
else SkillTemplate.model_validate(item)
for item in value or []
]

Expand Down Expand Up @@ -998,7 +1000,7 @@ def _parse_skills_raw(

Reads ``skills`` from the ``agent`` element when present, else the flat request. Mirrors
the MCP path so an unparsed ``skills`` is not silently dropped; canonical validation happens
on :class:`AgentConfig` construction. Each entry is a concrete inline ``SkillConfig`` by the
on :class:`AgentConfig` construction. Each entry is a concrete inline ``SkillTemplate`` by the
time the request is built (any ``@ag.embed`` reference resolved server-side first)."""
agent = params.get("agent")
source = agent if isinstance(agent, dict) else params
Expand Down
16 changes: 8 additions & 8 deletions sdks/python/agenta/sdk/agents/skills/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
"""Public skill configuration API.

A skill is one inline shape (:class:`SkillConfig`); references to skills that live elsewhere
A skill is one inline shape (:class:`SkillTemplate`); references to skills that live elsewhere
ride the existing ``@ag.embed`` mechanism and resolve into this same shape before the runner.
"""

from .errors import SkillConfigurationError, SkillError
from .models import SkillConfig, SkillFile
from .parsing import parse_skill_config, parse_skill_configs
from .errors import SkillValidationError, SkillError
from .models import SkillTemplate, SkillFile
from .parsing import parse_skill_template, parse_skill_templates
from .wire import skill_to_wire, skills_to_wire

__all__ = [
"SkillConfig",
"SkillTemplate",
"SkillFile",
"parse_skill_config",
"parse_skill_configs",
"parse_skill_template",
"parse_skill_templates",
"skill_to_wire",
"skills_to_wire",
"SkillError",
"SkillConfigurationError",
"SkillValidationError",
]
2 changes: 1 addition & 1 deletion sdks/python/agenta/sdk/agents/skills/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SkillError(RuntimeError):
"""Base error for the agent skills subsystem."""


class SkillConfigurationError(SkillError):
class SkillValidationError(SkillError):
def __init__(
self,
message: str,
Expand Down
4 changes: 2 additions & 2 deletions sdks/python/agenta/sdk/agents/skills/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
def _validate_safe_skill_file_path(path: str) -> str:
"""Reject a bundled-file ``path`` that is absolute, escapes the skill dir, or collides with
the composed ``SKILL.md``. Enforced on the model itself (not only in ``parsing.py``) so every
construction path — direct ``SkillFile(...)`` / ``SkillConfig(...)`` included — is safe."""
construction path — direct ``SkillFile(...)`` / ``SkillTemplate(...)`` included — is safe."""
if path.startswith("/") or path.startswith("\\"):
raise ValueError(
f"Skill file path must be relative, got absolute path: {path!r}"
Expand Down Expand Up @@ -74,7 +74,7 @@ def to_wire(self) -> Dict[str, Any]:
}


class SkillConfig(BaseModel):
class SkillTemplate(BaseModel):
"""An inline skill package. The SKILL.md frontmatter + body and any bundled files ride the
wire as content; the runner materializes them into a skill dir at run time. ``name`` and
``description`` are the two portable frontmatter fields; ``body`` is this skill's own
Expand Down
34 changes: 17 additions & 17 deletions sdks/python/agenta/sdk/agents/skills/parsing.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Strict parsing of inline skill configuration.

Parses a raw ``skills`` list (entries are either :class:`SkillConfig` or plain dicts, the
latter being the post-embed-resolution shape) into validated :class:`SkillConfig` objects.
Parses a raw ``skills`` list (entries are either :class:`SkillTemplate` or plain dicts, the
latter being the post-embed-resolution shape) into validated :class:`SkillTemplate` objects.

The actual rules — the name pattern, field bounds, and the safe-relative-file-path /
``SKILL.md`` checks — live on the Pydantic models (:mod:`.models`), so *every* construction path
(including a direct ``SkillConfig(...)``) enforces them. This module only adapts the model's
:class:`~pydantic.ValidationError` into a :class:`SkillConfigurationError` that carries the
(including a direct ``SkillTemplate(...)``) enforces them. This module only adapts the model's
:class:`~pydantic.ValidationError` into a :class:`SkillValidationError` that carries the
offending list index.
"""

Expand All @@ -16,8 +16,8 @@

from pydantic import ValidationError

from .errors import SkillConfigurationError
from .models import SkillConfig
from .errors import SkillValidationError
from .models import SkillTemplate

# Embed markers the server-side resolver inlines before the runner. If one survives to here,
# resolution was skipped (e.g. `flags.resolve=False`), so we raise a clear, typed error rather
Expand Down Expand Up @@ -60,29 +60,29 @@ def _unresolved_embed_message(value: Any) -> str | None:
return None


def parse_skill_config(value: SkillConfig | Mapping[str, Any]) -> SkillConfig:
def parse_skill_template(value: SkillTemplate | Mapping[str, Any]) -> SkillTemplate:
message = _unresolved_embed_message(value)
if message is not None:
raise SkillConfigurationError(message, value=value)
raise SkillValidationError(message, value=value)
try:
return SkillConfig.model_validate(value)
return SkillTemplate.model_validate(value)
except ValidationError as exc:
raise SkillConfigurationError(
raise SkillValidationError(
"Invalid skill configuration: "
f"{exc.errors(include_url=False, include_input=False)}",
value=value,
) from exc


def parse_skill_configs(
values: Sequence[SkillConfig | Mapping[str, Any]],
) -> list[SkillConfig]:
parsed: list[SkillConfig] = []
def parse_skill_templates(
values: Sequence[SkillTemplate | Mapping[str, Any]],
) -> list[SkillTemplate]:
parsed: list[SkillTemplate] = []
for index, value in enumerate(values):
try:
parsed.append(parse_skill_config(value))
except SkillConfigurationError as exc:
raise SkillConfigurationError(
parsed.append(parse_skill_template(value))
except SkillValidationError as exc:
raise SkillValidationError(
str(exc),
index=index,
value=value,
Expand Down
8 changes: 4 additions & 4 deletions sdks/python/agenta/sdk/agents/skills/wire.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Serialization of resolved skills to the runner contract.

By the time the wire is built every entry is a concrete :class:`SkillConfig` (references
By the time the wire is built every entry is a concrete :class:`SkillTemplate` (references
resolved server-side via ``@ag.embed``), so there is one shape to emit: ``WireSkill`` (see
``services/agent/src/protocol.ts``).
"""
Expand All @@ -9,12 +9,12 @@

from typing import Any, Dict, Sequence

from .models import SkillConfig
from .models import SkillTemplate


def skill_to_wire(skill: SkillConfig) -> Dict[str, Any]:
def skill_to_wire(skill: SkillTemplate) -> Dict[str, Any]:
return skill.to_wire()


def skills_to_wire(skills: Sequence[SkillConfig]) -> list[Dict[str, Any]]:
def skills_to_wire(skills: Sequence[SkillTemplate]) -> list[Dict[str, Any]]:
return [skill_to_wire(skill) for skill in skills]
18 changes: 14 additions & 4 deletions sdks/python/agenta/sdk/engines/running/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any, Dict, List, Optional, Tuple, Callable

from agenta.sdk.models.workflows import (
JsonSchemas,
WorkflowFlags,
WorkflowRevisionData,
)
Expand Down Expand Up @@ -642,17 +643,26 @@ def is_static_workflow_slug(slug: Optional[str]) -> bool:
def normalize_snippet_data(
data: Optional[WorkflowRevisionData],
) -> Optional[WorkflowRevisionData]:
"""For a non-runnable snippet revision (a skill, for now), keep only ``uri`` + ``parameters``.
"""For a non-runnable snippet revision (a skill, for now), keep ``uri`` + ``parameters`` and
only the ``parameters`` schema.

url / headers / runtime / script / schemas are all execution-surface concerns a snippet has
none of, so they are stripped. No-op for any runnable revision.
url / headers / runtime / script are execution-surface concerns a snippet has none of, so they
are stripped. A snippet is non-runnable: it has no inputs/outputs, so ``schemas`` keeps only
``parameters`` (the shape that describes the snippet's content). No-op for any runnable revision.
"""
if not data or not data.uri:
return data
_, _, key, _ = parse_uri(data.uri)
if key != "skill":
return data
return WorkflowRevisionData(uri=data.uri, parameters=data.parameters)
schemas = None
if data.schemas is not None and data.schemas.parameters is not None:
schemas = JsonSchemas(parameters=data.schemas.parameters)
return WorkflowRevisionData(
uri=data.uri,
parameters=data.parameters,
schemas=schemas,
)


def _has_messages_input(inputs_schema: Optional[Dict[str, Any]]) -> bool:
Expand Down
Loading
Loading