a2a-pack 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. a2a_pack-0.1.0/.gitignore +10 -0
  2. a2a_pack-0.1.0/LICENSE +21 -0
  3. a2a_pack-0.1.0/PKG-INFO +143 -0
  4. a2a_pack-0.1.0/README.md +81 -0
  5. a2a_pack-0.1.0/a2a_pack/__init__.py +144 -0
  6. a2a_pack-0.1.0/a2a_pack/a2a_client.py +170 -0
  7. a2a_pack-0.1.0/a2a_pack/agent.py +443 -0
  8. a2a_pack-0.1.0/a2a_pack/auth.py +35 -0
  9. a2a_pack-0.1.0/a2a_pack/card.py +88 -0
  10. a2a_pack-0.1.0/a2a_pack/cli/__init__.py +1 -0
  11. a2a_pack-0.1.0/a2a_pack/cli/api_client.py +129 -0
  12. a2a_pack-0.1.0/a2a_pack/cli/credentials.py +68 -0
  13. a2a_pack-0.1.0/a2a_pack/cli/loader.py +34 -0
  14. a2a_pack-0.1.0/a2a_pack/cli/main.py +478 -0
  15. a2a_pack-0.1.0/a2a_pack/cli/manifests.py +133 -0
  16. a2a_pack-0.1.0/a2a_pack/cli/templates/Dockerfile.tmpl +14 -0
  17. a2a_pack-0.1.0/a2a_pack/cli/templates/a2a.yaml.tmpl +8 -0
  18. a2a_pack-0.1.0/a2a_pack/cli/templates/agent.py.tmpl +24 -0
  19. a2a_pack-0.1.0/a2a_pack/cli/templates/deployment.yaml.tmpl +77 -0
  20. a2a_pack-0.1.0/a2a_pack/cli/templates/dockerignore.tmpl +7 -0
  21. a2a_pack-0.1.0/a2a_pack/cli/templates/requirements.txt.tmpl +1 -0
  22. a2a_pack-0.1.0/a2a_pack/cli/templates/workflow.yml.tmpl +35 -0
  23. a2a_pack-0.1.0/a2a_pack/context.py +521 -0
  24. a2a_pack-0.1.0/a2a_pack/discovery.py +176 -0
  25. a2a_pack-0.1.0/a2a_pack/grants.py +148 -0
  26. a2a_pack-0.1.0/a2a_pack/mcp/__init__.py +28 -0
  27. a2a_pack-0.1.0/a2a_pack/mcp/http.py +69 -0
  28. a2a_pack-0.1.0/a2a_pack/mcp/server.py +241 -0
  29. a2a_pack-0.1.0/a2a_pack/runtime.py +134 -0
  30. a2a_pack-0.1.0/a2a_pack/sandbox.py +174 -0
  31. a2a_pack-0.1.0/a2a_pack/serve/__init__.py +3 -0
  32. a2a_pack-0.1.0/a2a_pack/serve/asgi.py +312 -0
  33. a2a_pack-0.1.0/a2a_pack/workspace.py +528 -0
  34. a2a_pack-0.1.0/pyproject.toml +90 -0
