agentconnect-cli 0.1.0a1__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.
Files changed (64) hide show
  1. agentconnect/__init__.py +3 -0
  2. agentconnect/a2a_adapter/__init__.py +6 -0
  3. agentconnect/a2a_adapter/adapter.py +565 -0
  4. agentconnect/a2a_adapter/cards.py +149 -0
  5. agentconnect/a2a_adapter/convert.py +157 -0
  6. agentconnect/a2a_adapter/facade.py +560 -0
  7. agentconnect/a2a_adapter/seen.py +49 -0
  8. agentconnect/a2a_adapter/serve.py +67 -0
  9. agentconnect/adapter.py +319 -0
  10. agentconnect/adapters.py +52 -0
  11. agentconnect/apiclient.py +51 -0
  12. agentconnect/backoff.py +42 -0
  13. agentconnect/callback.py +314 -0
  14. agentconnect/cli.py +2324 -0
  15. agentconnect/cli_json.py +470 -0
  16. agentconnect/cli_support.py +444 -0
  17. agentconnect/clock.py +161 -0
  18. agentconnect/config.py +1213 -0
  19. agentconnect/daemon/__init__.py +41 -0
  20. agentconnect/deliveries.py +295 -0
  21. agentconnect/directory_client.py +255 -0
  22. agentconnect/failpoints.py +99 -0
  23. agentconnect/identity.py +111 -0
  24. agentconnect/isolation.py +345 -0
  25. agentconnect/keystore.py +279 -0
  26. agentconnect/limits.py +457 -0
  27. agentconnect/log.py +143 -0
  28. agentconnect/mcp/__init__.py +30 -0
  29. agentconnect/mcp/backend.py +405 -0
  30. agentconnect/mcp/server.py +382 -0
  31. agentconnect/mcp/session.py +373 -0
  32. agentconnect/mcp/tools.py +583 -0
  33. agentconnect/ownerfile.py +238 -0
  34. agentconnect/payloads.py +109 -0
  35. agentconnect/policy.py +1066 -0
  36. agentconnect/provision.py +239 -0
  37. agentconnect/py.typed +0 -0
  38. agentconnect/relay.py +405 -0
  39. agentconnect/runtime/__init__.py +46 -0
  40. agentconnect/runtime/agent.py +225 -0
  41. agentconnect/runtime/api.py +2043 -0
  42. agentconnect/runtime/engine.py +5359 -0
  43. agentconnect/runtime/errors.py +47 -0
  44. agentconnect/runtime/floor.py +649 -0
  45. agentconnect/runtime/groups.py +1351 -0
  46. agentconnect/runtime/knocks.py +1422 -0
  47. agentconnect/runtime/ledger.py +94 -0
  48. agentconnect/runtime/listing.py +1307 -0
  49. agentconnect/runtime/registry.py +53 -0
  50. agentconnect/runtime/runtime.py +158 -0
  51. agentconnect/runtime/sas.py +1002 -0
  52. agentconnect/sas_emoji.py +83 -0
  53. agentconnect/schemas.py +381 -0
  54. agentconnect/shapes.py +56 -0
  55. agentconnect/sockets.py +86 -0
  56. agentconnect/sse.py +234 -0
  57. agentconnect/store.py +3282 -0
  58. agentconnect/tasks.py +35 -0
  59. agentconnect/tokens.py +63 -0
  60. agentconnect/webhook.py +199 -0
  61. agentconnect_cli-0.1.0a1.dist-info/METADATA +103 -0
  62. agentconnect_cli-0.1.0a1.dist-info/RECORD +64 -0
  63. agentconnect_cli-0.1.0a1.dist-info/WHEEL +4 -0
  64. agentconnect_cli-0.1.0a1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,314 @@
