Use sandbox client for builder validation
All checks were successful
build / build (push) Successful in 14s
All checks were successful
build / build (push) Successful in 14s
This commit is contained in:
2
agent.py
2
agent.py
@@ -298,4 +298,4 @@ def _build_failure_error(exc: BaseException) -> str:
|
|||||||
"or a smaller build scope"
|
"or a smaller build scope"
|
||||||
)
|
)
|
||||||
return f"build graph failed: {type(exc).__name__}: {exc}"
|
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
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ def build_agent_builder(ctx: BuilderContext) -> Any:
|
|||||||
settings=settings,
|
settings=settings,
|
||||||
cp_jwt=ctx.cp_jwt,
|
cp_jwt=ctx.cp_jwt,
|
||||||
workspace=ctx.workspace,
|
workspace=ctx.workspace,
|
||||||
|
sandbox=ctx.sandbox,
|
||||||
grant_token=ctx.grant_token,
|
grant_token=ctx.grant_token,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` for legacy compatibility.
|
|||||||
In every mode, read `ctx.llm`;
|
In every mode, read `ctx.llm`;
|
||||||
never read `A2A_LITELLM_KEY`, `OPENAI_API_KEY`, provider keys, or platform
|
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
|
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`.
|
`Pricing` only accepts `price_per_call_usd`, `caller_pays_llm`, and `notes`.
|
||||||
Do not put `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`,
|
Do not put `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`,
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ Check these items:
|
|||||||
- `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`.
|
- `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`.
|
||||||
Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or
|
Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or
|
||||||
`total_usd=` in `agent.py`; those are derived platform fields.
|
`total_usd=` in `agent.py`; those are derived platform fields.
|
||||||
- Any code path that builds `ChatOpenAI` checks `ctx.llm.api_key` first or
|
- DeepAgents code uses `create_a2a_deep_agent` or another `ctx.llm`-backed
|
||||||
returns a clear setup/config result before the model constructor runs.
|
resolver, and checks `ctx.llm.api_key` before model construction.
|
||||||
- Any use of DeepAgents file tools passes `backend=ctx.workspace_backend()`.
|
- Any use of DeepAgents file tools passes `backend=ctx.workspace_backend()`.
|
||||||
- Any project skills are seeded into the backend and passed with
|
- Any project skills are seeded into the backend and passed with
|
||||||
`skills=skill_sources or None`.
|
`skills=skill_sources or None`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: deepagent-agent-design
|
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
|
# DeepAgent Agent Design
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ planning, file work, skill selection, and subagent delegation.
|
|||||||
Use this shape for non-trivial generated agents:
|
Use this shape for non-trivial generated agents:
|
||||||
|
|
||||||
1. Keep one or two public A2A `@skill` methods with clear typed parameters.
|
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
|
2. Inside each method, read `ctx.llm`, verify credentials are available, and
|
||||||
constructing `ChatOpenAI`, and build a DeepAgent with `create_deep_agent`.
|
build a DeepAgent with `create_a2a_deep_agent`.
|
||||||
3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the
|
3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the
|
||||||
caller's durable workspace instead of LangGraph state.
|
caller's durable workspace instead of LangGraph state.
|
||||||
4. Seed project skills into the current grant's write prefix and pass
|
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
|
Project `skills/` files are source files in the agent image. DeepAgents loads
|
||||||
skills from its backend, so seed those files into the invocation workspace
|
skills from its backend, so seed those files into the invocation workspace
|
||||||
before `create_deep_agent`:
|
before `create_a2a_deep_agent`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -84,8 +84,9 @@ Then:
|
|||||||
```python
|
```python
|
||||||
backend = ctx.workspace_backend()
|
backend = ctx.workspace_backend()
|
||||||
skill_sources = _seed_runtime_skills(backend, ctx)
|
skill_sources = _seed_runtime_skills(backend, ctx)
|
||||||
return create_deep_agent(
|
return create_a2a_deep_agent(
|
||||||
model=model,
|
ctx,
|
||||||
|
creds=creds,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
skills=skill_sources or None,
|
skills=skill_sources or None,
|
||||||
tools=[small_deterministic_tool],
|
tools=[small_deterministic_tool],
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: deepagents-implementation-patterns
|
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 Implementation Patterns
|
||||||
|
|
||||||
DeepAgents supplies the inner agent loop. In this platform, the reliable shape
|
DeepAgents supplies the inner agent loop. In this platform, the reliable shape
|
||||||
is: `ctx.llm`-backed `ChatOpenAI`, A2A workspace backend, project DeepAgents
|
is: `ctx.llm`-backed `create_a2a_deep_agent`, A2A workspace backend, project
|
||||||
skills, and a small number of exact tools. Hosted generated agents should
|
DeepAgents skills, and a small number of exact tools. Hosted generated agents should
|
||||||
declare `LLMProvisioning.PLATFORM`; explicit BYOK agents can declare
|
declare `LLMProvisioning.PLATFORM`; explicit BYOK agents can declare
|
||||||
`CALLER_PROVIDED`, but both modes still read the same `ctx.llm` object.
|
`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 typing import Any
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from langchain_openai import ChatOpenAI
|
from a2a_pack.deepagents import create_a2a_deep_agent
|
||||||
from deepagents import create_deep_agent
|
|
||||||
|
|
||||||
RUNTIME_SKILLS_DIR = "research-agent/.deepagents/skills/"
|
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 "
|
"LLM credentials were not available; declare PLATFORM for hosted "
|
||||||
"generated agents or configure caller-provided credentials."
|
"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
|
@tool
|
||||||
def validate_citations(items_json: str) -> str:
|
def validate_citations(items_json: str) -> str:
|
||||||
"""Validate that each citation has title, url, and claim fields."""
|
"""Validate that each citation has title, url, and claim fields."""
|
||||||
@@ -58,8 +50,9 @@ def build_graph(ctx: Any) -> Any:
|
|||||||
|
|
||||||
backend = ctx.workspace_backend()
|
backend = ctx.workspace_backend()
|
||||||
skill_sources = seed_runtime_skills(backend, ctx)
|
skill_sources = seed_runtime_skills(backend, ctx)
|
||||||
return create_deep_agent(
|
return create_a2a_deep_agent(
|
||||||
model=model,
|
ctx,
|
||||||
|
creds=creds,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
skills=skill_sources or None,
|
skills=skill_sources or None,
|
||||||
tools=[validate_citations],
|
tools=[validate_citations],
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Pass the backend into DeepAgents:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
backend = ctx.workspace_backend()
|
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
|
For stable, human-readable paths, ask model-created files to use
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
|
|||||||
from .config import Settings
|
from .config import Settings
|
||||||
|
|
||||||
|
|
||||||
A2A_PACK_MIN_VERSION = "0.1.60"
|
A2A_PACK_MIN_VERSION = "0.1.75"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -40,6 +40,7 @@ class ToolContext:
|
|||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
cp_jwt: str | None = None
|
cp_jwt: str | None = None
|
||||||
workspace: Any | None = None
|
workspace: Any | None = None
|
||||||
|
sandbox: Any | None = None
|
||||||
grant_token: str | None = None
|
grant_token: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -444,6 +445,29 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
" a2a frontend info --project . || true\n"
|
" a2a frontend info --project . || true\n"
|
||||||
"fi\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)
|
headers = _sandbox_headers(settings.sandbox_token, ctx.grant_token)
|
||||||
async with httpx.AsyncClient(timeout=settings.sandbox_timeout_s + 30) as c:
|
async with httpx.AsyncClient(timeout=settings.sandbox_timeout_s + 30) as c:
|
||||||
r = await c.post(
|
r = await c.post(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
a2a-pack>=0.1.60
|
a2a-pack>=0.1.75
|
||||||
httpx>=0.27
|
httpx>=0.27
|
||||||
boto3>=1.34
|
boto3>=1.34
|
||||||
deepagents>=0.5.0
|
deepagents>=0.5.0
|
||||||
|
|||||||
@@ -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("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("from a2a_pack import", files["a2apack-agent-authoring/SKILL.md"])
|
||||||
self.assertIn("ctx.workspace_shell", 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('config={"recursion_limit": 500}', files["deepagents-implementation-patterns/SKILL.md"])
|
||||||
self.assertIn("ctx.write_artifact", files["workspace-artifact-safety/SKILL.md"])
|
self.assertIn("ctx.write_artifact", files["workspace-artifact-safety/SKILL.md"])
|
||||||
self.assertIn("ctx.workspace_shell", files["workspace-artifact-safety/SKILL.md"])
|
self.assertIn("ctx.workspace_shell", files["workspace-artifact-safety/SKILL.md"])
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class TemplateInitTests(unittest.TestCase):
|
|||||||
self.assertIn("skills=skill_sources or None", files["agent.py"])
|
self.assertIn("skills=skill_sources or None", files["agent.py"])
|
||||||
self.assertIn("ctx.llm", files["agent.py"])
|
self.assertIn("ctx.llm", files["agent.py"])
|
||||||
self.assertIn("ctx.workspace_backend()", 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("name: research-agent", files["a2a.yaml"])
|
||||||
self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"])
|
self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"])
|
||||||
self.assertNotIn("{{ frontend_block }}", 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"])
|
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:
|
def test_cp_deploy_tarball_posts_agent_dsl(self) -> None:
|
||||||
workspace = _FakeWorkspace()
|
workspace = _FakeWorkspace()
|
||||||
prefix = "agents/demo-agent/"
|
prefix = "agents/demo-agent/"
|
||||||
@@ -529,6 +558,21 @@ class _FakeWorkspace:
|
|||||||
self.objects.pop(key, None)
|
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:
|
def _tool_by_name(tools: list[object], name: str) -> object:
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
if getattr(tool, "name", None) == name:
|
if getattr(tool, "name", None) == name:
|
||||||
|
|||||||
Reference in New Issue
Block a user