@@ -0,0 +1,10 @@
1
+ __pycache__
2
+ *.pyc
3
+ .venv
4
+ .pytest_cache
5
+ *.egg-info
6
+ dist
7
+ build
8
+ node_modules
9
+
10
+ .env
a2a_pack-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 a2a cloud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: a2a-pack
3
+ Version: 0.1.0
4
+ Summary: Developer SDK + CLI for building, packaging, and deploying A2A agents.
5
+ Project-URL: Homepage, https://a2acloud.io
6
+ Project-URL: Documentation, https://docs.a2acloud.io
7
+ Project-URL: Repository, https://gitea.a2acloud.io/gitea_admin/a2a-pack
8
+ Project-URL: Issues, https://gitea.a2acloud.io/gitea_admin/a2a-pack/issues
9
+ Author-email: a2a cloud <hello@a2acloud.io>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 a2a cloud
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: a2a,agent,agents,ai,llm,marketplace,mcp,microvm,model-context-protocol,sandbox
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Environment :: Web Environment
35
+ Classifier: Framework :: FastAPI
36
+ Classifier: Intended Audience :: Developers
37
+ Classifier: License :: OSI Approved :: MIT License
38
+ Classifier: Operating System :: OS Independent
39
+ Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3 :: Only
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
44
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
45
+ Classifier: Typing :: Typed
46
+ Requires-Python: >=3.11
47
+ Requires-Dist: fastapi>=0.110
48
+ Requires-Dist: httpx>=0.27
49
+ Requires-Dist: jinja2>=3
50
+ Requires-Dist: pydantic>=2.6
51
+ Requires-Dist: pyyaml>=6
52
+ Requires-Dist: rich>=13
53
+ Requires-Dist: typer>=0.12
54
+ Requires-Dist: uvicorn[standard]>=0.27
55
+ Provides-Extra: dev
56
+ Requires-Dist: build>=1.2; extra == 'dev'
57
+ Requires-Dist: httpx>=0.27; extra == 'dev'
58
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
59
+ Requires-Dist: pytest>=8; extra == 'dev'
60
+ Requires-Dist: twine>=5; extra == 'dev'
61
+ Description-Content-Type: text/markdown
62
+
63
+ # a2a-pack
64
+
65
+ **Developer SDK + CLI for building, packaging, and deploying [A2A](https://a2acloud.io) agents.**
66
+
67
+ One Python class becomes a sandboxed, discoverable, MCP-compatible AI
68
+ agent on the [a2a cloud](https://a2acloud.io) platform. Other agents
69
+ reach yours via HMAC-signed grants. The platform owns deployment,
70
+ execution, permissions, and (when you're ready) billing.
71
+
72
+ ```bash
73
+ pip install a2a-pack
74
+ a2a signup --email you@example.com --password ...
75
+ a2a init research-agent
76
+ cd research-agent
77
+ a2a deploy
78
+ # → https://research-agent.a2acloud.io (TLS, MCP, OpenAPI, all wired)
79
+ ```
80
+
81
+ ## What an agent looks like
82
+
83
+ ```python
84
+ from pydantic import BaseModel
85
+ from a2a_pack import (
86
+ A2AAgent, LLMProvisioning, NoAuth, Pricing, RunContext, skill,
87
+ )
88
+
89
+
90
+ class GreeterConfig(BaseModel):
91
+ suffix: str = "!"
92
+
93
+
94
+ class Greeter(A2AAgent[GreeterConfig, NoAuth]):
95
+ name = "greeter"
96
+ description = "Say hi."
97
+ version = "0.1.0"
98
+ config_model = GreeterConfig
99
+ auth_model = NoAuth
100
+
101
+ # Use the caller's own LLM key (forwarded by the platform) — the
102
+ # author's price stays small; the LLM bill goes to the caller's
103
+ # provider directly.
104
+ llm_provisioning = LLMProvisioning.CALLER_PROVIDED
105
+ pricing = Pricing(price_per_call_usd=0.01, caller_pays_llm=True)
106
+
107
+ @skill(description="Greet someone.")
108
+ async def greet(self, ctx: RunContext[NoAuth], who: str) -> str:
109
+ await ctx.emit_progress(f"greeting {who}")
110
+ return f"hello {who}{self.config.suffix}"
111
+ ```
112
+
113
+ That's it. `a2a deploy` packages the source, the control plane builds
114
+ the image, ArgoCD reconciles, you get a public URL.
115
+
116
+ ## Public surface
117
+
118
+ | Concept | Where |
119
+ |---|---|
120
+ | `A2AAgent` base class + `@skill` decorator | `a2a_pack.agent` |
121
+ | `RunContext`, `ctx.llm`, `ctx.ask`, `ctx.request_scope` | `a2a_pack.context` |
122
+ | Grant mint/verify (HMAC, audience-bound, glob-filtered, time-limited) | `a2a_pack.grants` |
123
+ | Workspace negotiation surface | `a2a_pack.workspace` |
124
+ | Sandbox client (microVM via libkrun) | `a2a_pack.sandbox` |
125
+ | Agent-to-agent client (HTTP, in-memory, custom) | `a2a_pack.a2a_client` |
126
+ | MCP server (skills → tools, mountable into your FastAPI app) | `a2a_pack.mcp` |
127
+ | Lifecycle / Resources / Pricing / LLMProvisioning declarations | `a2a_pack.runtime` |
128
+ | Card schema (auto-derived from your class) | `a2a_pack.card` |
129
+
130
+ Full reference + auto-generated docs at **https://docs.a2acloud.io**.
131
+
132
+ ## Self-hosting
133
+
134
+ The platform pieces (control plane, sandbox runtime, gitea, ArgoCD,
135
+ MinIO, LiteLLM) live at
136
+ [gitea.a2acloud.io](https://gitea.a2acloud.io) — the SDK is the only
137
+ piece you need on PyPI. If you want to run the whole stack locally
138
+ or in your own cluster, the bootstrap recipe is in the platform
139
+ [README](https://gitea.a2acloud.io/gitea_admin/a2a-pack).
140
+
141
+ ## License
142
+
143
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,81 @@
1
+ # a2a-pack
2
+
3
+ **Developer SDK + CLI for building, packaging, and deploying [A2A](https://a2acloud.io) agents.**
4
+
5
+ One Python class becomes a sandboxed, discoverable, MCP-compatible AI
6
+ agent on the [a2a cloud](https://a2acloud.io) platform. Other agents
7
+ reach yours via HMAC-signed grants. The platform owns deployment,
8
+ execution, permissions, and (when you're ready) billing.
9
+
10
+ ```bash
11
+ pip install a2a-pack
12
+ a2a signup --email you@example.com --password ...
13
+ a2a init research-agent
14
+ cd research-agent
15
+ a2a deploy
16
+ # → https://research-agent.a2acloud.io (TLS, MCP, OpenAPI, all wired)
17
+ ```
18
+
19
+ ## What an agent looks like
20
+
21
+ ```python
22
+ from pydantic import BaseModel
23
+ from a2a_pack import (
24
+ A2AAgent, LLMProvisioning, NoAuth, Pricing, RunContext, skill,
25
+ )
26
+
27
+
28
+ class GreeterConfig(BaseModel):
29
+ suffix: str = "!"
30
+
31
+
32
+ class Greeter(A2AAgent[GreeterConfig, NoAuth]):
33
+ name = "greeter"
34
+ description = "Say hi."
35
+ version = "0.1.0"
36
+ config_model = GreeterConfig
37
+ auth_model = NoAuth
38
+
39
+ # Use the caller's own LLM key (forwarded by the platform) — the
40
+ # author's price stays small; the LLM bill goes to the caller's
41
+ # provider directly.
42
+ llm_provisioning = LLMProvisioning.CALLER_PROVIDED
43
+ pricing = Pricing(price_per_call_usd=0.01, caller_pays_llm=True)
44
+
45
+ @skill(description="Greet someone.")
46
+ async def greet(self, ctx: RunContext[NoAuth], who: str) -> str:
47
+ await ctx.emit_progress(f"greeting {who}")
48
+ return f"hello {who}{self.config.suffix}"
49
+ ```
50
+
51
+ That's it. `a2a deploy` packages the source, the control plane builds
52
+ the image, ArgoCD reconciles, you get a public URL.
53
+
54
+ ## Public surface
55
+
56
+ | Concept | Where |
57
+ |---|---|
58
+ | `A2AAgent` base class + `@skill` decorator | `a2a_pack.agent` |
59
+ | `RunContext`, `ctx.llm`, `ctx.ask`, `ctx.request_scope` | `a2a_pack.context` |
60
+ | Grant mint/verify (HMAC, audience-bound, glob-filtered, time-limited) | `a2a_pack.grants` |
61
+ | Workspace negotiation surface | `a2a_pack.workspace` |
62
+ | Sandbox client (microVM via libkrun) | `a2a_pack.sandbox` |
63
+ | Agent-to-agent client (HTTP, in-memory, custom) | `a2a_pack.a2a_client` |
64
+ | MCP server (skills → tools, mountable into your FastAPI app) | `a2a_pack.mcp` |
65
+ | Lifecycle / Resources / Pricing / LLMProvisioning declarations | `a2a_pack.runtime` |
66
+ | Card schema (auto-derived from your class) | `a2a_pack.card` |
67
+
68
+ Full reference + auto-generated docs at **https://docs.a2acloud.io**.
69
+
70
+ ## Self-hosting
71
+
72
+ The platform pieces (control plane, sandbox runtime, gitea, ArgoCD,
73
+ MinIO, LiteLLM) live at
74
+ [gitea.a2acloud.io](https://gitea.a2acloud.io) — the SDK is the only
75
+ piece you need on PyPI. If you want to run the whole stack locally
76
+ or in your own cluster, the bootstrap recipe is in the platform
77
+ [README](https://gitea.a2acloud.io/gitea_admin/a2a-pack).
78
+
79
+ ## License
80
+
81
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,144 @@
1
+ """a2a-pack — developer SDK + CLI for the a2a cloud platform.
2
+
3
+ See https://docs.a2acloud.io for the full reference.
4
+ """
5
+
6
+ # Single source of truth — pyproject.toml reads this via hatch.version.
7
+ __version__ = "0.1.0"
8
+
9
+ from .a2a_client import (
10
+ A2AClient,
11
+ CallResult,
12
+ HttpA2AClient,
13
+ InMemoryA2AClient,
14
+ )
15
+ from .agent import (
16
+ A2AAgent,
17
+ ParamSpec,
18
+ SkillInputError,
19
+ SkillInvocationError,
20
+ SkillNotFound,
21
+ SkillSpec,
22
+ skill,
23
+ )
24
+ from .discovery import (
25
+ ControlPlaneDiscovery,
26
+ DiscoveredAgent,
27
+ DiscoveryClient,
28
+ InMemoryDiscovery,
29
+ )
30
+ from .grants import Grant, GrantInvalid, mint_grant, sign_grant, verify_grant
31
+ from .mcp import (
32
+ MCP_PROTOCOL_VERSION,
33
+ MCPServer,
34
+ build_http_app as build_mcp_http_app,
35
+ mount_http as mount_mcp_http,
36
+ skills_to_tools,
37
+ )
38
+ from .auth import APIKeyAuth, JWTAuth, NoAuth
39
+ from .card import AgentCard, SkillCard
40
+ from .context import (
41
+ AgentEvent,
42
+ ArtifactRef,
43
+ CancelledByCaller,
44
+ LLMCreds,
45
+ LocalRunContext,
46
+ MissingScopes,
47
+ RunContext,
48
+ )
49
+ from .runtime import (
50
+ AgentRuntime,
51
+ EgressPolicy,
52
+ Lifecycle,
53
+ LLMProvisioning,
54
+ Pricing,
55
+ Resources,
56
+ Sandbox,
57
+ SkillPolicy,
58
+ State,
59
+ )
60
+ from .sandbox import (
61
+ ExecResult,
62
+ SandboxClient,
63
+ SandboxHandle,
64
+ SandboxSpec,
65
+ SandboxUnavailable,
66
+ )
67
+ from .workspace import (
68
+ FileMatch,
69
+ FileType,
70
+ LocalWorkspaceClient,
71
+ LocalWorkspaceView,
72
+ WorkspaceAccess,
73
+ WorkspaceClient,
74
+ WorkspaceDenied,
75
+ WorkspaceGrant,
76
+ WorkspaceMode,
77
+ WorkspacePatch,
78
+ WorkspaceView,
79
+ )
80
+
81
+ __all__ = [
82
+ "A2AAgent",
83
+ "A2AClient",
84
+ "APIKeyAuth",
85
+ "AgentCard",
86
+ "AgentEvent",
87
+ "AgentRuntime",
88
+ "ArtifactRef",
89
+ "CallResult",
90
+ "CancelledByCaller",
91
+ "ControlPlaneDiscovery",
92
+ "DiscoveredAgent",
93
+ "DiscoveryClient",
94
+ "EgressPolicy",
95
+ "ExecResult",
96
+ "FileMatch",
97
+ "FileType",
98
+ "Grant",
99
+ "GrantInvalid",
100
+ "HttpA2AClient",
101
+ "InMemoryA2AClient",
102
+ "InMemoryDiscovery",
103
+ "JWTAuth",
104
+ "LLMCreds",
105
+ "LLMProvisioning",
106
+ "Lifecycle",
107
+ "LocalRunContext",
108
+ "MCP_PROTOCOL_VERSION",
109
+ "MCPServer",
110
+ "build_mcp_http_app",
111
+ "mount_mcp_http",
112
+ "skills_to_tools",
113
+ "LocalWorkspaceClient",
114
+ "LocalWorkspaceView",
115
+ "MissingScopes",
116
+ "NoAuth",
117
+ "ParamSpec",
118
+ "Pricing",
119
+ "Resources",
120
+ "RunContext",
121
+ "Sandbox",
122
+ "mint_grant",
123
+ "sign_grant",
124
+ "verify_grant",
125
+ "SandboxClient",
126
+ "SandboxHandle",
127
+ "SandboxSpec",
128
+ "SandboxUnavailable",
129
+ "SkillCard",
130
+ "SkillInputError",
131
+ "SkillInvocationError",
132
+ "SkillNotFound",
133
+ "SkillPolicy",
134
+ "SkillSpec",
135
+ "State",
136
+ "WorkspaceAccess",
137
+ "WorkspaceClient",
138
+ "WorkspaceDenied",
139
+ "WorkspaceGrant",
140
+ "WorkspaceMode",
141
+ "WorkspacePatch",
142
+ "WorkspaceView",
143
+ "skill",
144
+ ]
@@ -0,0 +1,170 @@
1
+ """Agent-to-agent invocation surface available via ``ctx.call(...)``.
2
+
3
+ An agent never speaks raw HTTP to another agent. It calls
4
+ ``ctx.call(target, skill, args, grant=...)`` and the runtime-attached
5
+ :class:`A2AClient` handles transport: HTTP for cross-pod, in-memory for
6
+ local tests, anything else (gRPC, message bus) for future runtimes.
7
+
8
+ The grant token (see :mod:`a2a_pack.grants`) is the *only* way to hand
9
+ workspace access across agents. Callee-side runtime validates it before
10
+ materializing a :class:`WorkspaceClient`.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass, field
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from .agent import A2AAgent
20
+ from .context import RunContext
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class CallResult:
25
+ """What an A2A invocation returns to the calling skill."""
26
+
27
+ result: Any
28
+ events: tuple[dict[str, Any], ...] = ()
29
+ artifacts: tuple[dict[str, Any], ...] = ()
30
+ grant_id: str | None = None # echoed for audit
31
+
32
+
33
+ class A2AClient(ABC):
34
+ """Transport-shaped agent-to-agent client."""
35
+
36
+ @abstractmethod
37
+ async def call(
38
+ self,
39
+ target: str,
40
+ skill: str,
41
+ *,
42
+ args: dict[str, Any] | None = None,
43
+ grant: str | None = None,
44
+ timeout: float | None = None,
45
+ ) -> CallResult:
46
+ """Invoke ``skill`` on ``target`` and return its :class:`CallResult`.
47
+
48
+ ``target`` is opaque to this layer — for the HTTP impl it's an agent
49
+ URL; for the in-memory impl it's an agent name.
50
+ """
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # In-memory: routes calls to A2AAgent instances in the same process. Useful
55
+ # for the demo + tests.
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ @dataclass
60
+ class InMemoryA2AClient(A2AClient):
61
+ """Routes calls to agent instances registered by name.
62
+
63
+ The receiving agent gets a *new* :class:`RunContext` built by the
64
+ ``ctx_factory`` callable, so caller and callee don't share state.
65
+ Pass ``ctx_factory=lambda agent, grant: ...`` to control how scoped
66
+ workspaces / sandboxes are wired in.
67
+ """
68
+
69
+ agents: dict[str, "A2AAgent"]
70
+ ctx_factory: Any = None # Callable[[A2AAgent, str | None], RunContext]
71
+
72
+ async def call(
73
+ self,
74
+ target: str,
75
+ skill: str,
76
+ *,
77
+ args: dict[str, Any] | None = None,
78
+ grant: str | None = None,
79
+ timeout: float | None = None,
80
+ ) -> CallResult:
81
+ if target not in self.agents:
82
+ raise KeyError(f"no agent registered: {target!r}")
83
+ agent = self.agents[target]
84
+ ctx = self.ctx_factory(agent, grant) if self.ctx_factory else None
85
+ if ctx is None:
86
+ from .context import LocalRunContext
87
+ from .auth import NoAuth
88
+
89
+ ctx = LocalRunContext(auth=NoAuth(), task_id=f"a2a-{target}")
90
+ result = await agent.invoke_json(skill, ctx, args or {})
91
+ events = tuple(
92
+ {"kind": e.kind, "payload": e.payload}
93
+ for e in getattr(ctx, "events", ())
94
+ )
95
+ # surface artifacts captured by LocalRunContext, if present
96
+ artifacts: tuple[dict[str, Any], ...] = ()
97
+ local_arts = getattr(ctx, "artifacts", None)
98
+ if isinstance(local_arts, dict):
99
+ artifacts = tuple(
100
+ {"name": name, "size_bytes": len(data)}
101
+ for name, data in local_arts.items()
102
+ )
103
+ return CallResult(
104
+ result=result,
105
+ events=events,
106
+ artifacts=artifacts,
107
+ grant_id=_grant_id_or_none(grant),
108
+ )
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # HTTP: posts to <target>/invoke/<skill> with {arguments, grant} body.
113
+ # ---------------------------------------------------------------------------
114
+
115
+
116
+ @dataclass
117
+ class HttpA2AClient(A2AClient):
118
+ """A2A client that POSTs to the standard /invoke/{skill} endpoint."""
119
+
120
+ default_timeout: float = 60.0
121
+
122
+ async def call(
123
+ self,
124
+ target: str,
125
+ skill: str,
126
+ *,
127
+ args: dict[str, Any] | None = None,
128
+ grant: str | None = None,
129
+ timeout: float | None = None,
130
+ ) -> CallResult:
131
+ import httpx # late import: server-side needs no client
132
+
133
+ body: dict[str, Any] = {"arguments": args or {}}
134
+ if grant is not None:
135
+ body["grant"] = grant
136
+ url = f"{target.rstrip('/')}/invoke/{skill}"
137
+ async with httpx.AsyncClient(timeout=timeout or self.default_timeout) as c:
138
+ resp = await c.post(url, json=body)
139
+ if resp.status_code >= 400:
140
+ raise RuntimeError(f"a2a {url} -> {resp.status_code}: {resp.text}")
141
+ data = resp.json()
142
+ return CallResult(
143
+ result=data.get("result"),
144
+ events=tuple(data.get("events") or ()),
145
+ artifacts=tuple(data.get("artifacts") or ()),
146
+ grant_id=_grant_id_or_none(grant),
147
+ )
148
+
149
+
150
+ def _grant_id_or_none(grant: str | None) -> str | None:
151
+ """Extract grant_id without re-validating the signature (audit only)."""
152
+ if not grant or "." not in grant:
153
+ return None
154
+ try:
155
+ from .grants import _b64decode
156
+
157
+ payload = _b64decode(grant.rsplit(".", 1)[0])
158
+ import json
159
+
160
+ return json.loads(payload).get("grant_id")
161
+ except Exception: # noqa: BLE001
162
+ return None
163
+
164
+
165
+ __all__ = [
166
+ "A2AClient",
167
+ "CallResult",
168
+ "HttpA2AClient",
169
+ "InMemoryA2AClient",
170
+ ]