1
+ """The in-process adapter: deliveries and events go to Python callbacks.
2
+
3
+ The embedded SDK registers it, so an agent running the runtime in its own
4
+ process gets messages without a webhook or a socket. The callbacks receive
5
+ the same JSON payloads the webhook and SSE adapters send
6
+ (:mod:`agentconnect.payloads`), so the SDK parses one shape in every mode.
7
+
8
+ Semantics match the webhook adapter (``docs/adapters/webhook.md``):
9
+
10
+ * **Messages** are handed over by the runtime's delivery workers, in order
11
+ within each group; groups don't wait for each other. A callback that
12
+ returns is an ack. One that raises, or still runs after ``timeout``
13
+ seconds (on the runtime's clock; it is then cancelled), is ``RETRY``: the
14
+ runtime retries that group with exponential backoff and hands over nothing
15
+ later in that group before it, as for a webhook that answers 500 or times
16
+ out. Nothing is dead-lettered.
17
+ * **Events** are queued in the runtime's encrypted database and handed over
18
+ by workers of their own, in order within each group, with the same
19
+ timeout and a backoff on failure, so a slow handler never holds up the
20
+ runtime or another group. The queue keeps the newest 10 000 per adapter.
21
+ A crash before the callback returned hands the event over again after the
22
+ restart.
23
+ * Until :meth:`CallbackAdapter.open` is called (the SDK's ``serve()``),
24
+ nothing is handed over: messages wait in the inbox, events in the queue.
25
+
26
+ Every callback runs in a task of its own; :meth:`CallbackAdapter.in_handler`
27
+ tells whether the caller is one (the embedded host uses it to shut down from
28
+ inside a handler without waiting on that handler), and
29
+ :meth:`CallbackAdapter.drain` waits until none runs.
30
+
31
+ A callback failure is logged with the exception type only, never the
32
+ message or its body.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import asyncio
38
+ import contextlib
39
+ import random
40
+ from collections.abc import Awaitable, Callable
41
+ from typing import Any, Protocol
42
+
43
+ from agentconnect import log
44
+ from agentconnect.adapter import (
45
+ Capabilities,
46
+ Delivery,
47
+ DeliveryResult,
48
+ Event,
49
+ GroupView,
50
+ OutboundSend,
51
+ Sender,
52
+ )
53
+ from agentconnect.backoff import Backoff
54
+ from agentconnect.clock import Clock
55
+ from agentconnect.payloads import event_payload, message_payload
56
+ from agentconnect.tasks import cancel_and_wait
57
+
58
+ LOG = log.get("agentconnect.callback")
59
+
60
+ DEFAULT_TIMEOUT = 120.0 # seconds a callback may run; agent turns call models
61
+ GRACE_SECS = 5.0 # after the timeout's cancellation, before a handler is abandoned
62
+ STOP_GRACE_SECS = 1.0 # real seconds a stopping runtime waits for a cancelled handler
63
+
64
+ # One payload in; returning means handled, raising means try again later.
65
+ PayloadCallback = Callable[[dict[str, Any]], Awaitable[None]]
66
+
67
+
68
+ class EventQueue(Protocol):
69
+ """Where the adapter keeps events until a callback took them (the store)."""
70
+
71
+ def adapter_event_add(self, adapter: str, event_id: str, payload: dict[str, Any]) -> bool: ...
72
+ def adapter_events(
73
+ self, adapter: str, group_id: str | None = None, limit: int = 100
74
+ ) -> list[tuple[int, dict[str, Any]]]: ...
75
+ def adapter_event_groups(self, adapter: str) -> list[str | None]: ...
76
+ def adapter_event_done(self, seq: int) -> None: ...
77
+
78
+
79
+ class HandlerCancelledError(Exception):
80
+ """A handler ended with ``CancelledError`` although nothing cancelled it
81
+ (it awaited a task or future something else cancelled): a failure of
82
+ that handler, retried like any other, not the end of the delivery."""
83
+
84
+
85
+ def _group_of(payload: dict[str, Any]) -> str | None:
86
+ group = payload.get("group")
87
+ return str(group["id"]) if isinstance(group, dict) and "id" in group else None
88
+
89
+
90
+ class CallbackAdapter:
91
+ """Implements :class:`agentconnect.adapter.Adapter` with two callbacks."""
92
+
93
+ def __init__( # noqa: PLR0913
94
+ self,
95
+ on_message: PayloadCallback,
96
+ on_event: PayloadCallback,
97
+ clock: Clock,
98
+ *,
99
+ name: str = "sdk",
100
+ timeout: float = DEFAULT_TIMEOUT,
101
+ rng: random.Random | None = None,
102
+ on_abandon: Callable[[dict[str, Any]], None] | None = None,
103
+ ) -> None:
104
+ self.name = name
105
+ self._on_abandon = on_abandon
106
+ # At most one handler per group that ignored its cancellation and
107
+ # was left running.
108
+ self._abandoned: dict[str | None, asyncio.Future[None]] = {}
109
+ self._on_message, self._on_event = on_message, on_event
110
+ self._clock = clock
111
+ self._timeout = timeout
112
+ self._rng = rng
113
+ self._queue: EventQueue | None = None
114
+ self._open = asyncio.Event()
115
+ self._wake = asyncio.Event()
116
+ self._worker: asyncio.Task[None] | None = None
117
+ self._groups: dict[str | None, asyncio.Task[None]] = {}
118
+ self._handlers: set[asyncio.Future[None]] = set()
119
+ self._idle = asyncio.Event()
120
+ self._idle.set()
121
+
122
+ def capabilities(self) -> Capabilities:
123
+ # Sends go through the runtime's AgentApi, not through on_outbound.
124
+ return Capabilities(accepts=["text", "data"], originates=False)
125
+
126
+ def bind_store(self, queue: EventQueue) -> None:
127
+ """Called by the runtime at start with its store."""
128
+ self._queue = queue
129
+
130
+ async def on_outbound(self, send: OutboundSend) -> None:
131
+ if self._worker is None or self._worker.done():
132
+ self._worker = asyncio.get_running_loop().create_task(self._run_events())
133
+
134
+ def open(self) -> None:
135
+ """Start handing deliveries and events to the callbacks."""
136
+ self._open.set()
137
+ self._wake.set()
138
+
139
+ def pause(self) -> None:
140
+ """Stop handing new ones over; they wait, nothing is lost. Callbacks
141
+ already running go on (:meth:`drain` waits for them)."""
142
+ self._open.clear()
143
+
144
+ @property
145
+ def is_open(self) -> bool:
146
+ return self._open.is_set()
147
+
148
+ def in_handler(self) -> bool:
149
+ """Whether the calling task is one of this adapter's callbacks."""
150
+ try:
151
+ task = asyncio.current_task()
152
+ except RuntimeError:
153
+ return False
154
+ return task is not None and task in self._handlers
155
+
156
+ def busy(self) -> bool:
157
+ """Whether any callback is running now."""
158
+ return bool(self._handlers)
159
+
160
+ async def drain(self, within: float) -> bool:
161
+ """Wait (real time, at most ``within`` seconds) until no callback runs;
162
+ whether none does."""
163
+ if not self._idle.is_set() and within > 0:
164
+ with contextlib.suppress(TimeoutError):
165
+ await asyncio.wait_for(self._idle.wait(), within)
166
+ return self._idle.is_set()
167
+
168
+ def _track(self, task: asyncio.Future[None]) -> None:
169
+ self._handlers.add(task)
170
+ self._idle.clear()
171
+
172
+ def finished(done: asyncio.Future[None]) -> None:
173
+ self._handlers.discard(done)
174
+ if not self._handlers:
175
+ self._idle.set()
176
+ if not done.cancelled():
177
+ done.exception() # retrieved: an abandoned one has no awaiter
178
+
179
+ task.add_done_callback(finished)
180
+
181
+ async def _call(self, callback: PayloadCallback, payload: dict[str, Any]) -> None:
182
+ """Run ``callback`` in a task of its own, bounded by the timeout on the
183
+ runtime's clock.
184
+
185
+ * It returns: done. It raises: that error. It ends with
186
+ ``CancelledError`` while this caller isn't being cancelled:
187
+ :class:`HandlerCancelledError`.
188
+ * Past the timeout it is cancelled and gets :data:`GRACE_SECS` to
189
+ stop (``TimeoutError``). One that ignores the cancellation is left
190
+ running (logged ``handler_ignored_cancel``), and ``on_abandon`` is
191
+ told, so the redelivery isn't refused as a duplicate in progress;
192
+ a group keeps at most one such handler and otherwise waits for it.
193
+ * If this caller is cancelled (the runtime stops), the handler is
194
+ cancelled too and waited for, at most :data:`STOP_GRACE_SECS`.
195
+ """
196
+ group = _group_of(payload)
197
+ task: asyncio.Future[None] = asyncio.ensure_future(callback(payload))
198
+ self._track(task)
199
+ timer = asyncio.ensure_future(self._clock.sleep(self._timeout, deadline=True))
200
+ settled = False
201
+ try:
202
+ done, _ = await asyncio.wait({task, timer}, return_when=asyncio.FIRST_COMPLETED)
203
+ if task in done:
204
+ settled = True
205
+ if task.cancelled():
206
+ current = asyncio.current_task()
207
+ if current is not None and current.cancelling():
208
+ raise asyncio.CancelledError
209
+ raise HandlerCancelledError
210
+ task.result()
211
+ return
212
+ task.cancel()
213
+ grace = asyncio.ensure_future(self._clock.sleep(GRACE_SECS, deadline=True))
214
+ try:
215
+ await asyncio.wait({task, grace}, return_when=asyncio.FIRST_COMPLETED)
216
+ finally:
217
+ grace.cancel()
218
+ if not task.done():
219
+ held = self._abandoned.get(group)
220
+ if held is not None and not held.done():
221
+ LOG.warning("handler_ignores_cancel", group=log.hid(group), waiting=True)
222
+ await asyncio.wait({task})
223
+ else:
224
+ self._abandoned[group] = task
225
+ LOG.warning("handler_ignored_cancel", group=log.hid(group))
226
+ if self._on_abandon is not None:
227
+ self._on_abandon(payload)
228
+ settled = True
229
+ raise TimeoutError
230
+ finally:
231
+ timer.cancel()
232
+ if not settled and not task.done():
233
+ # This caller is cancelled: the handler goes too.
234
+ task.cancel()
235
+ await asyncio.wait({task}, timeout=STOP_GRACE_SECS)
236
+
237
+ async def deliver(
238
+ self, group: GroupView, sender: Sender, to: list[str], inner: Delivery
239
+ ) -> DeliveryResult:
240
+ await self._open.wait()
241
+ try:
242
+ await self._call(self._on_message, message_payload(group, inner))
243
+ except Exception as e:
244
+ LOG.warning("callback_failed", kind="message", error=type(e).__name__)
245
+ return DeliveryResult.RETRY
246
+ return DeliveryResult.ACK
247
+
248
+ async def on_event(self, group: GroupView | None, event: Event) -> None:
249
+ """Queue the event durably; a worker hands it over. Raises (so the
250
+ runtime keeps the event and sends it again) only without a store."""
251
+ if self._queue is None:
252
+ msg = "the callback adapter has no store yet"
253
+ raise RuntimeError(msg)
254
+ self._queue.adapter_event_add(self.name, event.event_id, event_payload(group, event))
255
+ self._wake.set()
256
+
257
+ def _queued(self, group: str | None) -> list[tuple[int, dict[str, Any]]]:
258
+ if self._queue is None:
259
+ return []
260
+ return self._queue.adapter_events(self.name, group_id=group)
261
+
262
+ async def _run_events(self) -> None:
263
+ """Start a worker for every group with queued events."""
264
+ try:
265
+ while True:
266
+ self._wake.clear()
267
+ await self._open.wait()
268
+ groups = [] if self._queue is None else self._queue.adapter_event_groups(self.name)
269
+ for group in groups:
270
+ running = self._groups.get(group)
271
+ if running is None or running.done():
272
+ worker = asyncio.ensure_future(self._run_group(group))
273
+ worker.add_done_callback(self._group_done)
274
+ self._groups[group] = worker
275
+ await self._wake.wait()
276
+ finally:
277
+ workers, self._groups = list(self._groups.values()), {}
278
+ await cancel_and_wait(*workers)
279
+
280
+ def _group_done(self, worker: asyncio.Future[None]) -> None:
281
+ """A group's event worker that ended by surprise is logged, and the
282
+ next wake starts a new one."""
283
+ if worker.cancelled():
284
+ return
285
+ error = worker.exception()
286
+ if error is not None:
287
+ LOG.error("event_worker_died", reason=type(error).__name__)
288
+ self._wake.set()
289
+
290
+ async def _run_group(self, group: str | None) -> None:
291
+ """One group's events, in order, until none is queued."""
292
+ backoff = Backoff(base=1.0, cap=300.0, rng=self._rng)
293
+ while True:
294
+ rows = self._queued(group)
295
+ if not rows:
296
+ return
297
+ for seq, payload in rows:
298
+ await self._open.wait()
299
+ try:
300
+ await self._call(self._on_event, payload)
301
+ except Exception as e:
302
+ LOG.warning("callback_failed", kind="event", error=type(e).__name__)
303
+ await self._clock.sleep(backoff.next())
304
+ break # retry from the head, in order
305
+ backoff.reset()
306
+ if self._queue is not None:
307
+ self._queue.adapter_event_done(seq)
308
+
309
+ async def close(self) -> None:
310
+ """Called by the runtime when it stops."""
311
+ self._open.clear()
312
+ if self._worker is not None:
313
+ worker, self._worker = self._worker, None
314
+ await cancel_and_wait(worker)