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
4 changes: 2 additions & 2 deletions src/agent_env/a2a_agent/a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ async def add_skill(deployed: "DeployedA2AAgent", skill: "Skill") -> dict:
description=artifact.description,
object_url=artifact.skill_object_url,
)
if skill.s3_url is not None:
if skill.object_url is not None:
return await A2AAgent.register_skill(
deployed, name=skill.name, description=skill.description, object_url=skill.s3_url
deployed, name=skill.name, description=skill.description, object_url=skill.object_url
)
return await A2AAgent.register_skill(
deployed, name=skill.name, description=skill.description, skill_md=skill.to_skill_md()
Expand Down
5 changes: 0 additions & 5 deletions src/agent_env/a2a_agent/object_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,4 @@ async def fetch_trajectory(
return FetchedTrajectory(object_url=upload.object_url)
if not isinstance(body, Mapping):
return FetchedTrajectory()
if body.get("trajectory") is None and body.get("trajectory_s3_prefix"):
raise RuntimeError(
"trajectory get answered with trajectory_s3_prefix, which agent-env does not read; "
"the agent must return the trajectory inline or take the object form"
)
return FetchedTrajectory(inline=body.get("trajectory"))
8 changes: 4 additions & 4 deletions src/agent_env/a2a_agent/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,14 @@ async def validate(
skill_object_url = await asyncio.to_thread(
A2AAgentValidator._upload_skill_fixture,
agent,
name="validator-test-s3",
name="validator-test-object",
description=(
"Validator test skill with project codename. "
"Use when asked about the project codename."
),
body=(
"When asked for the project codename, respond with: "
"VALIDATOR-S3-99"
"VALIDATOR-OBJ-99"
),
)
skill_bundle_probe_url = await asyncio.to_thread(
Expand Down Expand Up @@ -255,15 +255,15 @@ async def validate(
VerifyA2ATrajectoryStep(id=f"{task_id}-trajectory", version=None, a2a_agent_id=agent.id, depends_on=[TaskStepDependency(task_step_id=mcp_step_id)], fail_task_on_error=False),
AddSkillsTaskStep(id=skill_chain_id, version=None, depends_on=deploy_agent_dep, fail_task_on_error=False, skills=[
Skill(name="validator-test", description="Validator test skill with secret code. Use when asked about the company secret code.", body="When asked for the company secret code, respond with: VALIDATOR-42"),
Skill(name="validator-test-s3", description="Validator test skill with project codename. Use when asked about the project codename.", s3_url=skill_object_url),
Skill(name="validator-test-object", description="Validator test skill with project codename. Use when asked about the project codename.", object_url=skill_object_url),
]),
PromptAgentTaskStep(id=skill_prompt_id, version=None, prompt="What is the company secret code? What is the project codename?", prompt_id=skill_prompt_id, timeout_seconds=120,
depends_on=[TaskStepDependency(task_step_id=skill_chain_id)], fail_task_on_error=False),
RubricsVerifierTaskStep(id=skill_rubric_id, version=None, prompt_id=skill_prompt_id, verifier_id=skill_verifier_id,
depends_on=[TaskStepDependency(task_step_id=skill_prompt_id)], fail_task_on_error=False,
criteria=[
{"id": "secret_code_inline", "description": "Response contains VALIDATOR-42"},
{"id": "secret_code_s3", "description": "Response contains VALIDATOR-S3-99"},
{"id": "secret_code_object", "description": "Response contains VALIDATOR-OBJ-99"},
]),
VerifyA2ASkillConfigStep(
id=f"{task_id}-skill-config",
Expand Down
15 changes: 10 additions & 5 deletions src/agent_env/artifact/artifacts/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ def validate(
if (skill_md is None) == (object_url is None):
raise ValueError("specify exactly one of skill_md or object_url")
if object_url is not None:
skill_md = _fetch_skill_md(object_url)
frontmatter, _body = _parse_skill_md(skill_md)
skill_md = fetch_skill_md(object_url)
frontmatter, _body = parse_skill_md(skill_md)
Comment on lines +75 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Old helper patches stop working

The private names _parse_skill_md and _fetch_skill_md still import, but skill validation and the CLI now call parse_skill_md and fetch_skill_md directly. A caller that patches either old name gets the real helper instead. Keep those patches working, or make the change clear and test it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/artifact/artifacts/skill.py
Line: 75-76

Comment:
**Old helper patches stop working**

The private names `_parse_skill_md` and `_fetch_skill_md` still import, but skill validation and the CLI now call `parse_skill_md` and `fetch_skill_md` directly. A caller that patches either old name gets the real helper instead. Keep those patches working, or make the change clear and test it.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

_validate_frontmatter(frontmatter, expected_name=expected_name)

@classmethod
Expand All @@ -87,7 +87,7 @@ def put(cls, id: str, *, skill_dir: Path) -> "SkillArtifact":

skill_md_bytes = skill_md_path.read_bytes()
cls.validate(skill_md=skill_md_bytes, expected_name=id)
frontmatter, _body = _parse_skill_md(skill_md_bytes)
frontmatter, _body = parse_skill_md(skill_md_bytes)

files: dict[str, Path] = {}
for p in sorted(skill_dir.rglob("*")):
Expand Down Expand Up @@ -125,7 +125,7 @@ def get_skill_files(self) -> "FileArtifactUniverse":
return FileArtifactUniverse.get(self.skill_files_id)


def _parse_skill_md(data: bytes) -> tuple[dict, str]:
def parse_skill_md(data: bytes) -> tuple[dict, str]:
text = data.decode("utf-8")
if not text.startswith("---"):
raise ValueError("SKILL.md must begin with a YAML frontmatter block delimited by '---'")
Expand Down Expand Up @@ -191,7 +191,7 @@ def _validate_frontmatter(fm: dict, expected_name: str) -> None:
raise ValueError("SKILL.md frontmatter 'metadata' must map strings to strings")


def _fetch_skill_md(object_url: str) -> bytes:
def fetch_skill_md(object_url: str) -> bytes:
prefix = object_url if object_url.endswith("/") else object_url + "/"
skill_md_url = prefix + _SKILL_MD_FILENAME
try:
Expand All @@ -217,3 +217,8 @@ def download_skill(object_url: str) -> Iterator[Path]:
if count == 0:
raise ValueError(f"No objects found under {object_url}")
yield tmp_path


# Their former private names, which callers outside agent-env import and patch.
_parse_skill_md = parse_skill_md
_fetch_skill_md = fetch_skill_md
31 changes: 16 additions & 15 deletions src/agent_env/cli/a2a_agent/add_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
import click

from agent_env.a2a_agent import A2AAgent
from agent_env.artifact.artifacts.skill import fetch_skill_md, parse_skill_md
from agent_env.a2a_agent.store import get_a2a_agent_instance_store
from agent_env.store.base import NotFoundError
from agent_env.cli.utils import deprecated_option, renamed_value
from agent_env.task_step.task_steps.add_skills import Skill


# ---------------------------------------------------------------------------
# agent-env a2a-agent add-skill --instance-id <id> --skill-artifact-id code-review
# agent-env a2a-agent add-skill --instance-id <id> --skill-md-path ./SKILL.md
# agent-env a2a-agent add-skill --instance-id <id> --skill-s3-url s3://bucket/prefix/
# agent-env a2a-agent add-skill --instance-id <id> --skill-object-url s3://bucket/prefix/
# ---------------------------------------------------------------------------


Expand All @@ -40,23 +42,26 @@
help="Local SKILL.md file to register inline",
)
@click.option(
"--skill-s3-url",
"skill_s3_url",
"--skill-object-url",
"skill_object_url",
default=None,
help="S3 prefix containing a skill directory",
help="Object-store prefix containing a skill directory",
)
@deprecated_option("--skill-s3-url", "skill_s3_url", "--skill-object-url")
def add_skill(
instance_id: str,
skill_artifact_id: Optional[str],
skill_artifact_version: Optional[int],
skill_md_path: Optional[Path],
skill_object_url: Optional[str],
skill_s3_url: Optional[str],
):
"""Register a skill against a deployed A2A agent's /ext/skill-config endpoint."""
sources = [bool(skill_artifact_id), bool(skill_md_path), bool(skill_s3_url)]
skill_object_url = renamed_value("--skill-object-url", skill_object_url, "--skill-s3-url", skill_s3_url)
sources = [bool(skill_artifact_id), bool(skill_md_path), bool(skill_object_url)]
if sum(sources) != 1:
click.echo(
"Error: specify exactly one of --skill-artifact-id, --skill-md-path, --skill-s3-url",
"Error: specify exactly one of --skill-artifact-id, --skill-md-path, --skill-object-url",
err=True,
)
raise SystemExit(1)
Expand All @@ -72,16 +77,12 @@ def add_skill(
if skill_artifact_id is not None:
skill = Skill(skill_artifact_id=skill_artifact_id, skill_artifact_version=skill_artifact_version)
click.echo(f"Registering SkillArtifact '{skill_artifact_id}' v{skill_artifact_version or 'latest'}...")
elif skill_s3_url is not None:
from agent_env.artifact.artifacts.skill import _fetch_skill_md, _parse_skill_md

frontmatter, _ = _parse_skill_md(_fetch_skill_md(skill_s3_url))
skill = Skill(name=frontmatter["name"], description=frontmatter["description"], s3_url=skill_s3_url)
click.echo(f"Registering skill from {skill_s3_url} (name={skill.name})...")
elif skill_object_url is not None:
frontmatter, _ = parse_skill_md(fetch_skill_md(skill_object_url))
skill = Skill(name=frontmatter["name"], description=frontmatter["description"], object_url=skill_object_url)
click.echo(f"Registering skill from {skill_object_url} (name={skill.name})...")
else:
from agent_env.artifact.artifacts.skill import _parse_skill_md

frontmatter, body = _parse_skill_md(skill_md_path.read_bytes())
frontmatter, body = parse_skill_md(skill_md_path.read_bytes())
skill = Skill(
name=frontmatter["name"],
description=frontmatter["description"],
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/cli/artifact/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def cli_put(
click.echo(
f"Created CliArtifact: id={result.id} version={result.version} "
f"command_name={result.command_name} entrypoint={result.entrypoint} "
f"cli_files_id={result.cli_files_id} cli_s3_url={result.cli_object_url}"
f"cli_files_id={result.cli_files_id} cli_object_url={result.cli_object_url}"
)


Expand Down
15 changes: 9 additions & 6 deletions src/agent_env/cli/artifact/file_artifact_universe.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import click

from agent_env.artifact import FileArtifact, FileArtifactUniverse
from agent_env.cli.utils import deprecated_option, renamed_value
from agent_env.store.ids import fs_safe, is_local_id, key_segment


Expand Down Expand Up @@ -67,14 +68,16 @@ def put(universe_id: str, file_artifact_ids: Tuple[str, ...]):
help="Local directory; every file under it is uploaded into the bundle (relative path preserved)",
)
@click.option(
"--s3-url",
"s3_url",
"--prefix-url",
"prefix_url",
default=None,
help="Object-store prefix to upload the bundle under. Defaults to the version's own prefix in the configured "
"object store (artifacts/file_artifact_universe/<id>/<version>/).",
)
def put_bundled(universe_id: str, file_dir: Path, s3_url: Optional[str]):
@deprecated_option("--s3-url", "s3_url", "--prefix-url")
def put_bundled(universe_id: str, file_dir: Path, prefix_url: Optional[str], s3_url: Optional[str]):
"""Upload every file under --file-dir as a single bundled FileArtifactUniverse."""
prefix_url = renamed_value("--prefix-url", prefix_url, "--s3-url", s3_url)
files: dict[str, Path] = {}
for p in sorted(file_dir.rglob("*")):
if p.is_file():
Expand All @@ -83,13 +86,13 @@ def put_bundled(universe_id: str, file_dir: Path, s3_url: Optional[str]):
click.echo(f"Error: no files found under {file_dir}", err=True)
raise SystemExit(1)

click.echo(f"Uploading {len(files)} file(s)" + (f" under {s3_url}..." if s3_url else "..."))
click.echo(f"Uploading {len(files)} file(s)" + (f" under {prefix_url}..." if prefix_url else "..."))
for rel in files:
click.echo(f" {rel}")
universe = FileArtifactUniverse.put_bundled(id=universe_id, files=files, prefix_url=s3_url)
universe = FileArtifactUniverse.put_bundled(id=universe_id, files=files, prefix_url=prefix_url)
click.echo(
f"Created FileArtifactUniverse: id={universe.id} version={universe.version} "
f"file_count={len(universe.file_artifact_ids)} bundle_s3_url={universe.bundle_object_url}"
f"file_count={len(universe.file_artifact_ids)} bundle_object_url={universe.bundle_object_url}"
)


Expand Down
29 changes: 16 additions & 13 deletions src/agent_env/cli/artifact/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import click

from agent_env.artifact import SkillArtifact
from agent_env.artifact.artifacts.skill import _parse_skill_md, download_skill
from agent_env.artifact.artifacts.skill import parse_skill_md, download_skill
from agent_env.cli.utils import deprecated_option, renamed_value


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -42,21 +43,23 @@ def skill():
help="Local skill directory containing SKILL.md at the root",
)
@click.option(
"--s3-url",
"s3_url",
"--prefix-url",
"prefix_url",
default=None,
help="S3 prefix containing the skill (s3://bucket/prefix/) — alternative to --skill-dir",
help="Object-store prefix containing the skill — alternative to --skill-dir",
)
def skill_put(artifact_id: str, skill_dir: Optional[Path], s3_url: Optional[str]):
"""Upload a skill as a SkillArtifact, from a local directory or an S3 prefix."""
if (skill_dir is None) == (s3_url is None):
click.echo("Error: specify exactly one of --skill-dir or --s3-url", err=True)
@deprecated_option("--s3-url", "s3_url", "--prefix-url")
def skill_put(artifact_id: str, skill_dir: Optional[Path], prefix_url: Optional[str], s3_url: Optional[str]):
"""Upload a skill as a SkillArtifact, from a local directory or an object-store prefix."""
prefix_url = renamed_value("--prefix-url", prefix_url, "--s3-url", s3_url)
if (skill_dir is None) == (prefix_url is None):
click.echo("Error: specify exactly one of --skill-dir or --prefix-url", err=True)
raise SystemExit(1)

source_cm = download_skill(s3_url) if s3_url else contextlib.nullcontext(skill_dir)
source_cm = download_skill(prefix_url) if prefix_url else contextlib.nullcontext(skill_dir)
with source_cm as source_dir:
if s3_url:
click.echo(f"Downloaded skill from {s3_url} to {source_dir}")
if prefix_url:
click.echo(f"Downloaded skill from {prefix_url} to {source_dir}")

file_count = sum(1 for p in source_dir.rglob("*") if p.is_file())
click.echo(f"Uploading SkillArtifact bundle ({file_count} file(s))...")
Expand All @@ -65,7 +68,7 @@ def skill_put(artifact_id: str, skill_dir: Optional[Path], s3_url: Optional[str]
f"Created SkillArtifact: id={result.id} version={result.version} "
f"spec_version={result.agent_skills_spec_version} "
f"skill_files_id={result.skill_files_id} "
f"skill_s3_url={result.skill_object_url}"
f"skill_object_url={result.skill_object_url}"
)
click.echo()
click.secho(_BANNER, fg="magenta", bold=True)
Expand All @@ -83,7 +86,7 @@ def skill_put(artifact_id: str, skill_dir: Optional[Path], s3_url: Optional[str]
for k, v in result.skill_metadata.items():
click.echo(f" {click.style(k, fg='cyan')}: {click.style(v, fg='green')}")

_, body = _parse_skill_md((source_dir / _SKILL_MD_FILENAME).read_bytes())
_, body = parse_skill_md((source_dir / _SKILL_MD_FILENAME).read_bytes())
preview = body[:_BODY_PREVIEW_CHARS]
truncated = len(body) > _BODY_PREVIEW_CHARS
click.secho(f"Body (first {_BODY_PREVIEW_CHARS} chars):", fg="cyan", bold=True)
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/cli/env/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,5 +409,5 @@ def create_cli(env_id: str, env_version: int | None, command_name: str | None, f
click.echo(
f"Created CliArtifact: id={artifact.id} version={artifact.version} "
f"command_name={artifact.command_name} entrypoint={artifact.entrypoint} "
f"cli_s3_url={artifact.cli_object_url}"
f"cli_object_url={artifact.cli_object_url}"
)
18 changes: 18 additions & 0 deletions src/agent_env/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ def build_platform_option(f):
)(f)


def deprecated_option(old: str, dest: str, replacement: str):
"""A hidden `old` flag, the deprecated spelling of `replacement`: using it notes the replacement on stderr.
Merge the two with :func:`renamed_value`."""
def note(ctx: click.Context, param: click.Parameter, value: str | None) -> str | None:
if value is not None:
click.echo(f"Warning: {old} is deprecated and will be removed; use {replacement}.", err=True)
return value

return click.option(old, dest, default=None, hidden=True, callback=note)


def renamed_value(new: str, new_value: str | None, old: str, old_value: str | None) -> str | None:
"""The value of flag `new`, or of its deprecated spelling `old`; giving both is a usage error."""
if new_value is not None and old_value is not None:
raise click.UsageError(f"{old} is the deprecated spelling of {new}; give only {new}")
return old_value if new_value is None else new_value


def refuse_unwritable_ids(*ids: str) -> None:
"""Refuse, before anything is built, an id the store a put writes it to wouldn't take, such as an @local id the
image's suffix makes too long."""
Expand Down
Loading
Loading