diff --git a/README.md b/README.md index c23ab25..b25d79b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The single `build(name, prompt)` skill: - `a2apack-agent-authoring` — how to shape `agent.py`, public `@skill` schemas, runtime metadata, pricing, resources, and auth - `deepagents-implementation-patterns` — concrete `create_deep_agent` - wiring for caller LLMs, skills, tools, and subagents + wiring for `ctx.llm` credentials, skills, tools, and subagents - `workspace-artifact-safety` — workspace grants, artifacts, bounded subprocesses, and scope expansion - `agent-quality-review` — pre-sandbox and pre-deploy checks for generated @@ -57,8 +57,9 @@ marketplace — the dashboard already surfaces these on the agent card. `$0.10 / call` during the current pricing model. The builder uses your selected LLM creds when present and falls back to a scoped platform grant. -The deployed agent is yours forever; subsequent invocations of your agent run on -their own pricing (whatever you set when generating it). +The deployed agent is yours forever; subsequent invocations of a generated LLM +agent default to scoped platform LLM grants unless you explicitly generate a +BYOK/caller-paid agent. ## How to call it diff --git a/agent.py b/agent.py index ee0d288..e1c7bfb 100644 --- a/agent.py +++ b/agent.py @@ -48,7 +48,7 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]): "Writes the project into the user's workspace, validates it in a " "sandbox, then ships it via the control plane." ) - version = "0.1.1" + version = "0.1.2" config_model = BuilderConfig auth_model = NoAuth @@ -278,4 +278,4 @@ def _find_url(value: Any) -> str | None: m = _URL_RE.search(value) return m.group(0) if m else None return None -# rebuild against a2a-pack 0.1.29 for negotiated write prefixes +# rebuild against a2a-pack 0.1.30 for generated platform LLM defaults diff --git a/agent_builder/builder.py b/agent_builder/builder.py index aceb627..271f391 100644 --- a/agent_builder/builder.py +++ b/agent_builder/builder.py @@ -29,8 +29,8 @@ class BuilderContext: grant_token: str | None = None # LLM creds forwarded by the orchestrator according to this agent's Card. # The builder accepts caller-selected creds and falls back to platform - # grants; generated agents usually use caller-provided creds. Falls back - # to settings for local development. + # grants. Hosted generated agents should usually use platform grants too. + # Falls back to settings for local development. llm_base_url: str | None = None llm_api_key: str | None = None llm_model: str | None = None @@ -47,17 +47,20 @@ user's workspace at ``agents//`` and then deploy it through the control plane. The default starter is the current a2a-pack DeepAgents scaffold. It declares -``LLMProvisioning.CALLER_PROVIDED``, reads caller LLM credentials from -``ctx.llm``, builds a ``ChatOpenAI`` model from those credentials, and wires a -small tool-calling DeepAgent with ``create_deep_agent`` plus -``wrap_model_call`` middleware. Do not recreate this from memory: call -``init_agent_template`` first and modify the generated files. +``LLMProvisioning.PLATFORM``, reads the scoped LLM grant from ``ctx.llm``, +builds a ``ChatOpenAI`` model from those credentials, and wires a small +tool-calling DeepAgent with ``create_deep_agent`` plus ``wrap_model_call`` +middleware. Do not recreate this from memory: call ``init_agent_template`` first +and modify the generated files. -Default generated/user agents to ``LLMProvisioning.CALLER_PROVIDED`` and -``caller_pays_llm=True`` unless the user explicitly wants platform-paid LLM -usage. ``LLMProvisioning.PLATFORM`` is allowed only through the A2A LiteLLM -grant path: the generated agent must still read ``ctx.llm`` and must never read -``A2A_LITELLM_KEY`` or any provider key directly. +Default hosted generated/user agents to ``LLMProvisioning.PLATFORM`` and +``caller_pays_llm=False`` so main-agent handoffs mint a scoped A2A LiteLLM grant +for the callee. The generated agent must still read ``ctx.llm`` and must never +read ``A2A_LITELLM_KEY``, ``OPENAI_API_KEY``, LiteLLM master keys, or provider +keys directly. If the user explicitly asks for BYOK or caller-paid inference, +use ``LLMProvisioning.CALLER_PROVIDED`` with ``caller_pays_llm=True`` and make +missing ``ctx.llm.api_key`` a clear setup/config result before constructing +``ChatOpenAI``. ``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is reserved for trusted platform/meta agents that should prefer caller-selected credentials and fall back to a scoped platform grant; do not use it for ordinary generated agents diff --git a/agent_builder/skills/a2apack-agent-authoring/SKILL.md b/agent_builder/skills/a2apack-agent-authoring/SKILL.md index 8c186b8..4f281e4 100644 --- a/agent_builder/skills/a2apack-agent-authoring/SKILL.md +++ b/agent_builder/skills/a2apack-agent-authoring/SKILL.md @@ -1,6 +1,6 @@ --- name: a2apack-agent-authoring -description: Author production a2a-pack agents with clear public @skill schemas, runtime declarations, caller-provided LLMs, workspace grants, pricing, resources, and secure platform capabilities. Use when writing or reviewing agent.py for an A2A agent. +description: Author production a2a-pack agents with clear public @skill schemas, runtime declarations, platform LLM grants, optional caller-provided LLMs, workspace grants, pricing, resources, and secure platform capabilities. Use when writing or reviewing agent.py for an A2A agent. --- # A2A Pack Agent Authoring @@ -21,13 +21,16 @@ Use `A2AAgent` class attributes for runtime and marketplace behavior: - `wants_cp_jwt=True` is only for trusted platform agents. It forwards the caller's control-plane token. -For generated/user-owned agents that call an LLM, default to -`LLMProvisioning.CALLER_PROVIDED` with `Pricing(..., caller_pays_llm=True, ...)`. -Use `LLMProvisioning.PLATFORM` only when the user explicitly wants -platform-paid LLM usage. Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` +For hosted generated/user-owned agents that call an LLM, default to +`LLMProvisioning.PLATFORM` with `Pricing(..., caller_pays_llm=False, ...)` so +main-agent handoffs mint a scoped A2A LiteLLM grant for the callee. Use +`LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or +caller-paid inference. Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` for trusted platform/meta agents that must work with either the caller's selected creds or a platform fallback grant. In every mode, read `ctx.llm`; -never read `A2A_LITELLM_KEY`, provider keys, or platform secrets directly. +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`. Keep each `@skill` method `async`, put `RunContext[...]` immediately after `self`, and annotate every public argument. Do not use `*args` or `**kwargs`; @@ -68,11 +71,11 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]): config_model = ResearchConfig auth_model = NoAuth - llm_provisioning = LLMProvisioning.CALLER_PROVIDED + llm_provisioning = LLMProvisioning.PLATFORM pricing = Pricing( price_per_call_usd=0.05, - caller_pays_llm=True, - notes="Caller supplies LLM credentials through the platform.", + caller_pays_llm=False, + notes="Uses a scoped platform LLM grant through ctx.llm.", ) resources = Resources(cpu="1", memory="1Gi", max_runtime_seconds=900) egress = EgressPolicy(allow_hosts=("api.openai.com",)) @@ -97,6 +100,12 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]): ) -> dict[str, Any]: creds = ctx.llm await ctx.emit_progress(f"researching with {creds.model}") + if not creds.api_key: + return { + "summary": "LLM credentials were not available for this run.", + "artifact": None, + "path": save_path, + } graph = self._build_graph(ctx) state = await graph.ainvoke( { diff --git a/agent_builder/skills/agent-quality-review/SKILL.md b/agent_builder/skills/agent-quality-review/SKILL.md index daafab6..c4150d4 100644 --- a/agent_builder/skills/agent-quality-review/SKILL.md +++ b/agent_builder/skills/agent-quality-review/SKILL.md @@ -33,11 +33,14 @@ Check these items: - The implementation reads `ctx.llm` for both `CALLER_PROVIDED` and `PLATFORM` LLM provisioning. `PLATFORM_OR_CALLER_PROVIDED` is acceptable only for trusted platform/meta agents and must also read `ctx.llm`. -- Generated/user agents should default to - `llm_provisioning = LLMProvisioning.CALLER_PROVIDED` and - `Pricing(..., caller_pays_llm=True, ...)`. If the user explicitly wants - platform-paid LLM usage, `LLMProvisioning.PLATFORM` is acceptable only if - the code still uses `ctx.llm` and never reads LiteLLM/provider keys directly. +- Hosted generated/user agents should default to + `llm_provisioning = LLMProvisioning.PLATFORM` and + `Pricing(..., caller_pays_llm=False, ...)`. Use + `LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or + caller-paid inference. In both modes, the code must use `ctx.llm` and never + read LiteLLM/provider keys directly. +- Any code path that builds `ChatOpenAI` checks `ctx.llm.api_key` first or + returns a clear setup/config result before the model constructor runs. - 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 fdfa06f..996617b 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 caller-provided LLM 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_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`, construct `ChatOpenAI`, and build a - DeepAgent with `create_deep_agent`. +2. Inside each method, read `ctx.llm`, verify credentials are available before + constructing `ChatOpenAI`, and build a DeepAgent with `create_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 diff --git a/agent_builder/skills/deepagents-implementation-patterns/SKILL.md b/agent_builder/skills/deepagents-implementation-patterns/SKILL.md index a076751..8580db2 100644 --- a/agent_builder/skills/deepagents-implementation-patterns/SKILL.md +++ b/agent_builder/skills/deepagents-implementation-patterns/SKILL.md @@ -1,12 +1,14 @@ --- name: deepagents-implementation-patterns -description: Implement DeepAgents inside a2a-pack agents with real deterministic tools, project skills, custom subagents, workspace-backed files, and caller-provided 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 ChatOpenAI models. Use when editing create_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: caller-provided `ChatOpenAI`, A2A workspace backend, project DeepAgents -skills, and a small number of exact tools. +is: `ctx.llm`-backed `ChatOpenAI`, 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. ## Implementation Shape @@ -32,6 +34,11 @@ stable output paths only when that is one of the grant write prefixes. def build_graph(ctx: Any) -> Any: creds = ctx.llm + if not creds.api_key: + raise RuntimeError( + "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, diff --git a/agent_builder/tools.py b/agent_builder/tools.py index 4bae704..3ef4da7 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: from .config import Settings -A2A_PACK_MIN_VERSION = "0.1.29" +A2A_PACK_MIN_VERSION = "0.1.30" @dataclass(frozen=True) diff --git a/requirements.txt b/requirements.txt index 8fa87b8..68177db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -a2a-pack>=0.1.29 +a2a-pack>=0.1.30 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 6aeb71e..cc7872f 100644 --- a/tests/test_builder_prompt.py +++ b/tests/test_builder_prompt.py @@ -53,6 +53,17 @@ class BuilderPromptTests(unittest.TestCase): self.assertIn(f"a2a-pack>={A2A_PACK_MIN_VERSION}", requirements) self.assertIn("a2a-pack>={A2A_PACK_MIN_VERSION}", tools_source) + def test_prompt_defaults_generated_agents_to_platform_llm_grants(self) -> None: + self.assertIn("LLMProvisioning.PLATFORM", SYSTEM_PROMPT) + self.assertIn("caller_pays_llm=False", SYSTEM_PROMPT) + self.assertIn("main-agent handoffs mint a scoped A2A LiteLLM grant", SYSTEM_PROMPT) + self.assertIn("ctx.llm.api_key", SYSTEM_PROMPT) + + files = _builder_skill_files() + self.assertIn("LLMProvisioning.PLATFORM", files["a2apack-agent-authoring/SKILL.md"]) + self.assertIn("caller_pays_llm=False", files["a2apack-agent-authoring/SKILL.md"]) + self.assertIn("ctx.llm.api_key", files["agent-quality-review/SKILL.md"]) + def test_prompt_requires_resource_mirror_for_heavy_agents(self) -> None: self.assertIn("declare resources in BOTH places", SYSTEM_PROMPT) self.assertIn("resources = Resources", SYSTEM_PROMPT) diff --git a/tests/test_template_init.py b/tests/test_template_init.py index f0da38f..bf7ae82 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -31,7 +31,7 @@ class TemplateInitTests(unittest.TestCase): self.assertEqual(set(files), {"agent.py", "a2a.yaml", "requirements.txt"}) self.assertIn('name = "research-agent"', files["agent.py"]) self.assertIn('description = "Research helper"', files["agent.py"]) - self.assertIn("LLMProvisioning.CALLER_PROVIDED", files["agent.py"]) + self.assertIn("LLMProvisioning.PLATFORM", files["agent.py"]) self.assertIn("WorkspaceAccess.dynamic", files["agent.py"]) self.assertIn("RUNTIME_SKILLS_DIR", files["agent.py"]) self.assertIn("_runtime_skills_root", files["agent.py"])