builder: require durable workspace execution
Some checks failed
build / build (push) Failing after 2s

This commit is contained in:
robert
2026-05-19 11:28:37 -03:00
parent 960c3558c9
commit d0fa3f3f5f
11 changed files with 126 additions and 34 deletions

View File

@@ -101,7 +101,8 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]):
),
}
]
}
},
config={"recursion_limit": 500},
)
report_text = _last_message_text(state)
ref = await ctx.write_artifact(
@@ -127,6 +128,17 @@ If a skill calls `ctx.workspace_backend()` or `ctx.workspace`, declare
`workspace_access`. If it writes user-visible files, also emit artifacts with
`ctx.write_artifact` and `ctx.emit_artifact`.
If a skill runs code, renderers, converters, or data tools that create
downloadable files, run them through `await ctx.workspace_shell(...)` or
`await ctx.workspace_python(...)` and make the command write under
`/workspace/outputs/...`. In-process subprocesses write inside the agent pod,
not the caller's mounted workspace.
When invoking an inner DeepAgents graph, pass
`config={"recursion_limit": 500}` to `graph.ainvoke(...)` or
`graph.astream_events(...)`. Complex skill/subagent workflows can exceed
LangGraph defaults during normal operation.
If an agent needs system binaries, mirror the need in `a2a.yaml`:
```yaml

View File

@@ -37,6 +37,9 @@ Check these items:
- Custom subagents that need project skills include their own `skills` field.
- Deterministic tools do exact work; no fake canned-response tools.
- File-producing skills call `ctx.write_artifact` and `ctx.emit_artifact`.
- Commands that create downloadable outputs run through `ctx.workspace_shell`
or `ctx.workspace_python` and write under `/workspace/outputs/...`. Plain
subprocesses in the agent container are not durable workspace writes.
- External calls, secrets, workspace access, pricing, and resources are
declared on the class.

View File

@@ -79,6 +79,15 @@ return create_deep_agent(
)
```
Invoke the graph with the same recursion budget agent-builder uses:
```python
state = await graph.ainvoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"recursion_limit": 500},
)
```
Because this uses `/outputs/...`, the write fits the normal A2A workspace
grant. If the agent uses `ctx.workspace_backend()`, also declare
`workspace_access = WorkspaceAccess.dynamic(...)` on the A2A agent class.

View File

@@ -127,3 +127,18 @@ for generated agents that read or write files.
Use `skills=skill_sources or None`, not an empty list, so agents without a
`skills/` directory still build cleanly.
## Invocation Budget
Generated agents should use the same recursion budget as agent-builder:
```python
state = await graph.ainvoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"recursion_limit": 500},
)
```
For streaming/event loops, pass the same config to `graph.astream_events(...)`.
This prevents normal multi-step skill/subagent workflows from failing at the
default LangGraph recursion cap.

View File

@@ -55,33 +55,33 @@ ref = await ctx.write_artifact("frames.zip", zip_bytes, "application/zip")
await ctx.emit_artifact(ref)
```
## Bounded Subprocesses
## Workspace-Mounted Execution
Media, rendering, archive, and data conversion agents must not run unbounded
commands. Use explicit timeouts and return structured errors:
Media, rendering, archive, and data conversion agents must run commands through
the workspace-mounted sandbox helpers when those commands create files:
```python
import asyncio
async def run_checked(cmd: list[str], timeout_s: int = 120) -> dict[str, str | int]:
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout_s)
except asyncio.TimeoutError:
return {"ok": 0, "error": "command timed out", "timeout_s": timeout_s}
async def run_checked(script: str, timeout_s: int = 120) -> dict[str, str | int]:
result = await ctx.workspace_shell(
script,
image="python:3.11-slim",
timeout_seconds=timeout_s,
memory_mib=1024,
cpus=1,
)
return {
"ok": 1 if proc.returncode == 0 else 0,
"returncode": proc.returncode,
"stdout": stdout.decode("utf-8", errors="replace")[-4000:],
"stderr": stderr.decode("utf-8", errors="replace")[-4000:],
"ok": 1 if result.ok else 0,
"returncode": result.exit_code,
"stdout": result.stdout[-4000:],
"stderr": result.stderr[-4000:],
}
```
Commands should write user-visible files under `/workspace/outputs/...`; that
path is backed by the caller's workspace bucket. Do not use
`asyncio.create_subprocess_exec(...)` for durable outputs. Plain subprocesses
run inside the agent container, which is not mounted to the caller workspace.
If a nonzero command still produced a usable output, preserve the output and
include the command failure as a warning instead of discarding the file.