fix: generate agents with platform LLM grants
All checks were successful
build / build (push) Successful in 21s
All checks were successful
build / build (push) Successful in 21s
This commit is contained in:
@@ -14,7 +14,7 @@ The single `build(name, prompt)` skill:
|
|||||||
- `a2apack-agent-authoring` — how to shape `agent.py`, public `@skill`
|
- `a2apack-agent-authoring` — how to shape `agent.py`, public `@skill`
|
||||||
schemas, runtime metadata, pricing, resources, and auth
|
schemas, runtime metadata, pricing, resources, and auth
|
||||||
- `deepagents-implementation-patterns` — concrete `create_deep_agent`
|
- `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
|
- `workspace-artifact-safety` — workspace grants, artifacts, bounded
|
||||||
subprocesses, and scope expansion
|
subprocesses, and scope expansion
|
||||||
- `agent-quality-review` — pre-sandbox and pre-deploy checks for generated
|
- `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
|
`$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.
|
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
|
The deployed agent is yours forever; subsequent invocations of a generated LLM
|
||||||
their own pricing (whatever you set when generating it).
|
agent default to scoped platform LLM grants unless you explicitly generate a
|
||||||
|
BYOK/caller-paid agent.
|
||||||
|
|
||||||
## How to call it
|
## How to call it
|
||||||
|
|
||||||
|
|||||||
4
agent.py
4
agent.py
@@ -48,7 +48,7 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]):
|
|||||||
"Writes the project into the user's workspace, validates it in a "
|
"Writes the project into the user's workspace, validates it in a "
|
||||||
"sandbox, then ships it via the control plane."
|
"sandbox, then ships it via the control plane."
|
||||||
)
|
)
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
|
|
||||||
config_model = BuilderConfig
|
config_model = BuilderConfig
|
||||||
auth_model = NoAuth
|
auth_model = NoAuth
|
||||||
@@ -278,4 +278,4 @@ def _find_url(value: Any) -> str | None:
|
|||||||
m = _URL_RE.search(value)
|
m = _URL_RE.search(value)
|
||||||
return m.group(0) if m else None
|
return m.group(0) if m else None
|
||||||
return 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
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class BuilderContext:
|
|||||||
grant_token: str | None = None
|
grant_token: str | None = None
|
||||||
# LLM creds forwarded by the orchestrator according to this agent's Card.
|
# LLM creds forwarded by the orchestrator according to this agent's Card.
|
||||||
# The builder accepts caller-selected creds and falls back to platform
|
# The builder accepts caller-selected creds and falls back to platform
|
||||||
# grants; generated agents usually use caller-provided creds. Falls back
|
# grants. Hosted generated agents should usually use platform grants too.
|
||||||
# to settings for local development.
|
# Falls back to settings for local development.
|
||||||
llm_base_url: str | None = None
|
llm_base_url: str | None = None
|
||||||
llm_api_key: str | None = None
|
llm_api_key: str | None = None
|
||||||
llm_model: str | None = None
|
llm_model: str | None = None
|
||||||
@@ -47,17 +47,20 @@ user's workspace at ``agents/<name>/`` and then deploy it through the
|
|||||||
control plane.
|
control plane.
|
||||||
|
|
||||||
The default starter is the current a2a-pack DeepAgents scaffold. It declares
|
The default starter is the current a2a-pack DeepAgents scaffold. It declares
|
||||||
``LLMProvisioning.CALLER_PROVIDED``, reads caller LLM credentials from
|
``LLMProvisioning.PLATFORM``, reads the scoped LLM grant from ``ctx.llm``,
|
||||||
``ctx.llm``, builds a ``ChatOpenAI`` model from those credentials, and wires a
|
builds a ``ChatOpenAI`` model from those credentials, and wires a small
|
||||||
small tool-calling DeepAgent with ``create_deep_agent`` plus
|
tool-calling DeepAgent with ``create_deep_agent`` plus ``wrap_model_call``
|
||||||
``wrap_model_call`` middleware. Do not recreate this from memory: call
|
middleware. Do not recreate this from memory: call ``init_agent_template`` first
|
||||||
``init_agent_template`` first and modify the generated files.
|
and modify the generated files.
|
||||||
|
|
||||||
Default generated/user agents to ``LLMProvisioning.CALLER_PROVIDED`` and
|
Default hosted generated/user agents to ``LLMProvisioning.PLATFORM`` and
|
||||||
``caller_pays_llm=True`` unless the user explicitly wants platform-paid LLM
|
``caller_pays_llm=False`` so main-agent handoffs mint a scoped A2A LiteLLM grant
|
||||||
usage. ``LLMProvisioning.PLATFORM`` is allowed only through the A2A LiteLLM
|
for the callee. The generated agent must still read ``ctx.llm`` and must never
|
||||||
grant path: the generated agent must still read ``ctx.llm`` and must never read
|
read ``A2A_LITELLM_KEY``, ``OPENAI_API_KEY``, LiteLLM master keys, or provider
|
||||||
``A2A_LITELLM_KEY`` or any provider key directly.
|
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
|
``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is reserved for trusted
|
||||||
platform/meta agents that should prefer caller-selected credentials and fall
|
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
|
back to a scoped platform grant; do not use it for ordinary generated agents
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: a2apack-agent-authoring
|
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
|
# 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
|
- `wants_cp_jwt=True` is only for trusted platform agents. It forwards the
|
||||||
caller's control-plane token.
|
caller's control-plane token.
|
||||||
|
|
||||||
For generated/user-owned agents that call an LLM, default to
|
For hosted generated/user-owned agents that call an LLM, default to
|
||||||
`LLMProvisioning.CALLER_PROVIDED` with `Pricing(..., caller_pays_llm=True, ...)`.
|
`LLMProvisioning.PLATFORM` with `Pricing(..., caller_pays_llm=False, ...)` so
|
||||||
Use `LLMProvisioning.PLATFORM` only when the user explicitly wants
|
main-agent handoffs mint a scoped A2A LiteLLM grant for the callee. Use
|
||||||
platform-paid LLM usage. Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`
|
`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
|
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`;
|
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
|
Keep each `@skill` method `async`, put `RunContext[...]` immediately after
|
||||||
`self`, and annotate every public argument. Do not use `*args` or `**kwargs`;
|
`self`, and annotate every public argument. Do not use `*args` or `**kwargs`;
|
||||||
@@ -68,11 +71,11 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]):
|
|||||||
config_model = ResearchConfig
|
config_model = ResearchConfig
|
||||||
auth_model = NoAuth
|
auth_model = NoAuth
|
||||||
|
|
||||||
llm_provisioning = LLMProvisioning.CALLER_PROVIDED
|
llm_provisioning = LLMProvisioning.PLATFORM
|
||||||
pricing = Pricing(
|
pricing = Pricing(
|
||||||
price_per_call_usd=0.05,
|
price_per_call_usd=0.05,
|
||||||
caller_pays_llm=True,
|
caller_pays_llm=False,
|
||||||
notes="Caller supplies LLM credentials through the platform.",
|
notes="Uses a scoped platform LLM grant through ctx.llm.",
|
||||||
)
|
)
|
||||||
resources = Resources(cpu="1", memory="1Gi", max_runtime_seconds=900)
|
resources = Resources(cpu="1", memory="1Gi", max_runtime_seconds=900)
|
||||||
egress = EgressPolicy(allow_hosts=("api.openai.com",))
|
egress = EgressPolicy(allow_hosts=("api.openai.com",))
|
||||||
@@ -97,6 +100,12 @@ class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]):
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
creds = ctx.llm
|
creds = ctx.llm
|
||||||
await ctx.emit_progress(f"researching with {creds.model}")
|
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)
|
graph = self._build_graph(ctx)
|
||||||
state = await graph.ainvoke(
|
state = await graph.ainvoke(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,11 +33,14 @@ Check these items:
|
|||||||
- The implementation reads `ctx.llm` for both `CALLER_PROVIDED` and
|
- The implementation reads `ctx.llm` for both `CALLER_PROVIDED` and
|
||||||
`PLATFORM` LLM provisioning. `PLATFORM_OR_CALLER_PROVIDED` is acceptable
|
`PLATFORM` LLM provisioning. `PLATFORM_OR_CALLER_PROVIDED` is acceptable
|
||||||
only for trusted platform/meta agents and must also read `ctx.llm`.
|
only for trusted platform/meta agents and must also read `ctx.llm`.
|
||||||
- Generated/user agents should default to
|
- Hosted generated/user agents should default to
|
||||||
`llm_provisioning = LLMProvisioning.CALLER_PROVIDED` and
|
`llm_provisioning = LLMProvisioning.PLATFORM` and
|
||||||
`Pricing(..., caller_pays_llm=True, ...)`. If the user explicitly wants
|
`Pricing(..., caller_pays_llm=False, ...)`. Use
|
||||||
platform-paid LLM usage, `LLMProvisioning.PLATFORM` is acceptable only if
|
`LLMProvisioning.CALLER_PROVIDED` only when the user explicitly wants BYOK or
|
||||||
the code still uses `ctx.llm` and never reads LiteLLM/provider keys directly.
|
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 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 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
|
# 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`, construct `ChatOpenAI`, and build a
|
2. Inside each method, read `ctx.llm`, verify credentials are available before
|
||||||
DeepAgent with `create_deep_agent`.
|
constructing `ChatOpenAI`, and build a DeepAgent with `create_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
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
---
|
---
|
||||||
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 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 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: caller-provided `ChatOpenAI`, A2A workspace backend, project DeepAgents
|
is: `ctx.llm`-backed `ChatOpenAI`, A2A workspace backend, project DeepAgents
|
||||||
skills, and a small number of exact tools.
|
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
|
## 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:
|
def build_graph(ctx: Any) -> Any:
|
||||||
creds = ctx.llm
|
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 = ChatOpenAI(
|
||||||
model=creds.model,
|
model=creds.model,
|
||||||
base_url=creds.base_url,
|
base_url=creds.base_url,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
|
|||||||
from .config import Settings
|
from .config import Settings
|
||||||
|
|
||||||
|
|
||||||
A2A_PACK_MIN_VERSION = "0.1.29"
|
A2A_PACK_MIN_VERSION = "0.1.30"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
a2a-pack>=0.1.29
|
a2a-pack>=0.1.30
|
||||||
httpx>=0.27
|
httpx>=0.27
|
||||||
boto3>=1.34
|
boto3>=1.34
|
||||||
deepagents>=0.5.0
|
deepagents>=0.5.0
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ class BuilderPromptTests(unittest.TestCase):
|
|||||||
self.assertIn(f"a2a-pack>={A2A_PACK_MIN_VERSION}", requirements)
|
self.assertIn(f"a2a-pack>={A2A_PACK_MIN_VERSION}", requirements)
|
||||||
self.assertIn("a2a-pack>={A2A_PACK_MIN_VERSION}", tools_source)
|
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:
|
def test_prompt_requires_resource_mirror_for_heavy_agents(self) -> None:
|
||||||
self.assertIn("declare resources in BOTH places", SYSTEM_PROMPT)
|
self.assertIn("declare resources in BOTH places", SYSTEM_PROMPT)
|
||||||
self.assertIn("resources = Resources", SYSTEM_PROMPT)
|
self.assertIn("resources = Resources", SYSTEM_PROMPT)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class TemplateInitTests(unittest.TestCase):
|
|||||||
self.assertEqual(set(files), {"agent.py", "a2a.yaml", "requirements.txt"})
|
self.assertEqual(set(files), {"agent.py", "a2a.yaml", "requirements.txt"})
|
||||||
self.assertIn('name = "research-agent"', files["agent.py"])
|
self.assertIn('name = "research-agent"', files["agent.py"])
|
||||||
self.assertIn('description = "Research helper"', 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("WorkspaceAccess.dynamic", files["agent.py"])
|
||||||
self.assertIn("RUNTIME_SKILLS_DIR", files["agent.py"])
|
self.assertIn("RUNTIME_SKILLS_DIR", files["agent.py"])
|
||||||
self.assertIn("_runtime_skills_root", files["agent.py"])
|
self.assertIn("_runtime_skills_root", files["agent.py"])
|
||||||
|
|||||||
Reference in New Issue
Block a user