agentforge-a2a 0.2.1__py3-none-any.whl

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.
@@ -0,0 +1,383 @@
1
+ """`A2AServer` — exposes an `Agent` over the A2A wire format
2
+ (feat-014).
3
+
4
+ FastAPI app with three endpoints:
5
+
6
+ - `POST /a2a/v1/calls` — invoke a whitelisted endpoint
7
+ synchronously and return one `A2AResponse`.
8
+ - `POST /a2a/v1/calls/stream` — invoke a whitelisted endpoint
9
+ and emit a Server-Sent-Events stream of `A2AChunk` frames
10
+ (v0.2 follow-up).
11
+ - `GET /a2a/v1/info` — return the endpoint catalogue +
12
+ framework version.
13
+
14
+ Lifecycle:
15
+
16
+ 1. Bearer auth via the canonical `AuthPolicy` (feat-014
17
+ chunk 1).
18
+ 2. Endpoint name validated against the whitelist (404
19
+ otherwise; on the stream endpoint we instead emit a
20
+ single ``kind="error"`` frame and close the stream so
21
+ the contract stays "always SSE" once authenticated).
22
+ 3. `X-AgentForge-Run-Id` (if present) is recorded on the
23
+ response as `parent_run_id` for the cross-framework chain.
24
+ 4. `X-AgentForge-Budget-Usd` (if present + valid) caps the
25
+ inner `Agent.run`'s budget.
26
+ 5. The supplied `task_builder` callable converts the payload
27
+ into the task string the `Agent` receives.
28
+ 6. Findings on the result are serialised through
29
+ `agentforge.recording._finding_payload` so the wire
30
+ shape stays tolerant of any `Finding` Protocol-compatible
31
+ object.
32
+
33
+ Streaming (v0.3) drives `Agent.stream(task)` and forwards each
34
+ `StreamingEvent` emitted by the strategy as one `A2AChunk` SSE
35
+ frame. The strategy's terminal `done` event is swallowed; the
36
+ server emits its own canonical `done` chunk carrying
37
+ `output` + `cost_usd` + `run_id`. The v0.2 transient
38
+ `agent._on_step` hook dance is gone — events arrive
39
+ pre-typed from the strategy generator.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import json
45
+ import logging
46
+ from collections.abc import AsyncIterator, Callable
47
+ from typing import Any
48
+
49
+ import uvicorn
50
+ from agentforge.agent import Agent
51
+ from agentforge.recording import _finding_payload
52
+ from agentforge_core.contracts.auth import AuthPolicy
53
+ from agentforge_core.observability.tracing import get_tracer
54
+ from agentforge_core.production.budget import BudgetPolicy
55
+ from fastapi import Depends, FastAPI, HTTPException, Request, status
56
+ from fastapi.responses import StreamingResponse
57
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
58
+ from opentelemetry.trace.propagation.tracecontext import (
59
+ TraceContextTextMapPropagator,
60
+ )
61
+ from pydantic import BaseModel, ConfigDict
62
+
63
+ from agentforge_a2a._runner import A2AServerRunner
64
+ from agentforge_a2a.values import (
65
+ A2AChunk,
66
+ A2AEndpointConfig,
67
+ A2AEndpointDescriptor,
68
+ A2APeerInfo,
69
+ A2AResponse,
70
+ )
71
+
72
+ _log = logging.getLogger("agentforge_a2a.server")
73
+ _VERSION = "0.1"
74
+ """A2A wire-format version we ship — bumped when the request/
75
+ response shape changes."""
76
+
77
+ TaskBuilder = Callable[[str, dict[str, Any]], str]
78
+ """Maps (endpoint_name, payload) -> the task string the agent
79
+ receives. Default: JSON-encode the payload."""
80
+
81
+
82
+ def _default_task_builder(endpoint: str, payload: dict[str, Any]) -> str:
83
+ """Concatenates endpoint name + JSON payload for the agent."""
84
+ body = json.dumps(payload, sort_keys=True)
85
+ return f"a2a.{endpoint}: {body}"
86
+
87
+
88
+ def _sse_frame(chunk: A2AChunk) -> bytes:
89
+ """Encode one `A2AChunk` as a single SSE `data:` frame."""
90
+ return b"data: " + chunk.model_dump_json().encode("utf-8") + b"\n\n"
91
+
92
+
93
+ class CallRequest(BaseModel):
94
+ """Body of `POST /a2a/v1/calls`."""
95
+
96
+ model_config = ConfigDict(extra="forbid")
97
+
98
+ endpoint: str
99
+ payload: dict[str, Any]
100
+ budget_usd: float | None = None
101
+
102
+
103
+ class A2AServer:
104
+ """FastAPI app exposing an `Agent` as an A2A peer."""
105
+
106
+ def __init__(
107
+ self,
108
+ agent: Agent,
109
+ *,
110
+ auth: AuthPolicy,
111
+ endpoints: list[str],
112
+ task_builder: TaskBuilder | None = None,
113
+ host: str = "0.0.0.0", # noqa: S104 # nosec B104
114
+ port: int = 8080,
115
+ runner: A2AServerRunner | None = None,
116
+ endpoint_descriptors: list[A2AEndpointConfig] | None = None,
117
+ server_name: str = "agentforge",
118
+ ) -> None:
119
+ self._agent = agent
120
+ self._auth = auth
121
+ self._endpoints = list(endpoints)
122
+ self._task_builder = task_builder or _default_task_builder
123
+ self._host = host
124
+ self._port = port
125
+ self._runner = runner
126
+ self._endpoint_descriptors = list(endpoint_descriptors or [])
127
+ self._server_name = server_name
128
+ self.app = self._build_app()
129
+
130
+ @property
131
+ def endpoints(self) -> tuple[str, ...]:
132
+ return tuple(self._endpoints)
133
+
134
+ def _build_app(self) -> FastAPI:
135
+ app = FastAPI(title="agentforge-a2a")
136
+ bearer = HTTPBearer(auto_error=False)
137
+
138
+ async def require_principal(
139
+ credentials: HTTPAuthorizationCredentials | None = Depends(bearer), # noqa: B008
140
+ ) -> Any:
141
+ token = credentials.credentials if credentials is not None else None
142
+ principal = await self._auth.authenticate(token)
143
+ if principal is None:
144
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
145
+ return principal
146
+
147
+ @app.get("/a2a/v1/info")
148
+ async def info(_p: Any = Depends(require_principal)) -> dict[str, Any]: # noqa: B008
149
+ return self._build_info().model_dump(mode="json")
150
+
151
+ @app.post("/a2a/v1/calls")
152
+ async def call(
153
+ body: CallRequest,
154
+ request: Request,
155
+ _p: Any = Depends(require_principal), # noqa: B008
156
+ ) -> dict[str, Any]:
157
+ return await self._handle_call(body, request)
158
+
159
+ @app.post("/a2a/v1/calls/stream")
160
+ async def stream_call(
161
+ body: CallRequest,
162
+ request: Request,
163
+ _p: Any = Depends(require_principal), # noqa: B008
164
+ ) -> StreamingResponse:
165
+ return StreamingResponse(
166
+ self._stream_call(body, request),
167
+ media_type="text/event-stream",
168
+ )
169
+
170
+ return app
171
+
172
+ def _build_info(self) -> A2APeerInfo:
173
+ """Render the discovery payload returned by `/a2a/v1/info`."""
174
+ by_name = {d.name: d for d in self._endpoint_descriptors}
175
+ descriptors = [
176
+ A2AEndpointDescriptor(
177
+ name=name,
178
+ description=(by_name[name].description if name in by_name else ""),
179
+ input_schema=(dict(by_name[name].accepts) if name in by_name else {}),
180
+ )
181
+ for name in self._endpoints
182
+ ]
183
+ return A2APeerInfo(
184
+ version=_VERSION,
185
+ server_name=self._server_name,
186
+ endpoints=descriptors,
187
+ )
188
+
189
+ async def _handle_call(self, body: CallRequest, request: Request) -> dict[str, Any]:
190
+ if body.endpoint not in self._endpoints:
191
+ raise HTTPException(
192
+ status_code=status.HTTP_404_NOT_FOUND,
193
+ detail=f"unknown endpoint: {body.endpoint!r}",
194
+ )
195
+ parent_run_id = request.headers.get("X-AgentForge-Run-Id")
196
+ # feat-009 v0.3 polish: W3C TraceContext propagation. Extract
197
+ # the caller's span context from `traceparent` (no-op when
198
+ # missing) and use it as the parent for the inner work so the
199
+ # `a2a.call` span — and its child `agent.run` — stitches into
200
+ # the caller's trace.
201
+ extracted_ctx = TraceContextTextMapPropagator().extract(dict(request.headers))
202
+ tracer = get_tracer()
203
+ with tracer.start_as_current_span(
204
+ "a2a.call",
205
+ context=extracted_ctx,
206
+ attributes={
207
+ "agentforge.a2a.endpoint": body.endpoint,
208
+ "agentforge.a2a.parent_run_id": parent_run_id or "",
209
+ },
210
+ ):
211
+ result = await self._run_with_budget_cap(body, request)
212
+ response = A2AResponse(
213
+ output=result.output,
214
+ findings=tuple(_finding_payload(f) for f in getattr(result, "findings", ())),
215
+ cost_usd=result.cost_usd,
216
+ run_id=result.run_id,
217
+ parent_run_id=parent_run_id,
218
+ )
219
+ body_dict: dict[str, Any] = json.loads(response.model_dump_json())
220
+ return body_dict
221
+
222
+ async def _stream_call(self, body: CallRequest, request: Request) -> AsyncIterator[bytes]:
223
+ """SSE generator for `POST /a2a/v1/calls/stream`.
224
+
225
+ Drives `Agent.stream(task)` and forwards each
226
+ ``StreamingEvent`` as one ``A2AChunk`` ``data:`` frame.
227
+ The strategy's terminal ``done`` event is swallowed; this
228
+ method emits the canonical ``done`` chunk carrying
229
+ ``output`` + ``cost_usd`` + ``run_id``. Strategy errors
230
+ surface as a terminal ``kind="error"`` frame.
231
+ """
232
+ parent_run_id = request.headers.get("X-AgentForge-Run-Id")
233
+ if body.endpoint not in self._endpoints:
234
+ yield _sse_frame(
235
+ A2AChunk(
236
+ kind="error",
237
+ content={
238
+ "error": "unknown_endpoint",
239
+ "message": f"unknown endpoint: {body.endpoint!r}",
240
+ },
241
+ parent_run_id=parent_run_id,
242
+ )
243
+ )
244
+ return
245
+
246
+ # feat-009 v0.3 polish: W3C TraceContext propagation.
247
+ extracted_ctx = TraceContextTextMapPropagator().extract(dict(request.headers))
248
+ tracer = get_tracer()
249
+ a2a_span_cm = tracer.start_as_current_span(
250
+ "a2a.call",
251
+ context=extracted_ctx,
252
+ attributes={
253
+ "agentforge.a2a.endpoint": body.endpoint,
254
+ "agentforge.a2a.parent_run_id": parent_run_id or "",
255
+ "agentforge.a2a.streaming": True,
256
+ },
257
+ )
258
+
259
+ task_str = self._task_builder(body.endpoint, body.payload)
260
+ budget_cap = self._read_budget_header(request)
261
+ original_budget = self._agent._budget
262
+ if budget_cap is not None:
263
+ self._agent._budget = BudgetPolicy(
264
+ usd=min(original_budget.usd, budget_cap),
265
+ max_tokens=original_budget.max_tokens,
266
+ max_iterations=original_budget.max_iterations,
267
+ error_streak_limit=original_budget.error_streak_limit,
268
+ )
269
+
270
+ done_content: dict[str, Any] | None = None
271
+ a2a_span_cm.__enter__()
272
+ try:
273
+ async for event in self._agent.stream(task_str):
274
+ if event.kind == "done":
275
+ raw = event.content if isinstance(event.content, dict) else {}
276
+ done_content = {
277
+ "output": _coerce_jsonable(raw.get("output")),
278
+ "cost_usd": float(raw.get("cost_usd", 0.0) or 0.0),
279
+ "run_id": str(raw.get("run_id", "")),
280
+ }
281
+ continue
282
+ yield _sse_frame(
283
+ A2AChunk(
284
+ kind=event.kind,
285
+ content=event.content,
286
+ metadata=dict(event.metadata),
287
+ parent_run_id=parent_run_id,
288
+ )
289
+ )
290
+ except Exception as exc:
291
+ yield _sse_frame(
292
+ A2AChunk(
293
+ kind="error",
294
+ content={
295
+ "error": type(exc).__name__,
296
+ "message": str(exc),
297
+ },
298
+ parent_run_id=parent_run_id,
299
+ )
300
+ )
301
+ return
302
+ finally:
303
+ if budget_cap is not None:
304
+ self._agent._budget = original_budget
305
+ a2a_span_cm.__exit__(None, None, None)
306
+
307
+ final = done_content or {"output": None, "cost_usd": 0.0, "run_id": ""}
308
+ yield _sse_frame(
309
+ A2AChunk(
310
+ kind="done",
311
+ content=final,
312
+ run_id=final["run_id"] or None,
313
+ parent_run_id=parent_run_id,
314
+ )
315
+ )
316
+
317
+ async def _run_with_budget_cap(self, body: CallRequest, request: Request) -> Any:
318
+ """Run the agent honouring the per-call ``X-AgentForge-Budget-Usd``
319
+ header. Restores the original budget on the way out."""
320
+ budget_cap = self._read_budget_header(request)
321
+ task = self._task_builder(body.endpoint, body.payload)
322
+ if budget_cap is None:
323
+ return await self._agent.run(task)
324
+ original_budget = self._agent._budget
325
+ self._agent._budget = BudgetPolicy(
326
+ usd=min(original_budget.usd, budget_cap),
327
+ max_tokens=original_budget.max_tokens,
328
+ max_iterations=original_budget.max_iterations,
329
+ error_streak_limit=original_budget.error_streak_limit,
330
+ )
331
+ try:
332
+ return await self._agent.run(task)
333
+ finally:
334
+ self._agent._budget = original_budget
335
+
336
+ @staticmethod
337
+ def _read_budget_header(request: Request) -> float | None:
338
+ raw = request.headers.get("X-AgentForge-Budget-Usd")
339
+ if raw is None or not raw:
340
+ return None
341
+ try:
342
+ value = float(raw)
343
+ except ValueError:
344
+ _log.warning("invalid X-AgentForge-Budget-Usd header: %r", raw)
345
+ return None
346
+ if value < 0:
347
+ return None
348
+ return value
349
+
350
+ async def serve(self) -> None:
351
+ """Run the server until interrupted. Uses the injected
352
+ runner when present, else falls back to a real uvicorn
353
+ server."""
354
+ if self._runner is not None:
355
+ await self._runner.serve()
356
+ return
357
+ config = uvicorn.Config(self.app, host=self._host, port=self._port, log_level="info")
358
+ server = uvicorn.Server(config)
359
+ await server.serve()
360
+
361
+ async def stop(self) -> None:
362
+ if self._runner is not None:
363
+ await self._runner.stop()
364
+
365
+
366
+ def _coerce_jsonable(value: Any) -> Any:
367
+ """Best-effort JSON-friendly coercion for the streamed
368
+ ``done`` content. Strings / numbers / bools / None / dicts /
369
+ lists pass through; everything else becomes ``str(value)``."""
370
+ if isinstance(value, str | int | float | bool) or value is None:
371
+ return value
372
+ if isinstance(value, dict):
373
+ return {str(k): _coerce_jsonable(v) for k, v in value.items()}
374
+ if isinstance(value, list | tuple):
375
+ return [_coerce_jsonable(v) for v in value]
376
+ return str(value)
377
+
378
+
379
+ __all__ = [
380
+ "A2AServer",
381
+ "CallRequest",
382
+ "TaskBuilder",
383
+ ]
@@ -0,0 +1,142 @@
1
+ """A2A value types (feat-014).
2
+
3
+ Wire-format-shaped Pydantic models that ride between the client
4
+ (`agent_call`) and the server (`A2AServer`). All frozen — values
5
+ are immutable once built.
6
+
7
+ `A2AResponse` mirrors spec §4.2. The three config models
8
+ (`A2APeerConfig`, `A2AEndpointConfig`, `A2AExposeConfig`) feed
9
+ the YAML side via `A2AConfig`.
10
+
11
+ v0.2 follow-up adds the discovery + streaming wire shapes:
12
+
13
+ - `A2AEndpointDescriptor` + `A2APeerInfo` — the rich shape
14
+ returned by `GET /a2a/v1/info` and consumed by
15
+ `discover_peer(...)`.
16
+ - `A2AChunk` + `A2AChunkKind` — the streaming wire format
17
+ emitted by `POST /a2a/v1/calls/stream`.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any
23
+
24
+ from agentforge_core.values.chat import StreamingChunkKind
25
+ from pydantic import BaseModel, ConfigDict, Field
26
+
27
+ A2AChunkKind = StreamingChunkKind
28
+ """Kinds of frames streamed over `POST /a2a/v1/calls/stream`.
29
+
30
+ Aliased to the framework-wide ``StreamingChunkKind`` so chat and A2A
31
+ share one wire vocabulary (feat-014 v0.3). Common kinds:
32
+
33
+ - ``text`` — per-token text chunk emitted by strategies that override
34
+ ``ReasoningStrategy.stream()``.
35
+ - ``thinking`` — model-internal reasoning token (when surfaced).
36
+ - ``step`` — generic agent step boundary (think / observe envelope).
37
+ - ``tool_call`` — agent invoked a tool.
38
+ - ``tool_result`` — observation from a tool call.
39
+ - ``done`` — terminal frame carrying the final output + cost.
40
+ - ``error`` — terminal error frame.
41
+ """
42
+
43
+
44
+ class A2AResponse(BaseModel):
45
+ """Response returned by `agent_call(...)` and built by
46
+ `A2AServer.POST /a2a/v1/calls`."""
47
+
48
+ # Not strict: the wire format coerces tuple↔list on JSON round-trip,
49
+ # so the client-side `model_validate(response.json())` needs the
50
+ # default lax sequence handling.
51
+ model_config = ConfigDict(frozen=True)
52
+
53
+ output: Any
54
+ findings: tuple[dict[str, Any], ...] = ()
55
+ cost_usd: float = Field(default=0.0, ge=0.0)
56
+ run_id: str
57
+ parent_run_id: str | None = None
58
+ metadata: dict[str, Any] = Field(default_factory=dict)
59
+
60
+
61
+ class A2APeerConfig(BaseModel):
62
+ """One entry under `modules.protocols[a2a].config.peers:`.
63
+
64
+ `auth` is a free-form dict: `{type: bearer, token: ...}` or
65
+ `{type: mtls, cert: ..., key: ...}` — interpreted by
66
+ `agentforge_a2a.auth.build_outgoing_auth`.
67
+ """
68
+
69
+ model_config = ConfigDict(strict=True, extra="forbid")
70
+
71
+ name: str = Field(min_length=1)
72
+ url: str = Field(min_length=1)
73
+ auth: dict[str, Any] = Field(default_factory=dict)
74
+
75
+
76
+ class A2AEndpointConfig(BaseModel):
77
+ """One entry under `modules.protocols[a2a].config.expose.endpoints:`."""
78
+
79
+ model_config = ConfigDict(strict=True, extra="forbid")
80
+
81
+ name: str = Field(min_length=1)
82
+ description: str = ""
83
+ accepts: dict[str, Any] = Field(default_factory=dict)
84
+
85
+
86
+ class A2AExposeConfig(BaseModel):
87
+ """`modules.protocols[a2a].config.expose:` — server-side
88
+ configuration when this agent acts as an A2A peer."""
89
+
90
+ model_config = ConfigDict(strict=True, extra="forbid")
91
+
92
+ enabled: bool = True
93
+ host: str = "0.0.0.0" # noqa: S104 # nosec B104 — caller binds explicitly in prod
94
+ port: int = 8080
95
+ auth: dict[str, Any] = Field(default_factory=dict)
96
+ endpoints: list[A2AEndpointConfig] = Field(default_factory=list)
97
+
98
+
99
+ class A2AEndpointDescriptor(BaseModel):
100
+ """One endpoint advertised by `GET /a2a/v1/info`."""
101
+
102
+ model_config = ConfigDict(frozen=True, strict=True)
103
+
104
+ name: str = Field(min_length=1)
105
+ description: str = ""
106
+ input_schema: dict[str, Any] = Field(default_factory=dict)
107
+
108
+
109
+ class A2APeerInfo(BaseModel):
110
+ """Discovery payload returned by `GET /a2a/v1/info`."""
111
+
112
+ model_config = ConfigDict(frozen=True, strict=True)
113
+
114
+ version: str
115
+ server_name: str = ""
116
+ endpoints: list[A2AEndpointDescriptor] = Field(default_factory=list)
117
+ metadata: dict[str, Any] = Field(default_factory=dict)
118
+
119
+
120
+ class A2AChunk(BaseModel):
121
+ """One frame on the streaming `/a2a/v1/calls/stream` channel."""
122
+
123
+ model_config = ConfigDict(frozen=True, strict=True)
124
+
125
+ kind: A2AChunkKind
126
+ content: dict[str, Any] | str | None = None
127
+ step: dict[str, Any] | None = None
128
+ run_id: str | None = None
129
+ parent_run_id: str | None = None
130
+ metadata: dict[str, Any] = Field(default_factory=dict)
131
+
132
+
133
+ __all__ = [
134
+ "A2AChunk",
135
+ "A2AChunkKind",
136
+ "A2AEndpointConfig",
137
+ "A2AEndpointDescriptor",
138
+ "A2AExposeConfig",
139
+ "A2APeerConfig",
140
+ "A2APeerInfo",
141
+ "A2AResponse",
142
+ ]
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentforge-a2a
3
+ Version: 0.2.1
4
+ Summary: A2A (Agent-to-Agent) protocol client + server for AgentForge
5
+ Project-URL: Homepage, https://github.com/Scaffoldic/agentforge-py
6
+ Project-URL: Repository, https://github.com/Scaffoldic/agentforge-py
7
+ Project-URL: Changelog, https://github.com/Scaffoldic/agentforge-py/blob/main/CHANGELOG.md
8
+ Author: The AgentForge Authors
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: a2a,agent,ai,multi-agent,protocol
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: agentforge-core~=0.2.1
22
+ Requires-Dist: agentforge-py~=0.2.1
23
+ Requires-Dist: fastapi>=0.115
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: uvicorn>=0.32
26
+ Description-Content-Type: text/markdown
27
+
28
+ # agentforge-a2a
29
+
30
+ A2A (Agent-to-Agent) protocol support for AgentForge: cross-framework
31
+ agent invocation over HTTP, with bearer / mTLS auth, run_id chain,
32
+ and budget propagation.
33
+
34
+ See [`docs/features/feat-014-a2a-protocol.md`](https://github.com/Scaffoldic/agentforge-py/blob/main/docs/features/feat-014-a2a-protocol.md)
35
+ for the design and runbook.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install agentforge-a2a
41
+ # or, from a scaffolded project:
42
+ agentforge add module a2a
43
+ ```
44
+
45
+ ## Call another agent
46
+
47
+ ```python
48
+ from agentforge_a2a import A2APeer, agent_call
49
+
50
+ peer = A2APeer.from_config({
51
+ "name": "fact-checker",
52
+ "url": "https://internal.fact-checker.example/a2a",
53
+ "auth": {"type": "bearer", "token": "${FACT_CHECKER_TOKEN}"},
54
+ })
55
+
56
+ result = await agent_call(
57
+ "fact-checker:verify",
58
+ {"claim": "The capital of Australia is Sydney."},
59
+ timeout_s=30,
60
+ peers={"fact-checker": peer},
61
+ )
62
+ print(result.output)
63
+ ```
64
+
65
+ ## Expose this agent
66
+
67
+ ```python
68
+ from agentforge import Agent, EnvBearerAuth
69
+ from agentforge_a2a import A2AServer
70
+
71
+ server = A2AServer(
72
+ agent=Agent(model="anthropic:claude-sonnet-4-6", strategy="react"),
73
+ auth=EnvBearerAuth("A2A_TOKENS"),
74
+ endpoints=["review-pr"],
75
+ host="0.0.0.0",
76
+ port=8080,
77
+ )
78
+ await server.serve()
79
+ ```
@@ -0,0 +1,16 @@
1
+ agentforge_a2a/__init__.py,sha256=PTQojfSXZl_kIJmEZBoQsClboyX22aERp-iFoohuqxw,1680
2
+ agentforge_a2a/_inmem_runner.py,sha256=HfUc6LAY0jhrxR39Z0zLXMeZgrFz_ULlisEKDCOHROA,4651
3
+ agentforge_a2a/_runner.py,sha256=STAvp2MN7Nf5wZXQw7QrzN6fTY3Ap9Lw6ogkczT6Wq8,7807
4
+ agentforge_a2a/auth.py,sha256=BckHadKq-RVfyYbecdnTZOAAU7ydLfRppUg7Pw5Fckw,2940
5
+ agentforge_a2a/bridge.py,sha256=M5DttrM-zAeuXX50RhhUqU-RpCu4defpFsEh1PWA3ks,5474
6
+ agentforge_a2a/client.py,sha256=wDWMH81UZ1N65XyUTmIDFWJTR8BVtiIp0nX-7-h_yco,11557
7
+ agentforge_a2a/config.py,sha256=F4e18Qr9SNmVaLBWR-YOAN55rwDTqW5f8brMZABDFoo,698
8
+ agentforge_a2a/manifest.yaml,sha256=yZ5vdyhH9JPORsfF3mLFnbz9jbP3BUuq-sWexDCFjeE,709
9
+ agentforge_a2a/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ agentforge_a2a/server.py,sha256=T1iBkgRQiFSOGnhtgnyBe-T5Nb35hCd2Hzf9IGzMIfw,14354
11
+ agentforge_a2a/values.py,sha256=gf3bXyhX4o4KhoN-0zpXoPDkVguRxDkKAXupSvW0Jq4,4485
12
+ agentforge_a2a-0.2.1.dist-info/METADATA,sha256=MC-hH7zqNcSjgSQVRv96uz5OijkMGpG7SgS1yvhV1dQ,2344
13
+ agentforge_a2a-0.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
14
+ agentforge_a2a-0.2.1.dist-info/entry_points.txt,sha256=2z9S4rbsUvPtzTRpXZpHUyPpePjJ4A-tkVJqg0jjmr4,61
15
+ agentforge_a2a-0.2.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
16
+ agentforge_a2a-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [agentforge.protocols]
2
+ a2a = agentforge_a2a.bridge:A2ABridge