commit fef061eca126d66792d270d363b808219bd05ce6 Author: a2a-platform Date: Wed Jul 8 20:35:23 2026 +0000 deploy diff --git a/.a2a/agent.dsl.json b/.a2a/agent.dsl.json new file mode 100644 index 0000000..43a3068 --- /dev/null +++ b/.a2a/agent.dsl.json @@ -0,0 +1,142 @@ +{ + "schema_version": "2026-06-04", + "language": "python", + "name": "hello-world", + "description": "A new A2A agent", + "version": "0.1.0", + "entrypoint": { + "module": "agent", + "class_name": "HelloWorld", + "function": null, + "command": [] + }, + "skills": [ + { + "name": "ask", + "description": "Ask the starter DeepAgent to answer with tool calls when useful", + "handler": "ask", + "tags": [], + "scopes": [], + "stream": false, + "policy": { + "timeout_seconds": null, + "idempotent": false, + "max_retries": 0, + "cost_class": null, + "allow_scope_expansion": false, + "grant_mode": null, + "grant_allow_patterns": [], + "grant_deny_patterns": [], + "grant_outputs_prefix": null, + "grant_write_prefixes": [], + "grant_ttl_seconds": null, + "grant_run_timeout_seconds": null, + "grant_approval_timeout_seconds": null, + "grant_scope_approval_timeout_seconds": null + }, + "input_schema": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + }, + "required": [ + "prompt" + ], + "additionalProperties": false + }, + "output_schema": { + "type": "string" + } + } + ], + "capabilities": {}, + "input_modes": [ + "application/json" + ], + "output_modes": [ + "application/json" + ], + "required_secrets": [], + "required_env": [], + "consumer_setup": { + "fields": [] + }, + "runtime": { + "lifecycle": "ephemeral", + "availability": "on_demand", + "state": "none", + "sandbox": "microsandbox", + "resources": { + "cpu": "100m", + "memory": "256Mi", + "gpu": 0, + "max_runtime_seconds": 600 + }, + "concurrency": 1, + "egress": { + "allow_hosts": [], + "allow_internal_services": [], + "deny_internet_by_default": true + }, + "tools_used": [ + "deepagents", + "langchain" + ], + "llm_provisioning": "platform", + "pricing": { + "price_per_call_usd": 0.0, + "caller_pays_llm": true, + "notes": "Starter agent uses the caller's saved LLM credential via ctx.llm." + }, + "wants_cp_jwt": false, + "platform_resources": { + "memory": null, + "databases": [] + }, + "endpoints": [], + "apt_packages": [] + }, + "template_lineage": null, + "meta_agent_manifest": null, + "state_schema": null, + "workspace_access": { + "enabled": true, + "max_files": 64, + "allowed_modes": [ + "read_only", + "read_write_overlay" + ], + "require_reason": false, + "deny_patterns": [], + "require_human_approval": false, + "max_total_size_bytes": 104857600 + }, + "config_schema": { + "properties": {}, + "title": "HelloWorldConfig", + "type": "object" + }, + "auth": { + "model": "a2a_pack.auth.NoAuth", + "strategy": "public", + "principal_schema": { + "additionalProperties": false, + "description": "Public agent: no caller identity required.", + "properties": {}, + "title": "NoAuth", + "type": "object" + }, + "resolver": null, + "required": false + }, + "metadata": { + "source": "python-a2a-pack", + "project_manifest": { + "name": "hello-world", + "version": "0.1.0", + "entrypoint": "agent:HelloWorld" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..afedd3f --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# hello-world + +A new A2A agent + +This project was scaffolded with `a2a init`. It starts as a DeepAgents-backed +A2A agent that uses the caller's saved LLM credential from `ctx.llm`. + +`LLMProvisioning.PLATFORM` and `LLMProvisioning.CALLER_PROVIDED` both read that +same `ctx.llm` path. Do not read provider keys, LiteLLM master keys, +`OPENAI_API_KEY`, or `A2A_LITELLM_KEY` directly in agent code, and do not +substitute fake fallback API keys when `ctx.llm.api_key` is empty. + +## Durable Files + +A2A Cloud workspaces are grant-scoped and backed by MinIO. Files become durable +when your agent uses one of the platform-owned file paths: + +- `ctx.workspace` for direct workspace reads and writes +- `ctx.write_artifact(...)` for explicit output artifacts +- `ctx.sandbox` for commands that read or write files in a sandbox +- `ctx.workspace_backend()` for framework file tools such as DeepAgents + +DeepAgents has its own built-in file tools (`write_file`, `read_file`, +`edit_file`). Without an A2A backend, those tools write into LangGraph state +only, so files can appear to the agent but never reach MinIO or `/workspace`. + +Keep this line when building DeepAgents graphs: + +```python +backend = ctx.workspace_backend() +return create_a2a_deep_agent(ctx, backend=backend, tools=[...]) +``` + +Invoke DeepAgents graphs with the starter recursion budget: + +```python +state = await graph.ainvoke( + {"messages": [{"role": "user", "content": prompt}]}, + config={"recursion_limit": 500}, +) +``` + +The backend respects the caller's grant. In handoffs, generated files should go +through `ctx.workspace_backend()`, `ctx.write_artifact(...)`, or a sandbox helper +so they are mirrored to the caller workspace instead of becoming private virtual +files. + +For stable, human-readable paths, write intentional outputs to +`/workspace/outputs/...` or `ctx.write_artifact(...)`. If sandboxed code writes +to process-local paths such as `/tmp/result.csv`, `/root`, or `/app`, the +platform captures changed rootfs files under `outputs/rootfs-captures/...` so +the caller can still download and inspect them. + +When a skill needs to run real code, render media, convert files, or call a +CLI that writes outputs, use the workspace-mounted sandbox helpers: + +```python +result = await ctx.workspace_shell( + "python script.py --out /tmp/result.txt", + image="python:3.11-slim", + timeout_seconds=120, +) +``` + +Do not rely on `asyncio.create_subprocess_exec(...)` for durable outputs. A +plain subprocess runs in the agent container, which is not mounted to the +caller workspace; files it creates in `/tmp` or the image filesystem can vanish +after the request. Use the sandbox helpers for any file-producing toolchain. + +## DeepAgents Skills + +If this project grows reusable workflow knowledge, add source-controlled skill +folders under `skills//SKILL.md`. The starter's `_seed_runtime_skills` +helper copies those packaged skills into the invocation workspace and passes the +resulting source path to `create_a2a_deep_agent(..., skills=[...])`, which is +how DeepAgents discovers skills with progressive disclosure. + +## Run Locally + +```bash +python -m pip install -r requirements.txt +a2a dev +a2a test +a2a test --invoke --skill summarize --args-json '{"text":"hello"}' +a2a card +``` + +`a2a dev` loads `.env.local`, creates `.a2a/workspace/{inputs,outputs}`, serves +the same HTTP invoke/card/MCP endpoints as production, and hot reloads local +code. Files written through `ctx.workspace_backend()` land under +`.a2a/workspace/outputs` before you deploy. + +## Auth + +The template is public by default (`auth_model = NoAuth`). To require the +caller's app login, declare a typed auth model and resolver in `agent.py`. +Resolvers receive the inbound bearer token and return the principal exposed as +`ctx.auth`. + +Hosted browser/direct invokes that use `LLMProvisioning.PLATFORM` still need an +A2A Cloud session so the runtime can mint a short-lived LLM/workspace grant for +that user. Agent-to-agent handoffs pass that grant explicitly. + +```python +from a2a_pack import JWTAuth, OIDCUserInfoAuthResolver + +auth_model = JWTAuth +auth_resolver = OIDCUserInfoAuthResolver( + "https://auth.example.com/oauth2/userinfo", + auth_model=JWTAuth, +) +``` + +For homegrown auth or SAML-backed apps, expose a bearer-token `/me` or +`/introspect` endpoint and use the same resolver contract. + +## Deploy + +```bash +a2a deploy +``` diff --git a/a2a.yaml b/a2a.yaml new file mode 100644 index 0000000..5fe1df2 --- /dev/null +++ b/a2a.yaml @@ -0,0 +1,20 @@ +# Project identity for `a2a deploy`. Most metadata (resources, scopes, +# secrets, workspace, etc.) lives on the Python class — this file only +# tells the CLI how to find it. +name: hello-world +version: 0.1.0 +entrypoint: agent:HelloWorld +expose: + public: true +# Optional platform resources: +# resources: +# memory: +# tiers: [kv, vector] +# namespace: hello-world +# databases: +# - name: app +# provider: neon +# engine: postgres +# env: +# url: DATABASE_URL + diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..6658f7c --- /dev/null +++ b/agent.py @@ -0,0 +1,189 @@ +"""hello-world agent. + +Starter stack: + - DeepAgents for tool-calling orchestration + - Caller-provided LLM credentials via ctx.llm + - A tiny model-call middleware hook you can replace with tracing, + routing, rate limits, or policy checks +""" +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from pydantic import BaseModel + +from a2a_pack import ( + A2AAgent, + LLMProvisioning, + NoAuth, + Pricing, + RunContext, + WorkspaceAccess, + WorkspaceMode, + skill, +) +from a2a_pack.context import LLMCreds + + +class HelloWorldConfig(BaseModel): + pass + + +SYSTEM_PROMPT = """\ +You are a compact tool-calling agent. + +Use the text_stats tool when the user asks about text, counts, summaries, +or anything where exact length/word numbers would help. Mention tool results +briefly instead of dumping raw JSON. +""" + +RUNTIME_SKILLS_DIR = "hello-world/.deepagents/skills/" +DEEPAGENTS_RECURSION_LIMIT = 500 + + +class HelloWorld(A2AAgent[HelloWorldConfig, NoAuth]): + name = "hello-world" + description = "A new A2A agent" + version = "0.1.0" + + config_model = HelloWorldConfig + auth_model = NoAuth + + # Hosted generated agents read the caller's saved LLM credential through + # ctx.llm. The platform may proxy that credential through LiteLLM, but agent + # code never reads provider keys, LiteLLM master keys, or OPENAI_API_KEY + # directly. + llm_provisioning = LLMProvisioning.PLATFORM + pricing = Pricing( + price_per_call_usd=0.0, + caller_pays_llm=True, + notes="Starter agent uses the caller's saved LLM credential via ctx.llm.", + ) + workspace_access = WorkspaceAccess.dynamic( + max_files=64, + allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), + require_reason=False, + ) + tools_used = ("deepagents", "langchain") + + @skill(description="Ask the starter DeepAgent to answer with tool calls when useful") + async def ask(self, ctx: RunContext[NoAuth], prompt: str) -> str: + creds = ctx.llm + await ctx.emit_progress(f"llm: {creds.model} via {creds.source}") + if not creds.api_key: + return ( + "LLM key required. Add an LLM credential in Settings > LLM " + "credentials before running this agent; for local --invoke " + "runs set AGENT_LLM_KEY." + ) + graph = self._build_deep_agent(ctx=ctx, creds=creds) + state = await graph.ainvoke( + {"messages": [{"role": "user", "content": prompt}]}, + config={"recursion_limit": DEEPAGENTS_RECURSION_LIMIT}, + ) + await ctx.emit_progress("deepagent finished") + return _last_message_text(state) + + def _build_deep_agent( + self, + *, + ctx: RunContext[NoAuth], + creds: LLMCreds, + ) -> Any: + # Lazy imports keep `a2a card` usable before local dependencies are + # installed. `a2a deploy` installs requirements.txt during the build. + from a2a_pack.deepagents import create_a2a_deep_agent + from langchain.agents.middleware import wrap_model_call + from langchain_core.tools import tool + + @tool + def text_stats(text: str) -> str: + """Return exact word, character, and line counts for text.""" + words = [part for part in text.split() if part.strip()] + return json.dumps( + { + "characters": len(text), + "words": len(words), + "lines": len(text.splitlines()) or 1, + } + ) + + @wrap_model_call + async def log_model_call(request: Any, handler: Any) -> Any: + messages = request.state.get("messages", []) + print( + "[middleware] model_call " + f"model={creds.model} source={creds.source} messages={len(messages)}" + ) + return await handler(request) + + backend = ctx.workspace_backend() + skill_sources = _seed_runtime_skills(backend, ctx) + # create_a2a_deep_agent resolves provider:model strings with + # langchain.init_chat_model from ctx.llm, preserving LiteLLM routing, + # provider-specific extra body, and runtime model overrides. + return create_a2a_deep_agent( + ctx, + creds=creds, + backend=backend, + skills=skill_sources or None, + tools=[text_stats], + middleware=[log_model_call], + system_prompt=SYSTEM_PROMPT, + ) + + +def _runtime_skills_root(ctx: RunContext[Any]) -> str: + workspace = getattr(ctx, "_workspace", None) + prefixes = tuple(getattr(workspace, "write_prefixes", ()) or ()) + if not prefixes: + outputs_prefix = getattr(workspace, "outputs_prefix", None) + prefixes = (outputs_prefix or "outputs/",) + prefix = str(prefixes[0]).strip("/") + return f"/{prefix}/{RUNTIME_SKILLS_DIR}" if prefix else f"/{RUNTIME_SKILLS_DIR}" + + +def _seed_runtime_skills(backend: Any, ctx: RunContext[Any]) -> list[str]: + """Copy packaged DeepAgents skills into the invocation workspace. + + DeepAgents loads skills from its backend, while source-controlled + ``skills/`` folders live in the image. This bridge lets generated agents + ship reusable SKILL.md bundles without giving up durable A2A workspace + files. + """ + root = Path(__file__).parent / "skills" + if not root.exists(): + return [] + runtime_skills_root = _runtime_skills_root(ctx) + uploads: list[tuple[str, bytes]] = [] + for path in root.rglob("*"): + if path.is_file(): + rel = path.relative_to(root).as_posix() + uploads.append((runtime_skills_root + rel, path.read_bytes())) + if uploads: + backend.upload_files(uploads) + return [runtime_skills_root] + return [] + + +def _last_message_text(state: dict[str, Any]) -> str: + messages = state.get("messages") or [] + if not messages: + return json.dumps(state, default=str) + + content = getattr(messages[-1], "content", None) + if isinstance(content, str): + return content + if isinstance(content, list): + parts: list[str] = [] + for item in content: + if isinstance(item, dict): + text = item.get("text") or item.get("content") + if text: + parts.append(str(text)) + elif item: + parts.append(str(item)) + return "\n".join(parts) if parts else json.dumps(content, default=str) + return str(content or messages[-1]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9227d0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# a2a-pack is auto-installed by the deploy build. +# These starter deps power the DeepAgents tool-calling example in agent.py. +deepagents>=0.5.0 +langchain>=0.3 +langchain-openai>=0.2 +langgraph>=0.6