From d1cb69a849b8dd0628a20d5caca7670448d31044 Mon Sep 17 00:00:00 2001 From: Juan Pablo Vega Date: Sun, 28 Jun 2026 10:33:28 +0200 Subject: [PATCH] [fix] Clean up Skill Template --- api/oss/src/core/workflows/static_catalog.py | 10 ++-- sdks/python/agenta/sdk/agents/__init__.py | 16 +++---- .../sdk/agents/adapters/agenta_builtins.py | 14 +++--- sdks/python/agenta/sdk/agents/dtos.py | 18 ++++---- .../agenta/sdk/agents/skills/__init__.py | 16 +++---- .../python/agenta/sdk/agents/skills/errors.py | 2 +- .../python/agenta/sdk/agents/skills/models.py | 4 +- .../agenta/sdk/agents/skills/parsing.py | 34 +++++++------- sdks/python/agenta/sdk/agents/skills/wire.py | 8 ++-- .../agenta/sdk/engines/running/utils.py | 18 ++++++-- sdks/python/agenta/sdk/utils/types.py | 39 ++++++++++++---- .../agents/test_transport_roundtrip.py | 4 +- .../pytest/unit/agents/skills/test_models.py | 26 +++++------ .../pytest/unit/agents/skills/test_parsing.py | 38 +++++++-------- .../unit/agents/skills/test_skills_e2e.py | 10 ++-- .../pytest/unit/agents/skills/test_wire.py | 10 ++-- .../unit/agents/test_harness_adapters.py | 4 +- .../pytest/unit/agents/test_wire_contract.py | 4 +- .../oss/tests/pytest/unit/test_skill_flags.py | 29 ++++++++++-- ...alog.py => test_skill_template_catalog.py} | 28 +++++------ services/oss/src/agent/schemas.py | 2 +- .../SchemaControls/AgentConfigControl.tsx | 44 +++++++++--------- .../SchemaControls/SkillFormView.tsx | 2 +- ...igControl.tsx => SkillTemplateControl.tsx} | 42 ++++++++--------- .../src/DrillInView/SchemaControls/index.ts | 4 +- .../DrillInView/SchemaControls/skillUpload.ts | 2 +- ...l.test.ts => skillTemplateControl.test.ts} | 46 +++++++++---------- 27 files changed, 265 insertions(+), 209 deletions(-) rename sdks/python/oss/tests/pytest/unit/{test_skill_config_catalog.py => test_skill_template_catalog.py} (84%) rename web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/{SkillConfigControl.tsx => SkillTemplateControl.tsx} (86%) rename web/packages/agenta-entity-ui/tests/unit/{skillConfigControl.test.ts => skillTemplateControl.test.ts} (63%) diff --git a/api/oss/src/core/workflows/static_catalog.py b/api/oss/src/core/workflows/static_catalog.py index 1ad8cb65ea..af42b73810 100644 --- a/api/oss/src/core/workflows/static_catalog.py +++ b/api/oss/src/core/workflows/static_catalog.py @@ -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, @@ -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")}, ), ) diff --git a/sdks/python/agenta/sdk/agents/__init__.py b/sdks/python/agenta/sdk/agents/__init__.py index da405ed6d6..96c0bf58dd 100644 --- a/sdks/python/agenta/sdk/agents/__init__.py +++ b/sdks/python/agenta/sdk/agents/__init__.py @@ -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, ) @@ -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", diff --git a/sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py b/sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py index ca628f6619..a8c91d7a65 100644 --- a/sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py +++ b/sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py @@ -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. @@ -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 @@ -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 " @@ -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]: @@ -136,7 +136,7 @@ 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 @@ -144,7 +144,7 @@ def force_skills(skills: List[SkillConfig]) -> List[SkillConfig]: 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) diff --git a/sdks/python/agenta/sdk/agents/dtos.py b/sdks/python/agenta/sdk/agents/dtos.py index 51e8324bf7..bd19e3109d 100644 --- a/sdks/python/agenta/sdk/agents/dtos.py +++ b/sdks/python/agenta/sdk/agents/dtos.py @@ -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 @@ -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 @@ -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( @@ -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 @@ -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 [] ] @@ -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 diff --git a/sdks/python/agenta/sdk/agents/skills/__init__.py b/sdks/python/agenta/sdk/agents/skills/__init__.py index d896cda077..c0f805f5b9 100644 --- a/sdks/python/agenta/sdk/agents/skills/__init__.py +++ b/sdks/python/agenta/sdk/agents/skills/__init__.py @@ -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", ] diff --git a/sdks/python/agenta/sdk/agents/skills/errors.py b/sdks/python/agenta/sdk/agents/skills/errors.py index 267b2b0c51..237cff3770 100644 --- a/sdks/python/agenta/sdk/agents/skills/errors.py +++ b/sdks/python/agenta/sdk/agents/skills/errors.py @@ -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, diff --git a/sdks/python/agenta/sdk/agents/skills/models.py b/sdks/python/agenta/sdk/agents/skills/models.py index 5e96b5ebbc..da82e1c237 100644 --- a/sdks/python/agenta/sdk/agents/skills/models.py +++ b/sdks/python/agenta/sdk/agents/skills/models.py @@ -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}" @@ -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 diff --git a/sdks/python/agenta/sdk/agents/skills/parsing.py b/sdks/python/agenta/sdk/agents/skills/parsing.py index 8f23a1de4e..ca02759e82 100644 --- a/sdks/python/agenta/sdk/agents/skills/parsing.py +++ b/sdks/python/agenta/sdk/agents/skills/parsing.py @@ -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. """ @@ -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 @@ -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, diff --git a/sdks/python/agenta/sdk/agents/skills/wire.py b/sdks/python/agenta/sdk/agents/skills/wire.py index 421f44e2a3..59b9ef36bd 100644 --- a/sdks/python/agenta/sdk/agents/skills/wire.py +++ b/sdks/python/agenta/sdk/agents/skills/wire.py @@ -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``). """ @@ -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] diff --git a/sdks/python/agenta/sdk/engines/running/utils.py b/sdks/python/agenta/sdk/engines/running/utils.py index 438a11e2f5..c0d8e07478 100644 --- a/sdks/python/agenta/sdk/engines/running/utils.py +++ b/sdks/python/agenta/sdk/engines/running/utils.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple, Callable from agenta.sdk.models.workflows import ( + JsonSchemas, WorkflowFlags, WorkflowRevisionData, ) @@ -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: diff --git a/sdks/python/agenta/sdk/utils/types.py b/sdks/python/agenta/sdk/utils/types.py index 24430b26e7..00dcf9ab46 100644 --- a/sdks/python/agenta/sdk/utils/types.py +++ b/sdks/python/agenta/sdk/utils/types.py @@ -1184,12 +1184,12 @@ class AgentConfigSchema(AgSchemaMixin): "enforcement (strict or best-effort). Optional; unset means no declared boundary." ), ) - skills: List[Union["SkillConfigSchema", "_SkillEmbedRefSchema"]] = Field( + skills: List[Union["_SkillTemplateRefSchema", "_SkillEmbedRefSchema"]] = Field( default_factory=list, title="Skills", description=( - "Skills the agent ships: each is an inline SKILL.md package (name, description, " - "body, optional bundled files) or an @ag.embed reference to a stored skill the " + "Skills the agent ships: each is an inline skill template (resolved from the " + "``skill-template`` catalog type) or an @ag.embed reference to a stored skill the " "backend inlines into that same shape before the runner sees it." ), ) @@ -1277,18 +1277,18 @@ class _SkillFileSchema(BaseModel): ) -class SkillConfigSchema(AgSchemaMixin): +class SkillTemplateSchema(AgSchemaMixin): """The playground's editable inline-skill package (one ``skills`` entry), as one semantic type. - Schema-generation counterpart to the runtime :class:`agenta.sdk.agents.SkillConfig`: it emits - a rich JSON Schema for the ``skill_config`` control. The runtime model coerces the loose shapes + Schema-generation counterpart to the runtime :class:`agenta.sdk.agents.SkillTemplate`: it emits + a rich JSON Schema for the ``skill-template`` control. The runtime model coerces the loose shapes the playground emits; this strict twin describes them. A skill that lives elsewhere is authored as an ``@ag.embed`` reference instead, which the backend inlines into this same shape. """ model_config = ConfigDict(extra="forbid") - __ag_type__ = "skill_config" + __ag_type__ = "skill-template" name: str = Field( min_length=1, @@ -1332,7 +1332,7 @@ class _SkillEmbedRefSchema(BaseModel): The seeded default config and the playground both keep skills the user references (rather than writes inline) as a bare ``{"@ag.embed": {...}}`` object; the backend's embed resolver inlines - it into a :class:`SkillConfigSchema` shape before the runner sees it. So the raw/advanced + it into a :class:`SkillTemplateSchema` shape before the runner sees it. So the raw/advanced schema must accept this reference form alongside the inline package, or a valid default would fail validation. The embed body is intentionally permissive (``Dict[str, Any]``) — its inner ``@ag.references`` / ``@ag.selector`` keys are the embed resolver's contract, not this schema's. @@ -1347,6 +1347,25 @@ class _SkillEmbedRefSchema(BaseModel): ) +class _SkillTemplateRefSchema(AgSchemaMixin): + """The inline ``skills`` arm, emitted as a bare ``{x-ag-type-ref: "skill-template"}`` node. + + The agent config no longer inlines the full skill-template schema; it points at the + ``skill-template`` catalog type (``/catalog/types/skill-template``) the same way inputs point at + ``messages``. The frontend resolves the ref to render the editor. The author still writes an + inline skill package here; its full shape lives in the resolved ``skill-template`` type. + """ + + __ag_type_ref__ = "skill-template" + + model_config = ConfigDict(extra="allow") + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema, handler): + # A pure ref node: only the x-ag-type-ref marker, no inlined object shape. + return {"x-ag-type-ref": cls.__ag_type_ref__} + + class _ToolEmbedRefSchema(BaseModel): """An ``@ag.embed`` reference standing in for one ``tools`` entry (the embed syntax). @@ -1388,8 +1407,8 @@ class _ToolEmbedRefSchema(BaseModel): AgentConfigSchema.ag_type(): _dereference_schema( AgentConfigSchema.model_json_schema() ), - SkillConfigSchema.ag_type(): _dereference_schema( - SkillConfigSchema.model_json_schema() + SkillTemplateSchema.ag_type(): _dereference_schema( + SkillTemplateSchema.model_json_schema() ), # The `/run` wire contract (request + result), exported from the dedicated Pydantic wire # models in `agenta.sdk.agents.wire_models`. This puts the service<->runner wire interface in diff --git a/sdks/python/oss/tests/pytest/integration/agents/test_transport_roundtrip.py b/sdks/python/oss/tests/pytest/integration/agents/test_transport_roundtrip.py index 97b6ce7343..ae035bb8ea 100644 --- a/sdks/python/oss/tests/pytest/integration/agents/test_transport_roundtrip.py +++ b/sdks/python/oss/tests/pytest/integration/agents/test_transport_roundtrip.py @@ -21,7 +21,7 @@ PiHarness, SessionConfig, ) -from agenta.sdk.agents.skills import SkillConfig +from agenta.sdk.agents.skills import SkillTemplate from ._fake_runner_backend import FakeRunnerBackend @@ -142,7 +142,7 @@ async def test_resolved_skill_reaches_the_runner_over_the_wire(tmp_path): # arrive at the runner as a concrete `skills` package over the real wire + transport, not as # an embed and not dropped. The skill-echo runner reports the `skills` it saw. harness = PiHarness(Environment(_backend(tmp_path, _SKILL_ECHO_RUNNER))) - skill = SkillConfig( + skill = SkillTemplate( name="release-notes", description="Draft release notes.", body="Read the changelog, then write notes.", diff --git a/sdks/python/oss/tests/pytest/unit/agents/skills/test_models.py b/sdks/python/oss/tests/pytest/unit/agents/skills/test_models.py index 61ebe7f217..861e76e75f 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/skills/test_models.py +++ b/sdks/python/oss/tests/pytest/unit/agents/skills/test_models.py @@ -1,4 +1,4 @@ -"""``SkillConfig`` / ``SkillFile`` validation: the single inline-package shape. +"""``SkillTemplate`` / ``SkillFile`` validation: the single inline-package shape. A skill is one shape (no discriminator). These lock the name pattern, the required fields, the length bounds, the default-deny flags, and ``extra="forbid"`` so a stray key never rides the @@ -10,7 +10,7 @@ import pytest from pydantic import ValidationError -from agenta.sdk.agents import SkillConfig, SkillFile +from agenta.sdk.agents import SkillTemplate, SkillFile def _skill(**overrides): @@ -24,7 +24,7 @@ def _skill(**overrides): def test_minimal_skill_defaults(): - skill = SkillConfig(**_skill()) + skill = SkillTemplate(**_skill()) assert skill.name == "release-notes" assert skill.files == [] assert skill.disable_model_invocation is False @@ -33,7 +33,7 @@ def test_minimal_skill_defaults(): @pytest.mark.parametrize("name", ["release-notes", "a", "skill1", "a-b-c", "x9"]) def test_valid_skill_names(name): - assert SkillConfig(**_skill(name=name)).name == name + assert SkillTemplate(**_skill(name=name)).name == name @pytest.mark.parametrize( @@ -50,26 +50,26 @@ def test_valid_skill_names(name): ) def test_invalid_skill_names_rejected(name): with pytest.raises(ValidationError): - SkillConfig(**_skill(name=name)) + SkillTemplate(**_skill(name=name)) def test_description_required_and_bounded(): with pytest.raises(ValidationError): - SkillConfig(**_skill(description="")) + SkillTemplate(**_skill(description="")) with pytest.raises(ValidationError): - SkillConfig(**_skill(description="x" * 1025)) + SkillTemplate(**_skill(description="x" * 1025)) def test_body_required_and_bounded(): with pytest.raises(ValidationError): - SkillConfig(**_skill(body="")) + SkillTemplate(**_skill(body="")) with pytest.raises(ValidationError): - SkillConfig(**_skill(body="x" * 50_001)) + SkillTemplate(**_skill(body="x" * 50_001)) def test_extra_fields_forbidden(): with pytest.raises(ValidationError): - SkillConfig(**_skill(source="curated")) + SkillTemplate(**_skill(source="curated")) def test_skill_file_defaults_and_bounds(): @@ -105,7 +105,7 @@ def test_skill_file_path_validated_on_the_model(path): with pytest.raises(ValidationError): SkillFile(path=path, content="x") with pytest.raises(ValidationError): - SkillConfig(**_skill(files=[{"path": path, "content": "x"}])) + SkillTemplate(**_skill(files=[{"path": path, "content": "x"}])) @pytest.mark.parametrize( @@ -117,7 +117,7 @@ def test_skill_file_safe_paths_accepted_on_the_model(path): def test_to_wire_minimal_omits_optional_flags(): - wire = SkillConfig(**_skill()).to_wire() + wire = SkillTemplate(**_skill()).to_wire() assert wire == { "name": "release-notes", "description": "Draft release notes from a changelog.", @@ -129,7 +129,7 @@ def test_to_wire_minimal_omits_optional_flags(): def test_to_wire_carries_files_and_flags_camelcase(): - wire = SkillConfig( + wire = SkillTemplate( **_skill( files=[ {"path": "scripts/foo.py", "content": "print(1)", "executable": True} diff --git a/sdks/python/oss/tests/pytest/unit/agents/skills/test_parsing.py b/sdks/python/oss/tests/pytest/unit/agents/skills/test_parsing.py index 81057bdfbe..6d2cbac58a 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/skills/test_parsing.py +++ b/sdks/python/oss/tests/pytest/unit/agents/skills/test_parsing.py @@ -1,4 +1,4 @@ -"""``parse_skill_configs``: list-of-dicts -> ``List[SkillConfig]`` with safe-path validation. +"""``parse_skill_templates``: list-of-dicts -> ``List[SkillTemplate]`` with safe-path validation. Tolerates entries that are already plain dicts (the post-embed-resolution shape) and rejects bundled-file paths that are absolute or escape the skill dir. The index is carried on the error @@ -9,8 +9,8 @@ import pytest -from agenta.sdk.agents import SkillConfig, parse_skill_config, parse_skill_configs -from agenta.sdk.agents.skills import SkillConfigurationError +from agenta.sdk.agents import SkillTemplate, parse_skill_template, parse_skill_templates +from agenta.sdk.agents.skills import SkillValidationError def _skill(**overrides): @@ -24,23 +24,23 @@ def _skill(**overrides): def test_parses_plain_dicts(): - parsed = parse_skill_configs([_skill(), _skill(name="other")]) + parsed = parse_skill_templates([_skill(), _skill(name="other")]) assert [s.name for s in parsed] == ["release-notes", "other"] - assert all(isinstance(s, SkillConfig) for s in parsed) + assert all(isinstance(s, SkillTemplate) for s in parsed) -def test_passes_through_skill_config_instances(): - skill = SkillConfig(**_skill()) - assert parse_skill_config(skill).name == "release-notes" +def test_passes_through_skill_template_instances(): + skill = SkillTemplate(**_skill()) + assert parse_skill_template(skill).name == "release-notes" def test_empty_list_is_empty(): - assert parse_skill_configs([]) == [] + assert parse_skill_templates([]) == [] def test_invalid_name_raises_with_index(): - with pytest.raises(SkillConfigurationError) as exc: - parse_skill_configs([_skill(), _skill(name="Bad Name")]) + with pytest.raises(SkillValidationError) as exc: + parse_skill_templates([_skill(), _skill(name="Bad Name")]) assert exc.value.index == 1 @@ -58,14 +58,14 @@ def test_invalid_name_raises_with_index(): ) def test_rejects_unsafe_file_paths(path): # The model's path validator raises a ValidationError, which the parser wraps into a - # SkillConfigurationError (so unsafe paths are rejected on the parsing path too). - with pytest.raises(SkillConfigurationError): - parse_skill_config(_skill(files=[{"path": path, "content": "x"}])) + # SkillValidationError (so unsafe paths are rejected on the parsing path too). + with pytest.raises(SkillValidationError): + parse_skill_template(_skill(files=[{"path": path, "content": "x"}])) @pytest.mark.parametrize("path", ["scripts/foo.py", "references/notes.md", "a.txt"]) def test_accepts_safe_relative_file_paths(path): - skill = parse_skill_config(_skill(files=[{"path": path, "content": "x"}])) + skill = parse_skill_template(_skill(files=[{"path": path, "content": "x"}])) assert skill.files[0].path == path @@ -75,14 +75,14 @@ def test_unresolved_object_embed_raises_clear_error(): embed = { "@ag.embed": {"@ag.references": {"workflow_revision": {"slug": "my-skill"}}} } - with pytest.raises(SkillConfigurationError) as exc: - parse_skill_config(embed) + with pytest.raises(SkillValidationError) as exc: + parse_skill_template(embed) assert "unresolved" in str(exc.value).lower() def test_unresolved_snippet_token_raises_clear_error(): - with pytest.raises(SkillConfigurationError) as exc: - parse_skill_configs( + with pytest.raises(SkillValidationError) as exc: + parse_skill_templates( ["@{{workflow_revision.slug=my-skill, path=parameters.skill}}"] ) assert "unresolved" in str(exc.value).lower() diff --git a/sdks/python/oss/tests/pytest/unit/agents/skills/test_skills_e2e.py b/sdks/python/oss/tests/pytest/unit/agents/skills/test_skills_e2e.py index 764bb4654c..4fef9249c3 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/skills/test_skills_e2e.py +++ b/sdks/python/oss/tests/pytest/unit/agents/skills/test_skills_e2e.py @@ -3,13 +3,13 @@ These lock the two author shapes the skills feature ships: -1. **Inline skill -> wire.** An `AgentConfig` carrying an inline `SkillConfig` produces a runner +1. **Inline skill -> wire.** An `AgentConfig` carrying an inline `SkillTemplate` produces a runner request whose `skills[0]` is the materialized inline package (name/description/body/files + camelCase flags), via the `wire_skills()` seam that `request_to_wire` spreads. 2. **Embed skill -> resolve -> wire.** An `AgentConfig` whose `skills` list holds an `@ag.embed` entry, run through the resolution middleware against a MOCKED resolve endpoint that returns a - `SkillConfig`-shaped `parameters.skill`, ends up on the wire as a concrete inline package (the + `SkillTemplate`-shaped `parameters.skill`, ends up on the wire as a concrete inline package (the embed is gone). This mirrors how the resolver tests mock `/workflows/revisions/resolve`, then carries the resolved params the rest of the way: `from_params` -> harness -> `request_to_wire`. @@ -31,7 +31,7 @@ PiHarness, SessionConfig, ) -from agenta.sdk.agents.skills import SkillConfig +from agenta.sdk.agents.skills import SkillTemplate from agenta.sdk.agents.utils import request_to_wire from agenta.sdk.contexts.tracing import TracingContext, tracing_context_manager from agenta.sdk.middlewares.running.resolver import ResolverMiddleware @@ -96,7 +96,7 @@ def test_minimal_inline_skill_omits_optional_flags_on_the_wire(make_env): agent = AgentConfig( instructions="hi", model="gpt-5.5", - skills=[SkillConfig(name="a", description="d", body="b")], + skills=[SkillTemplate(name="a", description="d", body="b")], ) wire = _pi_wire(env, agent) @@ -114,7 +114,7 @@ def test_minimal_inline_skill_omits_optional_flags_on_the_wire(make_env): @pytest.mark.asyncio async def test_embed_skill_resolves_to_a_concrete_package_on_the_wire(make_env): - # The author config references a skill by an `@ag.embed` inside the skills list (the platform default-config shape). The resolver inlines the stored `SkillConfig` BEFORE the handler + # The author config references a skill by an `@ag.embed` inside the skills list (the platform default-config shape). The resolver inlines the stored `SkillTemplate` BEFORE the handler # builds the AgentConfig, so the runner must never see the embed -- only a concrete package. params_with_embed = { "skills": [ diff --git a/sdks/python/oss/tests/pytest/unit/agents/skills/test_wire.py b/sdks/python/oss/tests/pytest/unit/agents/skills/test_wire.py index 166471f4bb..d7a010e1ae 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/skills/test_wire.py +++ b/sdks/python/oss/tests/pytest/unit/agents/skills/test_wire.py @@ -1,4 +1,4 @@ -"""``skills_to_wire``: resolved ``SkillConfig`` list -> the ``WireSkill[]`` runner contract. +"""``skills_to_wire``: resolved ``SkillTemplate`` list -> the ``WireSkill[]`` runner contract. The wire is camelCase to match ``services/agent/src/protocol.ts``; optional flags and ``files`` are omitted when unset so a minimal skill stays minimal. @@ -6,7 +6,7 @@ from __future__ import annotations -from agenta.sdk.agents import SkillConfig, skills_to_wire +from agenta.sdk.agents import SkillTemplate, skills_to_wire def test_skills_to_wire_empty(): @@ -15,8 +15,8 @@ def test_skills_to_wire_empty(): def test_skills_to_wire_minimal(): skills = [ - SkillConfig(name="a", description="d", body="b"), - SkillConfig(name="c", description="e", body="f"), + SkillTemplate(name="a", description="d", body="b"), + SkillTemplate(name="c", description="e", body="f"), ] assert skills_to_wire(skills) == [ {"name": "a", "description": "d", "body": "b"}, @@ -25,7 +25,7 @@ def test_skills_to_wire_minimal(): def test_skills_to_wire_full_shape(): - skill = SkillConfig( + skill = SkillTemplate( name="release-notes", description="Draft release notes.", body="Read the changelog.", diff --git a/sdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.py b/sdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.py index d00663561f..c492c430ed 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.py +++ b/sdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.py @@ -173,9 +173,9 @@ def test_agenta_does_not_duplicate_an_already_present_platform_skill(make_env): def test_force_skills_unions_forced_after_author_skills(): - from agenta.sdk.agents.skills import SkillConfig + from agenta.sdk.agents.skills import SkillTemplate - author = SkillConfig( + author = SkillTemplate( name="release-notes", description="Draft notes.", body="Do it." ) diff --git a/sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py b/sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py index bd6803d8c8..485d061453 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py +++ b/sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py @@ -32,7 +32,7 @@ RunContextTrace, RunContextWorkflow, SandboxPermission, - SkillConfig, + SkillTemplate, ToolCallback, TraceContext, ) @@ -220,7 +220,7 @@ def test_request_to_wire_skills_ride_their_own_seam_not_tools(): # Skills are emitted by `wire_skills`, not folded into the tool wire. config = PiAgentConfig(skills=[dict(_SKILL)]) assert "skills" not in config.wire_tools() - assert config.wire_skills() == {"skills": [SkillConfig(**_SKILL).to_wire()]} + assert config.wire_skills() == {"skills": [SkillTemplate(**_SKILL).to_wire()]} def test_request_to_wire_omits_skills_when_none(): diff --git a/sdks/python/oss/tests/pytest/unit/test_skill_flags.py b/sdks/python/oss/tests/pytest/unit/test_skill_flags.py index 82a8f3b843..e5dac34e0f 100644 --- a/sdks/python/oss/tests/pytest/unit/test_skill_flags.py +++ b/sdks/python/oss/tests/pytest/unit/test_skill_flags.py @@ -2,8 +2,8 @@ A skill is a non-runnable snippet identified by the builtin uri ``agenta:builtin:skill:v0``. ``is_skill`` is uri-derived (``key == "skill"``), not caller-settable, and a skill carries no -execution surface (url / script / handler are all stripped). These tests pin that derivation and -that ``is_skill`` is exposed on the SDK flag models. +execution surface (url / script / handler are stripped); it keeps only its ``parameters`` and that +parameters schema. These tests pin that derivation and that ``is_skill`` is on the SDK flag models. """ from agenta.sdk.engines.running.utils import ( @@ -12,6 +12,7 @@ normalize_snippet_data, ) from agenta.sdk.models.workflows import ( + JsonSchemas, WorkflowFlags, WorkflowQueryFlags, WorkflowRevisionData, @@ -53,12 +54,18 @@ def test_infer_flags_derives_is_skill_from_uri(): assert flags.has_handler is False -def test_normalize_snippet_data_keeps_only_uri_and_parameters(): +def test_normalize_snippet_data_keeps_uri_parameters_and_parameters_schema(): + params_schema = {"type": "object", "properties": {"skill": {"type": "object"}}} data = WorkflowRevisionData( uri=AGENTA_BUILTIN_SKILL_URI, url="https://example.com/skill", script="print('x')", parameters={"skill": {"name": "s", "description": "d", "body": "b"}}, + schemas=JsonSchemas( + parameters=params_schema, + inputs={"type": "object"}, + outputs={"type": "object"}, + ), ) normalized = normalize_snippet_data(data) @@ -69,3 +76,19 @@ def test_normalize_snippet_data_keeps_only_uri_and_parameters(): } assert normalized.url is None assert normalized.script is None + # A snippet is non-runnable: it keeps the parameters schema but no inputs/outputs. + assert normalized.schemas is not None + assert normalized.schemas.parameters == params_schema + assert normalized.schemas.inputs is None + assert normalized.schemas.outputs is None + + +def test_normalize_snippet_data_without_schemas_stays_none(): + data = WorkflowRevisionData( + uri=AGENTA_BUILTIN_SKILL_URI, + parameters={"skill": {"name": "s", "description": "d", "body": "b"}}, + ) + + normalized = normalize_snippet_data(data) + + assert normalized.schemas is None diff --git a/sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py b/sdks/python/oss/tests/pytest/unit/test_skill_template_catalog.py similarity index 84% rename from sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py rename to sdks/python/oss/tests/pytest/unit/test_skill_template_catalog.py index cd24afc9c0..e1f00534af 100644 --- a/sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py +++ b/sdks/python/oss/tests/pytest/unit/test_skill_template_catalog.py @@ -1,24 +1,24 @@ -"""The ``skill_config`` catalog type and the ``skills`` field on the agent-config twin. +"""The ``skill-template`` catalog type and the ``skills`` field on the agent-config twin. These pin that the playground gets a typed editor for an inline skill package and that the agent-config control renders a list of those skills, where each item is EITHER an inline -``skill_config`` package OR an ``@ag.embed`` reference the backend inlines server-side. +``skill-template`` package OR an ``@ag.embed`` reference the backend inlines server-side. """ import jsonschema -from agenta.sdk.utils.types import CATALOG_TYPES, SkillConfigSchema +from agenta.sdk.utils.types import CATALOG_TYPES, SkillTemplateSchema -def test_skill_config_registered_in_catalog(): - assert SkillConfigSchema.ag_type() == "skill_config" - assert "skill_config" in CATALOG_TYPES +def test_skill_template_registered_in_catalog(): + assert SkillTemplateSchema.ag_type() == "skill-template" + assert "skill-template" in CATALOG_TYPES -def test_skill_config_schema_shape(): - schema = CATALOG_TYPES["skill_config"] +def test_skill_template_schema_shape(): + schema = CATALOG_TYPES["skill-template"] - assert schema["x-ag-type"] == "skill_config" + assert schema["x-ag-type"] == "skill-template" assert set(schema["properties"]) == { "name", "description", @@ -36,18 +36,20 @@ def test_skill_config_schema_shape(): assert set(file_item["properties"]) == {"path", "content", "executable"} -def test_agent_config_catalog_exposes_skills_as_inline_or_embed_union(): +def test_agent_config_catalog_exposes_skills_as_ref_or_embed_union(): agent_config = CATALOG_TYPES["agent_config"] assert "skills" in agent_config["properties"] skills_item = agent_config["properties"]["skills"]["items"] - # Each entry is a union: an inline skill_config package, or an @ag.embed reference. + # Each entry is a union: a skill-template ref (resolved from /catalog/types/skill-template), + # or an @ag.embed reference. The full inline shape lives in the skill-template catalog type, + # not inlined here (mirrors how inputs reference `messages`). variants = skills_item["anyOf"] assert len(variants) == 2 - inline = next(v for v in variants if v.get("x-ag-type") == "skill_config") - assert {"name", "description", "body"}.issubset(inline["properties"]) + ref = next(v for v in variants if v.get("x-ag-type-ref") == "skill-template") + assert "properties" not in ref # a bare ref node, not the inlined schema embed = next(v for v in variants if "@ag.embed" in v.get("properties", {})) assert embed["required"] == ["@ag.embed"] diff --git a/services/oss/src/agent/schemas.py b/services/oss/src/agent/schemas.py index 61bb8637dc..e4c1575212 100644 --- a/services/oss/src/agent/schemas.py +++ b/services/oss/src/agent/schemas.py @@ -39,7 +39,7 @@ # Reserved slug of the static default skill, served from code by the StaticWorkflowCatalog # (api/oss/src/core/workflows/static_catalog.py), never the database. The default config # references it by stable slug through an @ag.embed; the embed resolver inlines the catalogue's -# SkillConfig (at the canonical parameters.skill selector) before the runner sees it. The +# SkillTemplate (at the canonical parameters.skill selector) before the runner sees it. The # `__ag__` prefix is reserved: a user cannot author or shadow it. This replaces both # AGENTA_FORCED_SKILLS and the old per-project skill seeder. Single source: the SDK constant. _DEFAULT_SKILL_SLUG = GETTING_STARTED_WITH_AGENTA_SLUG diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx index 4b175e34e0..96a6495620 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx @@ -294,11 +294,11 @@ function asObj(value: unknown): Record | undefined { : undefined } -/** The reserved slug namespace for platform-owned skills (mirrors the backend `_agenta.*`). */ -const PLATFORM_SKILL_SLUG_PREFIX = "_agenta." +/** The reserved slug namespace for static (Agenta-owned) skills (mirrors the backend `__ag__*`). */ +const STATIC_SKILL_SLUG_PREFIX = "__ag__" /** The slug an `@ag.embed` entry points at (a `workflow` or pinned `workflow_revision` reference). */ -function platformEmbedSlug(skill: Record): string | undefined { +function staticEmbedSlug(skill: Record): string | undefined { const refs = asObj(asObj(skill["@ag.embed"])?.["@ag.references"]) if (!refs) return undefined const slug = asObj(refs.workflow)?.slug ?? asObj(refs.workflow_revision)?.slug @@ -313,29 +313,29 @@ function embedRevisionVersion(skill: Record): string | undefine } /** - * Whether a skill entry is platform-owned and so read-only for the author. The reliable client-side - * signal is the reserved `_agenta.` slug prefix on the embed's referenced workflow (or pinned - * revision); a resolved object carrying `flags.is_platform === true` counts too. + * Whether a skill entry is static (Agenta-owned) and so read-only for the author. The reliable client-side + * signal is the reserved `__ag__` slug prefix on the embed's referenced workflow (or pinned + * revision); a resolved object carrying `flags.is_static === true` counts too. */ -function isPlatformSkill(skill: unknown): boolean { +function isStaticSkill(skill: unknown): boolean { const s = asObj(skill) if (!s) return false - const slug = platformEmbedSlug(s) - if (slug && slug.startsWith(PLATFORM_SKILL_SLUG_PREFIX)) return true - return asObj(s.flags)?.is_platform === true + const slug = staticEmbedSlug(s) + if (slug && slug.startsWith(STATIC_SKILL_SLUG_PREFIX)) return true + return asObj(s.flags)?.is_static === true } function describeSkill(skill: unknown): ItemDescriptor { const s = (skill ?? {}) as Record - if (isPlatformSkill(s)) { - const slug = platformEmbedSlug(s) + if (isStaticSkill(s)) { + const slug = staticEmbedSlug(s) const version = embedRevisionVersion(s) return { - name: slug ?? "Platform skill", + name: slug ?? "Static skill", mono: "sk", color: "#6b7280", - tags: version ? ["platform", `v${version}`] : ["platform"], - typeLabel: "platform skill", + tags: version ? ["static", `v${version}`] : ["static"], + typeLabel: "static skill", subtitle: "Provided by Agenta — read-only", } } @@ -794,7 +794,7 @@ export function AgentConfigControl({ // Skills are a sibling of tools/MCP: a flat array on the agent config. Each entry is an inline // SKILL.md package (name + description + body + files + flags) or an `@ag.embed` reference the - // backend inlines — the `skill_config` catalog type (SkillConfigSchema in the SDK). + // backend inlines — the `skill-template` catalog type (SkillTemplateSchema in the SDK). const skills = useMemo( () => (Array.isArray(config.skills) ? (config.skills as unknown[]) : []), [config.skills], @@ -1235,9 +1235,9 @@ export function AgentConfigControl({ handleSkillDelete(index) closeEditor() }} - // Platform skills (`_agenta.*`) are read-only: no remove, and + // Static skills (`__ag__*`) are read-only: no remove, and // the drawer opens disabled (see the skill drawer below). - disabled={disabled || isPlatformSkill(skill)} + disabled={disabled || isStaticSkill(skill)} /> ))} @@ -1471,14 +1471,14 @@ export function AgentConfigControl({ onSave={commitDraft} saveDisabled={draftInvalid || (drawerView === "json" && jsonInvalid)} jsonOnly={isEmbedRefSkill(draft)} - // Platform skills (`_agenta.*`) are read-only — view their JSON but can't edit. - disabled={disabled || isPlatformSkill(draft)} + // Static skills (`__ag__*`) are read-only — view their JSON but can't edit. + disabled={disabled || isStaticSkill(draft)} form={ setDraft(v)} - disabled={disabled || isPlatformSkill(draft)} + disabled={disabled || isStaticSkill(draft)} /> } json={ @@ -1487,7 +1487,7 @@ export function AgentConfigControl({ value={draft} onChange={(v) => setDraft(v as Record)} onValidityChange={(valid) => setJsonInvalid(!valid)} - disabled={disabled || isPlatformSkill(draft)} + disabled={disabled || isStaticSkill(draft)} /> } /> diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillFormView.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillFormView.tsx index 3f648b6675..69bd680dc9 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillFormView.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillFormView.tsx @@ -2,7 +2,7 @@ * SkillFormView * * Structured form view for one inline skill, the Form side of {@link ConfigItemDrawer}. Mirrors - * the inline `SkillConfigSchema` shape (sdk/utils/types.py, `__ag_type__ = "skill_config"`): a + * the inline `SkillTemplateSchema` shape (sdk/utils/types.py, `__ag_type__ = "skill-template"`): a * kebab `name`, a `description` (the trigger the model matches), the `body` (SKILL.md Markdown), * the supporting `files[]`, and two behaviour flags. * diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillConfigControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillTemplateControl.tsx similarity index 86% rename from web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillConfigControl.tsx rename to web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillTemplateControl.tsx index fcce262a6d..072aa0bbca 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillConfigControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillTemplateControl.tsx @@ -1,5 +1,5 @@ /** - * SkillConfigControl + * SkillTemplateControl * * Schema-driven control for one declared skill on the agent config. Skills are a sibling of * `tools` and `mcp_servers`: each entry is either an inline SKILL.md package (`name`, @@ -7,7 +7,7 @@ * `@ag.embed` reference to a stored skill the backend inlines into that same shape before the * runner sees it. The shape is open enough that a JSON editor is the pragmatic v1 — the same * approach McpServerItemControl takes for MCP servers — with a name header and a delete control. - * The typed shape lives in the `skill_config` catalog type (SkillConfigSchema in the SDK); this + * The typed shape lives in the `skill-template` catalog type (SkillTemplateSchema in the SDK); this * control just edits one entry of the `skills` array. * * The full inline-authoring form (separate fields per file, an upload affordance) is out of @@ -23,7 +23,7 @@ import {MinusCircle} from "@phosphor-icons/react" import {Button, Tag, Tooltip, Typography} from "antd" import clsx from "clsx" -export interface SkillConfigControlProps { +export interface SkillTemplateControlProps { /** Skill value (object or JSON string). An inline package or an `@ag.embed` reference. */ value: unknown /** Called when the skill value changes (only on valid JSON) */ @@ -54,8 +54,8 @@ export function isEmbedRef(skill: Record): boolean { return "@ag.embed" in skill } -/** The reserved slug namespace for platform-owned skills (mirrors the backend `_agenta.*`). */ -const PLATFORM_SLUG_PREFIX = "_agenta." +/** The reserved slug namespace for static (Agenta-owned) skills (mirrors the backend `__ag__*`). */ +const STATIC_SLUG_PREFIX = "__ag__" function asObj(value: unknown): Record | undefined { return isPlainObject(value) ? value : undefined @@ -66,7 +66,7 @@ function asObj(value: unknown): Record | undefined { * `workflow_revision` reference under `@ag.embed > @ag.references`. Returns `undefined` for an * inline (non-embed) entry or an embed without a slug. */ -export function platformEmbedSlug(skill: Record): string | undefined { +export function staticEmbedSlug(skill: Record): string | undefined { const refs = asObj(asObj(skill["@ag.embed"])?.["@ag.references"]) if (!refs) return undefined const workflowSlug = asObj(refs.workflow)?.slug @@ -83,14 +83,14 @@ function embedRevisionVersion(skill: Record): string | undefine } /** - * Whether a skill entry is platform-owned and so read-only for the author. The reliable - * client-side signal is the reserved `_agenta.` slug prefix on the embed's referenced workflow (or - * pinned workflow_revision); a resolved object carrying `flags.is_platform === true` counts too. + * Whether a skill entry is static (Agenta-owned) and so read-only for the author. The reliable + * client-side signal is the reserved `__ag__` slug prefix on the embed's referenced workflow (or + * pinned workflow_revision); a resolved object carrying `flags.is_static === true` counts too. */ -export function isPlatformSkill(skill: Record): boolean { - const slug = platformEmbedSlug(skill) - if (slug && slug.startsWith(PLATFORM_SLUG_PREFIX)) return true - return asObj(skill.flags)?.is_platform === true +export function isStaticSkill(skill: Record): boolean { + const slug = staticEmbedSlug(skill) + if (slug && slug.startsWith(STATIC_SLUG_PREFIX)) return true + return asObj(skill.flags)?.is_static === true } /** @@ -116,18 +116,18 @@ function skillLabel(skill: Record): string { return "Skill" } -export const SkillConfigControl = memo(function SkillConfigControl({ +export const SkillTemplateControl = memo(function SkillTemplateControl({ value, onChange, onDelete, disabled = false, className, -}: SkillConfigControlProps) { +}: SkillTemplateControlProps) { const {SharedEditor} = useDrillInUI() const skillObj = toSkillObj(value) const name = skillLabel(skillObj) const embed = isEmbedRef(skillObj) - const platform = isPlatformSkill(skillObj) + const isStatic = isStaticSkill(skillObj) const [editorText, setEditorText] = useState(() => safeStringify(skillObj ?? {})) @@ -177,10 +177,10 @@ export const SkillConfigControl = memo(function SkillConfigControl({ ) - // A platform-owned skill is a default the author cannot edit or remove: render it read-only, - // with no JSON/body editor and no delete control (the embed and its body stay untouched). - if (platform) { - const slug = platformEmbedSlug(skillObj) + // A static (Agenta-owned) skill is a default the author cannot edit or remove: render it + // read-only, with no JSON/body editor and no delete control (the embed and its body stay intact). + if (isStatic) { + const slug = staticEmbedSlug(skillObj) const version = embedRevisionVersion(skillObj) return (
{name} - Platform skill + Static skill {version && {version}}
{slug && ( diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts index 2700634954..5d4a7b6e71 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts @@ -73,8 +73,8 @@ export type {ToolObj, ToolFunction} from "./toolUtils" export {McpServerItemControl} from "./McpServerItemControl" export type {McpServerItemControlProps} from "./McpServerItemControl" -export {SkillConfigControl} from "./SkillConfigControl" -export type {SkillConfigControlProps} from "./SkillConfigControl" +export {SkillTemplateControl} from "./SkillTemplateControl" +export type {SkillTemplateControlProps} from "./SkillTemplateControl" export {SandboxPermissionControl} from "./SandboxPermissionControl" export type {SandboxPermissionControlProps} from "./SandboxPermissionControl" diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/skillUpload.ts b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/skillUpload.ts index a61af17465..737e48a9bd 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/skillUpload.ts +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/skillUpload.ts @@ -2,7 +2,7 @@ * skillUpload * * Turns an uploaded skill — a folder, a set of loose files, or a `.zip` / `.skill` archive — - * into the inline `SkillConfigSchema` shape: `name` + `description` (parsed from the SKILL.md + * into the inline `SkillTemplateSchema` shape: `name` + `description` (parsed from the SKILL.md * YAML frontmatter), `body` (the Markdown after the frontmatter), and `files[]` (the supporting * files laid beside SKILL.md, by relative path). Pure helpers, unit-testable without the DOM, * plus a small DataTransfer reader for drag-and-dropped folders. diff --git a/web/packages/agenta-entity-ui/tests/unit/skillConfigControl.test.ts b/web/packages/agenta-entity-ui/tests/unit/skillTemplateControl.test.ts similarity index 63% rename from web/packages/agenta-entity-ui/tests/unit/skillConfigControl.test.ts rename to web/packages/agenta-entity-ui/tests/unit/skillTemplateControl.test.ts index 4a231a7788..5c7b232aa2 100644 --- a/web/packages/agenta-entity-ui/tests/unit/skillConfigControl.test.ts +++ b/web/packages/agenta-entity-ui/tests/unit/skillTemplateControl.test.ts @@ -1,5 +1,5 @@ /** - * Unit tests for the pure round-trip helpers behind SkillConfigControl. + * Unit tests for the pure round-trip helpers behind SkillTemplateControl. * * A skills entry is either an inline SKILL.md package or an `@ag.embed` reference the backend * inlines server-side. The control edits the entry as JSON and must preserve an `@ag.embed` @@ -10,10 +10,10 @@ import {describe, expect, it} from "vitest" import { isEmbedRef, - isPlatformSkill, + isStaticSkill, parseSkillEditorText, - platformEmbedSlug, -} from "../../src/DrillInView/SchemaControls/SkillConfigControl" + staticEmbedSlug, +} from "../../src/DrillInView/SchemaControls/SkillTemplateControl" const EMBED_ENTRY = { "@ag.embed": { @@ -22,17 +22,17 @@ const EMBED_ENTRY = { }, } -const PLATFORM_EMBED_ENTRY = { +const STATIC_EMBED_ENTRY = { "@ag.embed": { - "@ag.references": {workflow: {slug: "_agenta.agenta-getting-started"}}, + "@ag.references": {workflow: {slug: "__ag__getting_started_with_agenta"}}, "@ag.selector": {path: "parameters.skill"}, }, } -const PLATFORM_REVISION_EMBED_ENTRY = { +const STATIC_REVISION_EMBED_ENTRY = { "@ag.embed": { "@ag.references": { - workflow_revision: {slug: "_agenta.agenta-getting-started", version: "v3"}, + workflow_revision: {slug: "__ag__getting_started_with_agenta", version: "v1"}, }, "@ag.selector": {path: "parameters.skill"}, }, @@ -44,7 +44,7 @@ const INLINE_ENTRY = { body: "Read the changelog.", } -describe("SkillConfigControl: isEmbedRef", () => { +describe("SkillTemplateControl: isEmbedRef", () => { it("detects an @ag.embed reference entry", () => { expect(isEmbedRef(EMBED_ENTRY)).toBe(true) }) @@ -54,35 +54,35 @@ describe("SkillConfigControl: isEmbedRef", () => { }) }) -describe("SkillConfigControl: isPlatformSkill", () => { - it("flags an embed whose workflow slug uses the reserved _agenta. namespace", () => { - expect(isPlatformSkill(PLATFORM_EMBED_ENTRY)).toBe(true) - expect(platformEmbedSlug(PLATFORM_EMBED_ENTRY)).toBe("_agenta.agenta-getting-started") +describe("SkillTemplateControl: isStaticSkill", () => { + it("flags an embed whose workflow slug uses the reserved __ag__ namespace", () => { + expect(isStaticSkill(STATIC_EMBED_ENTRY)).toBe(true) + expect(staticEmbedSlug(STATIC_EMBED_ENTRY)).toBe("__ag__getting_started_with_agenta") }) it("flags a pinned workflow_revision embed in the reserved namespace", () => { - expect(isPlatformSkill(PLATFORM_REVISION_EMBED_ENTRY)).toBe(true) - expect(platformEmbedSlug(PLATFORM_REVISION_EMBED_ENTRY)).toBe( - "_agenta.agenta-getting-started", + expect(isStaticSkill(STATIC_REVISION_EMBED_ENTRY)).toBe(true) + expect(staticEmbedSlug(STATIC_REVISION_EMBED_ENTRY)).toBe( + "__ag__getting_started_with_agenta", ) }) it("treats a non-reserved embed slug as a normal editable skill", () => { - expect(isPlatformSkill(EMBED_ENTRY)).toBe(false) + expect(isStaticSkill(EMBED_ENTRY)).toBe(false) }) it("treats an inline package as a normal editable skill", () => { - expect(isPlatformSkill(INLINE_ENTRY)).toBe(false) - expect(platformEmbedSlug(INLINE_ENTRY)).toBeUndefined() + expect(isStaticSkill(INLINE_ENTRY)).toBe(false) + expect(staticEmbedSlug(INLINE_ENTRY)).toBeUndefined() }) - it("honours a resolved flags.is_platform === true marker", () => { - expect(isPlatformSkill({name: "x", flags: {is_platform: true}})).toBe(true) - expect(isPlatformSkill({name: "x", flags: {is_platform: false}})).toBe(false) + it("honours a resolved flags.is_static === true marker", () => { + expect(isStaticSkill({name: "x", flags: {is_static: true}})).toBe(true) + expect(isStaticSkill({name: "x", flags: {is_static: false}})).toBe(false) }) }) -describe("SkillConfigControl: parseSkillEditorText round-trip", () => { +describe("SkillTemplateControl: parseSkillEditorText round-trip", () => { it("preserves an @ag.embed entry unchanged through the editor round-trip", () => { const text = JSON.stringify(EMBED_ENTRY) const parsed = parseSkillEditorText(text)