diff --git a/agent_builder/builder.py b/agent_builder/builder.py index 15194ec..f0ef686 100644 --- a/agent_builder/builder.py +++ b/agent_builder/builder.py @@ -94,6 +94,23 @@ Your tools: - cp_deploy_tarball(name, version="0.1.0", public=True) — ship it to the platform; returns the public URL + - cp_refresh_agent(name) — force the control plane to re-fetch + the agent's live card after an + out-of-band redeploy + - list_a2a_pack(subdir="") — browse the installed a2a_pack SDK + - read_a2a_pack(path) — read SDK source. Use these when + you're unsure what's exposed. The + code you scaffold runs on this + exact SDK, so reading it is the + authoritative reference — start + with ``read_a2a_pack("agent.py")`` + for ``@skill`` semantics, + ``context.py`` for ``RunContext`` + (workspace, sandbox, emit_progress, + ask, collect, request_scope), + ``runtime.py`` for ``AgentRuntime`` + fields (apt_packages, pricing, + egress, etc.). Discipline: diff --git a/agent_builder/tools.py b/agent_builder/tools.py index bf14e73..3ca7e06 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -352,9 +352,91 @@ def build_tools(ctx: ToolContext) -> list[Any]: "card": body.get("card") or {}, }) + @tool + def list_a2a_pack(subdir: str = "") -> str: + """List ``.py`` files under the installed ``a2a_pack`` package. + + Use this when you need to recall what the SDK actually exposes + — what decorators exist, what ``RunContext`` methods are + available, how ``WorkspaceClient`` is shaped, what the + ``AgentRuntime`` fields are, etc. The agent code you scaffold + runs on this exact package, so reading it is the authoritative + reference (the system-prompt examples are intentionally + abridged). + + ``subdir`` is an optional path segment relative to the package + root (``""``, ``"serve"``, ``"mcp"`` …). Returns JSON: + ``{root, files: [{path, size}]}``. + """ + try: + import a2a_pack + import os as _os + base = _os.path.dirname(_os.path.abspath(a2a_pack.__file__)) + except Exception as exc: # noqa: BLE001 + return json.dumps({"error": f"a2a_pack not importable: {exc}"}) + if subdir.startswith("/") or ".." in subdir.split("/"): + return json.dumps({"error": "subdir must be relative; '..' disallowed"}) + root = _os.path.join(base, subdir) if subdir else base + if not _os.path.commonpath([base, _os.path.abspath(root)]) == base: + return json.dumps({"error": "subdir escapes package root"}) + if not _os.path.isdir(root): + return json.dumps({"error": f"not a directory: {subdir}"}) + out: list[dict[str, Any]] = [] + for dirpath, _, filenames in _os.walk(root): + for fname in filenames: + if not fname.endswith(".py"): + continue + full = _os.path.join(dirpath, fname) + rel = _os.path.relpath(full, base) + try: + size = _os.path.getsize(full) + except OSError: + continue + out.append({"path": rel, "size": size}) + out.sort(key=lambda r: r["path"]) + return json.dumps({"root": base, "files": out}) + + @tool + def read_a2a_pack(path: str) -> str: + """Read a file from the installed ``a2a_pack`` package. + + Path is relative to the package root, e.g. ``agent.py``, + ``context.py``, ``runtime.py``, ``workspace.py``, ``card.py``, + ``mcp/server.py``. Returns JSON: ``{path, content}``. Capped + at 64 KiB; reach for ``list_a2a_pack`` first if you need to + chase a deeper file. + """ + try: + import a2a_pack + import os as _os + base = _os.path.dirname(_os.path.abspath(a2a_pack.__file__)) + except Exception as exc: # noqa: BLE001 + return json.dumps({"error": f"a2a_pack not importable: {exc}"}) + if path.startswith("/") or ".." in path.split("/"): + return json.dumps({"error": "path must be relative; '..' disallowed"}) + full = _os.path.abspath(_os.path.join(base, path)) + if _os.path.commonpath([base, full]) != base: + return json.dumps({"error": "path escapes package root"}) + if not _os.path.isfile(full): + return json.dumps({"error": f"not a file: {path}"}) + try: + with open(full, "r", encoding="utf-8") as fh: + content = fh.read(64 * 1024 + 1) + except OSError as exc: + return json.dumps({"error": str(exc)}) + truncated = len(content) > 64 * 1024 + if truncated: + content = content[: 64 * 1024] + return json.dumps({ + "path": path, + "content": content, + "truncated": truncated, + }) + return [ list_agent_files, write_agent_file, read_agent_file, test_agent_in_sandbox, cp_deploy_tarball, cp_refresh_agent, + list_a2a_pack, read_a2a_pack, ]