diff --git a/agent.py b/agent.py index 8165bbf..9cedfd1 100644 --- a/agent.py +++ b/agent.py @@ -298,4 +298,4 @@ def _build_failure_error(exc: BaseException) -> str: "or a smaller build scope" ) return f"build graph failed: {type(exc).__name__}: {exc}" -# rebuild against a2a-pack 0.1.60 for source-grant-aware agent updates +# rebuild against a2a-pack 0.1.75 for source-grant-aware agent updates diff --git a/agent_builder/builder.py b/agent_builder/builder.py index 7f65565..2eee6d5 100644 --- a/agent_builder/builder.py +++ b/agent_builder/builder.py @@ -321,6 +321,7 @@ def build_agent_builder(ctx: BuilderContext) -> Any: settings=settings, cp_jwt=ctx.cp_jwt, workspace=ctx.workspace, + sandbox=ctx.sandbox, grant_token=ctx.grant_token, ) ) diff --git a/agent_builder/skills/a2apack-agent-authoring/SKILL.md b/agent_builder/skills/a2apack-agent-authoring/SKILL.md index 8cfa589..2bebfa5 100644 --- a/agent_builder/skills/a2apack-agent-authoring/SKILL.md +++ b/agent_builder/skills/a2apack-agent-authoring/SKILL.md @@ -29,7 +29,7 @@ Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` for legacy compatibility. In every mode, read `ctx.llm`; never read `A2A_LITELLM_KEY`, `OPENAI_API_KEY`, provider keys, or platform secrets directly. If `ctx.llm.api_key` is empty, return a clear setup/config -result before constructing `ChatOpenAI`. +result before constructing a model or calling `create_a2a_deep_agent`. `Pricing` only accepts `price_per_call_usd`, `caller_pays_llm`, and `notes`. Do not put `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, diff --git a/agent_builder/skills/agent-quality-review/SKILL.md b/agent_builder/skills/agent-quality-review/SKILL.md index dc0442e..bbe0d48 100644 --- a/agent_builder/skills/agent-quality-review/SKILL.md +++ b/agent_builder/skills/agent-quality-review/SKILL.md @@ -41,8 +41,8 @@ Check these items: - `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`. Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or `total_usd=` in `agent.py`; those are derived platform fields. -- Any code path that builds `ChatOpenAI` checks `ctx.llm.api_key` first or - returns a clear setup/config result before the model constructor runs. +- DeepAgents code uses `create_a2a_deep_agent` or another `ctx.llm`-backed + resolver, and checks `ctx.llm.api_key` before model construction. - Any use of DeepAgents file tools passes `backend=ctx.workspace_backend()`. - Any project skills are seeded into the backend and passed with `skills=skill_sources or None`. diff --git a/agent_builder/skills/deepagent-agent-design/SKILL.md b/agent_builder/skills/deepagent-agent-design/SKILL.md index 996617b..85b2c90 100644 --- a/agent_builder/skills/deepagent-agent-design/SKILL.md +++ b/agent_builder/skills/deepagent-agent-design/SKILL.md @@ -1,6 +1,6 @@ --- name: deepagent-agent-design -description: Design generated A2A agents around DeepAgents skills, subagents, durable workspace backends, and ctx.llm-backed reasoning. Use whenever building or modifying an agent, deciding whether to add tools, creating skill bundles, wiring create_deep_agent, or avoiding shallow fake tools. +description: Design generated A2A agents around DeepAgents skills, subagents, durable workspace backends, and ctx.llm-backed reasoning. Use whenever building or modifying an agent, deciding whether to add tools, creating skill bundles, wiring create_a2a_deep_agent, or avoiding shallow fake tools. --- # DeepAgent Agent Design @@ -13,8 +13,8 @@ planning, file work, skill selection, and subagent delegation. Use this shape for non-trivial generated agents: 1. Keep one or two public A2A `@skill` methods with clear typed parameters. -2. Inside each method, read `ctx.llm`, verify credentials are available before - constructing `ChatOpenAI`, and build a DeepAgent with `create_deep_agent`. +2. Inside each method, read `ctx.llm`, verify credentials are available, and + build a DeepAgent with `create_a2a_deep_agent`. 3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the caller's durable workspace instead of LangGraph state. 4. Seed project skills into the current grant's write prefix and pass @@ -44,7 +44,7 @@ linked from `SKILL.md`. Project `skills/` files are source files in the agent image. DeepAgents loads skills from its backend, so seed those files into the invocation workspace -before `create_deep_agent`: +before `create_a2a_deep_agent`: ```python from pathlib import Path @@ -84,8 +84,9 @@ Then: ```python backend = ctx.workspace_backend() skill_sources = _seed_runtime_skills(backend, ctx) -return create_deep_agent( - model=model, +return create_a2a_deep_agent( + ctx, + creds=creds, backend=backend, skills=skill_sources or None, tools=[small_deterministic_tool], diff --git a/agent_builder/skills/deepagents-implementation-patterns/SKILL.md b/agent_builder/skills/deepagents-implementation-patterns/SKILL.md index 8580db2..c49f269 100644 --- a/agent_builder/skills/deepagents-implementation-patterns/SKILL.md +++ b/agent_builder/skills/deepagents-implementation-patterns/SKILL.md @@ -1,12 +1,12 @@ --- name: deepagents-implementation-patterns -description: Implement DeepAgents inside a2a-pack agents with real deterministic tools, project skills, custom subagents, workspace-backed files, and ctx.llm-backed ChatOpenAI models. Use when editing create_deep_agent calls or deciding what belongs in tools, skills, or subagents. +description: Implement DeepAgents inside a2a-pack agents with real deterministic tools, project skills, custom subagents, workspace-backed files, and ctx.llm-backed model resolution. Use when editing create_a2a_deep_agent calls or deciding what belongs in tools, skills, or subagents. --- # DeepAgents Implementation Patterns DeepAgents supplies the inner agent loop. In this platform, the reliable shape -is: `ctx.llm`-backed `ChatOpenAI`, A2A workspace backend, project DeepAgents -skills, and a small number of exact tools. Hosted generated agents should +is: `ctx.llm`-backed `create_a2a_deep_agent`, A2A workspace backend, project +DeepAgents skills, and a small number of exact tools. Hosted generated agents should declare `LLMProvisioning.PLATFORM`; explicit BYOK agents can declare `CALLER_PROVIDED`, but both modes still read the same `ctx.llm` object. @@ -20,8 +20,7 @@ from pathlib import Path from typing import Any from langchain_core.tools import tool -from langchain_openai import ChatOpenAI -from deepagents import create_deep_agent +from a2a_pack.deepagents import create_a2a_deep_agent RUNTIME_SKILLS_DIR = "research-agent/.deepagents/skills/" @@ -39,13 +38,6 @@ def build_graph(ctx: Any) -> Any: "LLM credentials were not available; declare PLATFORM for hosted " "generated agents or configure caller-provided credentials." ) - model = ChatOpenAI( - model=creds.model, - base_url=creds.base_url, - api_key=creds.api_key, - temperature=0.0, - ) - @tool def validate_citations(items_json: str) -> str: """Validate that each citation has title, url, and claim fields.""" @@ -58,8 +50,9 @@ def build_graph(ctx: Any) -> Any: backend = ctx.workspace_backend() skill_sources = seed_runtime_skills(backend, ctx) - return create_deep_agent( - model=model, + return create_a2a_deep_agent( + ctx, + creds=creds, backend=backend, skills=skill_sources or None, tools=[validate_citations], diff --git a/agent_builder/skills/workspace-artifact-safety/SKILL.md b/agent_builder/skills/workspace-artifact-safety/SKILL.md index 6dadb32..25177b8 100644 --- a/agent_builder/skills/workspace-artifact-safety/SKILL.md +++ b/agent_builder/skills/workspace-artifact-safety/SKILL.md @@ -26,7 +26,7 @@ Pass the backend into DeepAgents: ```python backend = ctx.workspace_backend() -graph = create_deep_agent(model=model, backend=backend, skills=skill_sources or None) +graph = create_a2a_deep_agent(ctx, backend=backend, skills=skill_sources or None) ``` For stable, human-readable paths, ask model-created files to use diff --git a/agent_builder/tools.py b/agent_builder/tools.py index 244f26c..b8d7435 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from .config import Settings -A2A_PACK_MIN_VERSION = "0.1.60" +A2A_PACK_MIN_VERSION = "0.1.75" @dataclass(frozen=True) @@ -40,6 +40,7 @@ class ToolContext: settings: "Settings" cp_jwt: str | None = None workspace: Any | None = None + sandbox: Any | None = None grant_token: str | None = None @@ -444,6 +445,29 @@ def build_tools(ctx: ToolContext) -> list[Any]: " a2a frontend info --project . || true\n" "fi\n" ) + if ctx.sandbox is not None: + try: + out = await ctx.sandbox.run_shell( + script, + image=settings.image, + workspace=bucket, + memory_mib=512, + timeout_seconds=settings.sandbox_timeout_s, + ) + except Exception as exc: # noqa: BLE001 + return json.dumps({ + "error": f"sandbox {type(exc).__name__}", + "detail": str(exc)[:1000], + }) + result: dict[str, Any] = { + "exit_code": getattr(out, "exit_code", None), + "stdout": (getattr(out, "stdout", "") or "")[:4000], + "stderr": (getattr(out, "stderr", "") or "")[:4000], + } + if sanitized_fields: + result["sanitized_agent_card_fields"] = list(sanitized_fields) + return json.dumps(result) + headers = _sandbox_headers(settings.sandbox_token, ctx.grant_token) async with httpx.AsyncClient(timeout=settings.sandbox_timeout_s + 30) as c: r = await c.post( diff --git a/requirements.txt b/requirements.txt index 07ba47a..f3ab529 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -a2a-pack>=0.1.60 +a2a-pack>=0.1.75 httpx>=0.27 boto3>=1.34 deepagents>=0.5.0 diff --git a/tests/test_builder_prompt.py b/tests/test_builder_prompt.py index abab23e..62b56e4 100644 --- a/tests/test_builder_prompt.py +++ b/tests/test_builder_prompt.py @@ -162,7 +162,7 @@ inline = Pricing(price_per_call_usd=0.01, compute={"runtime_seconds": 600}, tota self.assertIn("skills=skill_sources or None", files["deepagent-agent-design/SKILL.md"]) self.assertIn("from a2a_pack import", files["a2apack-agent-authoring/SKILL.md"]) self.assertIn("ctx.workspace_shell", files["a2apack-agent-authoring/SKILL.md"]) - self.assertIn("create_deep_agent", files["deepagents-implementation-patterns/SKILL.md"]) + self.assertIn("create_a2a_deep_agent", files["deepagents-implementation-patterns/SKILL.md"]) self.assertIn('config={"recursion_limit": 500}', files["deepagents-implementation-patterns/SKILL.md"]) self.assertIn("ctx.write_artifact", files["workspace-artifact-safety/SKILL.md"]) self.assertIn("ctx.workspace_shell", files["workspace-artifact-safety/SKILL.md"]) diff --git a/tests/test_template_init.py b/tests/test_template_init.py index 9b0d138..76c9f8b 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -47,7 +47,7 @@ class TemplateInitTests(unittest.TestCase): self.assertIn("skills=skill_sources or None", files["agent.py"]) self.assertIn("ctx.llm", files["agent.py"]) self.assertIn("ctx.workspace_backend()", files["agent.py"]) - self.assertIn("create_deep_agent", files["agent.py"]) + self.assertIn("create_a2a_deep_agent", files["agent.py"]) self.assertIn("name: research-agent", files["a2a.yaml"]) self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"]) self.assertNotIn("{{ frontend_block }}", files["a2a.yaml"]) @@ -272,6 +272,35 @@ class TemplateInitTests(unittest.TestCase): self.assertIn("manifest_json must be a JSON object", result["error"]) + def test_test_agent_in_sandbox_uses_attached_sandbox_client(self) -> None: + workspace = _FakeWorkspace() + prefix = "agents/demo-agent/" + workspace.write_bytes(prefix + "agent.py", b"print('ok')\n") + workspace.write_bytes(prefix + "a2a.yaml", b"name: demo-agent\n") + workspace.write_bytes(prefix + "requirements.txt", b"") + sandbox = _FakeSandbox() + tools = build_tools( + ToolContext( + bucket="bucket", + settings=_settings(), + workspace=workspace, + sandbox=sandbox, + grant_token="grant-token", + ) + ) + test_tool = _tool_by_name(tools, "test_agent_in_sandbox") + + _FakeAsyncClient.posts = [] + with patch("agent_builder.tools.httpx.AsyncClient", _FakeAsyncClient): + result = json.loads( + asyncio.run(test_tool.ainvoke({"name": "demo-agent"})) + ) + + self.assertEqual(result["exit_code"], 0) + self.assertEqual(len(sandbox.calls), 1) + self.assertEqual(sandbox.calls[0]["workspace"], "bucket") + self.assertEqual(_FakeAsyncClient.posts, []) + def test_cp_deploy_tarball_posts_agent_dsl(self) -> None: workspace = _FakeWorkspace() prefix = "agents/demo-agent/" @@ -529,6 +558,21 @@ class _FakeWorkspace: self.objects.pop(key, None) +class _FakeExecResult: + exit_code = 0 + stdout = "--- card ---\n{}" + stderr = "" + + +class _FakeSandbox: + def __init__(self) -> None: + self.calls: list[dict[str, object]] = [] + + async def run_shell(self, script: str, **kwargs: object) -> _FakeExecResult: + self.calls.append({"script": script, **kwargs}) + return _FakeExecResult() + + def _tool_by_name(tools: list[object], name: str) -> object: for tool in tools: if getattr(tool, "name", None) == name: