cf-agents 0.1.0__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 (112) hide show
  1. agents/__init__.py +175 -0
  2. agents/_ffi.py +516 -0
  3. agents/agent/__init__.py +31 -0
  4. agents/agent/agent.py +1201 -0
  5. agents/agent/current.py +34 -0
  6. agents/agent/errors.py +12 -0
  7. agents/agent/routing.py +343 -0
  8. agents/agent/types.py +139 -0
  9. agents/chat/__init__.py +73 -0
  10. agents/chat/_json.py +16 -0
  11. agents/chat/accumulator.py +254 -0
  12. agents/chat/agent.py +1565 -0
  13. agents/chat/builder.py +436 -0
  14. agents/chat/chunks.py +369 -0
  15. agents/chat/codec.py +241 -0
  16. agents/chat/concurrency.py +148 -0
  17. agents/chat/errors.py +17 -0
  18. agents/chat/handshake.py +265 -0
  19. agents/chat/messages.py +312 -0
  20. agents/chat/persistence.py +56 -0
  21. agents/chat/protocol.py +248 -0
  22. agents/chat/reconciler.py +287 -0
  23. agents/chat/repair.py +128 -0
  24. agents/chat/resumable_stream.py +653 -0
  25. agents/chat/terminal.py +51 -0
  26. agents/chat/tool_state.py +238 -0
  27. agents/chat/turn_queue.py +118 -0
  28. agents/chat/types.py +112 -0
  29. agents/core/__init__.py +24 -0
  30. agents/core/encoding.py +56 -0
  31. agents/core/errors.py +31 -0
  32. agents/core/events.py +95 -0
  33. agents/core/methods.py +81 -0
  34. agents/core/naming.py +21 -0
  35. agents/core/platform_errors.py +110 -0
  36. agents/core/records.py +20 -0
  37. agents/core/retry.py +143 -0
  38. agents/core/sql.py +100 -0
  39. agents/core/timing.py +74 -0
  40. agents/core/types.py +50 -0
  41. agents/dynamic_agents/__init__.py +18 -0
  42. agents/dynamic_agents/api.py +77 -0
  43. agents/dynamic_agents/connections.py +130 -0
  44. agents/dynamic_agents/dynamic_agents.py +997 -0
  45. agents/dynamic_agents/errors.py +12 -0
  46. agents/dynamic_agents/paths.py +258 -0
  47. agents/dynamic_agents/registry.py +104 -0
  48. agents/dynamic_agents/stubs.py +115 -0
  49. agents/dynamic_agents/types.py +131 -0
  50. agents/fibers/__init__.py +34 -0
  51. agents/fibers/errors.py +25 -0
  52. agents/fibers/fibers.py +898 -0
  53. agents/fibers/keep_alive.py +197 -0
  54. agents/fibers/store.py +352 -0
  55. agents/fibers/types.py +218 -0
  56. agents/lifecycle/__init__.py +34 -0
  57. agents/lifecycle/capability.py +132 -0
  58. agents/lifecycle/capability_runner.py +149 -0
  59. agents/lifecycle/errors.py +28 -0
  60. agents/lifecycle/host_context.py +65 -0
  61. agents/lifecycle/job_driver.py +475 -0
  62. agents/lifecycle/job_queue.py +497 -0
  63. agents/lifecycle/lifecycle.py +596 -0
  64. agents/lifecycle/services.py +144 -0
  65. agents/lifecycle/types.py +290 -0
  66. agents/observability/__init__.py +6 -0
  67. agents/observability/observability.py +88 -0
  68. agents/observability/types.py +40 -0
  69. agents/py.typed +0 -0
  70. agents/queue/__init__.py +6 -0
  71. agents/queue/queue.py +561 -0
  72. agents/queue/types.py +121 -0
  73. agents/schedules/__init__.py +7 -0
  74. agents/schedules/cron.py +270 -0
  75. agents/schedules/errors.py +13 -0
  76. agents/schedules/scheduler.py +754 -0
  77. agents/schedules/timing.py +96 -0
  78. agents/schedules/types.py +171 -0
  79. agents/sessions/__init__.py +32 -0
  80. agents/sessions/chunking.py +31 -0
  81. agents/sessions/sanitize.py +66 -0
  82. agents/sessions/sessions.py +552 -0
  83. agents/sessions/store.py +331 -0
  84. agents/sessions/tokens.py +113 -0
  85. agents/sessions/types.py +119 -0
  86. agents/state/__init__.py +6 -0
  87. agents/state/state.py +147 -0
  88. agents/state/types.py +18 -0
  89. agents/streams/__init__.py +17 -0
  90. agents/streams/errors.py +38 -0
  91. agents/streams/store.py +275 -0
  92. agents/streams/streams.py +561 -0
  93. agents/streams/types.py +93 -0
  94. agents/tasks/__init__.py +54 -0
  95. agents/tasks/decorator.py +132 -0
  96. agents/tasks/engine.py +265 -0
  97. agents/tasks/errors.py +183 -0
  98. agents/tasks/registry.py +26 -0
  99. agents/tasks/replay.py +366 -0
  100. agents/tasks/serialization.py +50 -0
  101. agents/tasks/store.py +223 -0
  102. agents/tasks/tasks.py +1116 -0
  103. agents/tasks/types.py +354 -0
  104. agents/websockets/__init__.py +19 -0
  105. agents/websockets/connection.py +187 -0
  106. agents/websockets/errors.py +31 -0
  107. agents/websockets/rpc.py +286 -0
  108. agents/websockets/types.py +123 -0
  109. agents/websockets/websockets.py +513 -0
  110. cf_agents-0.1.0.dist-info/METADATA +24 -0
  111. cf_agents-0.1.0.dist-info/RECORD +112 -0
  112. cf_agents-0.1.0.dist-info/WHEEL +4 -0
agents/__init__.py ADDED
@@ -0,0 +1,175 @@
1
+ """Python port of the Cloudflare Agents SDK.
2
+
3
+ Re-exports each subpackage's public names (its ``__all__``). Keep ``__all__``
4
+ below in step with the subpackages' (``tests/test_public_api.py`` checks it).
5
+ """
6
+
7
+ from .agent import *
8
+ from .chat import *
9
+ from .core import *
10
+ from .dynamic_agents import *
11
+ from .fibers import *
12
+ from .lifecycle import *
13
+ from .observability import *
14
+ from .queue import *
15
+ from .schedules import *
16
+ from .sessions import *
17
+ from .state import *
18
+ from .streams import *
19
+ from .tasks import *
20
+ from .websockets import *
21
+
22
+ __all__ = (
23
+ "DEFAULT_MAX_CHUNK_BYTES",
24
+ "AIChatAgent",
25
+ "AIChatAgentOptions",
26
+ "Agent",
27
+ "AgentOptions",
28
+ "AgentPathStep",
29
+ "AgentRoute",
30
+ "AgentStub",
31
+ "AgentsException",
32
+ "AnyToolPart",
33
+ "AppendEvent",
34
+ "AppendResult",
35
+ "CallableMetadata",
36
+ "CancelledRun",
37
+ "ChatMessageOptions",
38
+ "ChatResponseResult",
39
+ "ChatStreamError",
40
+ "ClearEvent",
41
+ "ClientToolSchema",
42
+ "CompletedRun",
43
+ "Connection",
44
+ "ConnectionContext",
45
+ "ConnectionStateTooLargeError",
46
+ "CurrentAgent",
47
+ "DataPart",
48
+ "Debounce",
49
+ "DeleteEvent",
50
+ "Disposable",
51
+ "DuplicateConnectionIdError",
52
+ "DuplicateTaskStepError",
53
+ "Duration",
54
+ "DynamicAgents",
55
+ "FailedRun",
56
+ "FiberAborted",
57
+ "FiberCompleted",
58
+ "FiberConflictError",
59
+ "FiberContext",
60
+ "FiberErrored",
61
+ "FiberInspection",
62
+ "FiberInterrupted",
63
+ "FiberNotFoundError",
64
+ "FiberRecoveryContext",
65
+ "FiberRecoveryResult",
66
+ "FiberStatus",
67
+ "Fibers",
68
+ "FilePart",
69
+ "InvalidCronExpressionError",
70
+ "JSONValue",
71
+ "JobContext",
72
+ "JobOutcome",
73
+ "KeepAlive",
74
+ "Lifecycle",
75
+ "LifecycleCapability",
76
+ "LifecycleEvent",
77
+ "LifecycleJob",
78
+ "LifecycleJobs",
79
+ "LifecycleRoutes",
80
+ "LifecycleServices",
81
+ "LifecycleStatus",
82
+ "LoggingObservability",
83
+ "MemoryLimitContext",
84
+ "MessageConcurrency",
85
+ "MissingTaskDefinitionError",
86
+ "NonRetryableError",
87
+ "Observability",
88
+ "ObservabilityEvent",
89
+ "PendingRun",
90
+ "Queue",
91
+ "QueueItem",
92
+ "ReadonlyConnectionError",
93
+ "ReasoningPart",
94
+ "RecentHistoryResult",
95
+ "Reschedule",
96
+ "RetryOptions",
97
+ "RouteAddress",
98
+ "RouteContext",
99
+ "RoutingRetryEvent",
100
+ "RoutingRetryOptions",
101
+ "RunningRun",
102
+ "SaveMessagesResult",
103
+ "Schedule",
104
+ "ScheduleType",
105
+ "Scheduler",
106
+ "Session",
107
+ "SessionChangeEvent",
108
+ "SessionChangeListener",
109
+ "SessionMessage",
110
+ "SessionRowStat",
111
+ "Sessions",
112
+ "Source",
113
+ "SourceDocumentPart",
114
+ "SourceUrlPart",
115
+ "Sql",
116
+ "SqlError",
117
+ "SqlValue",
118
+ "StartFiberResult",
119
+ "State",
120
+ "StateSource",
121
+ "StepInterruption",
122
+ "StepRetries",
123
+ "StepStartPart",
124
+ "StepTimeoutError",
125
+ "StreamChunk",
126
+ "StreamClosedError",
127
+ "StreamNotFoundError",
128
+ "StreamSerializationError",
129
+ "StreamState",
130
+ "StreamStatus",
131
+ "StreamWriter",
132
+ "StreamingResponse",
133
+ "Streams",
134
+ "SubAgentAbortedError",
135
+ "SubAgentInfo",
136
+ "TaskError",
137
+ "TaskHandle",
138
+ "TaskReceipt",
139
+ "TaskReplayDivergedError",
140
+ "TaskRun",
141
+ "TaskRunState",
142
+ "TaskSerializationError",
143
+ "TaskStep",
144
+ "TaskStepAttempt",
145
+ "Tasks",
146
+ "TextPart",
147
+ "ToolApproval",
148
+ "ToolApprovalRequestedPart",
149
+ "ToolApprovalRespondedPart",
150
+ "ToolInputAvailablePart",
151
+ "ToolInputStreamingPart",
152
+ "ToolOutputAvailablePart",
153
+ "ToolOutputDeniedPart",
154
+ "ToolOutputErrorPart",
155
+ "ToolPart",
156
+ "ToolState",
157
+ "UIMessage",
158
+ "UIMessagePart",
159
+ "UnknownPart",
160
+ "UpdateEvent",
161
+ "WaitingRun",
162
+ "WebSocketHandlers",
163
+ "WebSockets",
164
+ "build_agent_path",
165
+ "build_agent_url",
166
+ "callable",
167
+ "get_agent_by_name",
168
+ "get_current_agent",
169
+ "get_sub_agent_by_name",
170
+ "retry",
171
+ "route_agent_request",
172
+ "route_sub_agent_request",
173
+ "subscribe",
174
+ "task",
175
+ )
agents/_ffi.py ADDED
@@ -0,0 +1,516 @@
1
+ """The boundary between Python and the Workers JavaScript runtime.
2
+
3
+ This is the only SDK module that imports ``js`` / ``pyodide``, so everything
4
+ else imports and unit-tests under CPython (tests replace this module). The
5
+ conversion rules are specified in ``.design/utilities.md`` §3.
6
+
7
+ Storage, SQL parameters, and hibernation attachments use structured clone,
8
+ so they go through `py_to_js` / `js_to_py`. Native DO RPC goes through the
9
+ Workers SDK's own converters, wrapped by `to_rpc` / `from_rpc`.
10
+ """
11
+
12
+ import json
13
+ import logging
14
+ from collections.abc import AsyncIterator, Awaitable, Callable, Generator
15
+ from contextlib import contextmanager, suppress
16
+ from datetime import UTC, datetime
17
+ from typing import Any
18
+
19
+ import js
20
+ from pyodide.ffi import (
21
+ JsException,
22
+ JsProxy,
23
+ create_once_callable,
24
+ create_proxy,
25
+ jsnull,
26
+ to_js,
27
+ )
28
+ from workers import Request, Response, python_from_rpc, python_to_rpc
29
+
30
+ __all__ = (
31
+ "ConsoleHandler",
32
+ "JsException",
33
+ "JsProxy",
34
+ "ProxyScope",
35
+ "abort_without_alarm_retry",
36
+ "block_concurrency_while",
37
+ "call_rpc",
38
+ "clone_request",
39
+ "env_binding_names",
40
+ "export_names",
41
+ "facet_abort",
42
+ "facet_delete",
43
+ "facet_fetch",
44
+ "facet_get",
45
+ "from_rpc",
46
+ "has_namespace",
47
+ "js_to_py",
48
+ "make_request",
49
+ "namespace_stub",
50
+ "proxies",
51
+ "py_to_js",
52
+ "read_attachment",
53
+ "request_with",
54
+ "send_message",
55
+ "streaming_response",
56
+ "to_rpc",
57
+ "unwrap",
58
+ "websocket_error_response",
59
+ "websocket_pair",
60
+ "websocket_rejection",
61
+ "with_headers",
62
+ "write_attachment",
63
+ )
64
+
65
+ _TYPED_ARRAY_TAGS = frozenset(
66
+ {
67
+ "[object ArrayBuffer]",
68
+ "[object Uint8Array]",
69
+ "[object Int8Array]",
70
+ "[object Uint8ClampedArray]",
71
+ "[object Uint16Array]",
72
+ "[object Int16Array]",
73
+ "[object Uint32Array]",
74
+ "[object Int32Array]",
75
+ "[object Float32Array]",
76
+ "[object Float64Array]",
77
+ "[object DataView]",
78
+ }
79
+ )
80
+
81
+
82
+ def py_to_js(value: Any) -> Any:
83
+ """Convert a Python value to a structured-clone JS value.
84
+
85
+ Parameters
86
+ ----------
87
+ value
88
+ ``None``, ``bool``, ``int``, ``float``, ``str``, ``bytes`` (or
89
+ ``bytearray`` / ``memoryview``), a timezone-aware ``datetime``, or a
90
+ ``list`` / ``dict`` (with ``str`` keys) of these.
91
+
92
+ Returns
93
+ -------
94
+ Any
95
+ The JS value: ``None`` becomes ``null``, a ``dict`` a plain object, a
96
+ ``list`` an array, bytes a ``Uint8Array`` copy, a ``datetime`` a
97
+ ``Date``.
98
+
99
+ Raises
100
+ ------
101
+ TypeError
102
+ For any other type (tuples, sets, objects), a non-``str`` dict key, or
103
+ a naive ``datetime``.
104
+ """
105
+ return to_js(
106
+ _prepare(value),
107
+ dict_converter=js.Object.fromEntries,
108
+ create_pyproxies=False,
109
+ )
110
+
111
+
112
+ def _prepare(value: Any) -> Any:
113
+ """Replace values `to_js` would get wrong, and reject unsupported types."""
114
+ if value is None:
115
+ return jsnull
116
+ if isinstance(value, bool | int | float | str):
117
+ return value
118
+ if isinstance(value, bytes | bytearray | memoryview):
119
+ return bytes(value)
120
+ if isinstance(value, datetime):
121
+ return _js_date(value)
122
+ if isinstance(value, list):
123
+ return [_prepare(item) for item in value]
124
+ if isinstance(value, dict):
125
+ return {_require_str_key(key): _prepare(item) for key, item in value.items()}
126
+ raise TypeError(f"{type(value).__name__} can't be stored as a JS value")
127
+
128
+
129
+ def _require_str_key(key: object) -> str:
130
+ if not isinstance(key, str):
131
+ raise TypeError(f"dict keys must be str, not {type(key).__name__}")
132
+ return key
133
+
134
+
135
+ def _js_date(value: datetime) -> JsProxy:
136
+ if value.tzinfo is None:
137
+ raise TypeError("naive datetime; use a timezone-aware datetime")
138
+ return js.Date.new(value.timestamp() * 1000)
139
+
140
+
141
+ def js_to_py(value: Any) -> Any:
142
+ """Convert a structured-clone JS value to Python.
143
+
144
+ Parameters
145
+ ----------
146
+ value
147
+ A JS value, as returned by storage, SQL, or an attachment read.
148
+
149
+ Returns
150
+ -------
151
+ Any
152
+ ``None`` for ``null`` / ``undefined``; ``dict`` for a plain object;
153
+ ``list`` for an array; ``bytes`` for an ``ArrayBuffer`` or typed array;
154
+ a UTC ``datetime`` for a ``Date``; primitives unchanged.
155
+
156
+ Raises
157
+ ------
158
+ TypeError
159
+ For any other JS object (functions, maps, class instances).
160
+ """
161
+ if value is jsnull or value is None:
162
+ return None
163
+ if not isinstance(value, JsProxy):
164
+ return value
165
+ tag = str(js.Object.prototype.toString.call(value))
166
+ if tag == "[object Array]":
167
+ return [js_to_py(item) for item in value]
168
+ if tag == "[object Object]":
169
+ return {str(key): js_to_py(item) for key, item in js.Object.entries(value)}
170
+ if tag in _TYPED_ARRAY_TAGS:
171
+ return value.to_bytes()
172
+ if tag == "[object Date]":
173
+ return datetime.fromtimestamp(value.getTime() / 1000, UTC)
174
+ raise TypeError(f"can't convert JS {tag} to a Python value")
175
+
176
+
177
+ def to_rpc(value: Any) -> Any:
178
+ """Convert a value for native DO RPC (the Workers SDK's ``python_to_rpc``)."""
179
+ return python_to_rpc(value)
180
+
181
+
182
+ def from_rpc(value: Any) -> Any:
183
+ """Convert a native DO RPC result (the Workers SDK's ``python_from_rpc``)."""
184
+ return python_from_rpc(value)
185
+
186
+
187
+ def unwrap(value: Any) -> Any:
188
+ """Return the raw JS object behind a Workers SDK binding wrapper.
189
+
190
+ The SDK wraps ``ctx.storage`` (and bindings in ``env``) to convert every
191
+ call; hot paths use the raw object with `py_to_js` / `js_to_py` instead
192
+ (``.design/utilities.md`` §3.8). Relies on the SDK's private ``_binding``
193
+ attribute.
194
+ """
195
+ return getattr(value, "_binding", value)
196
+
197
+
198
+ class ProxyScope:
199
+ """Proxies created for JS callbacks, destroyed together when the scope ends."""
200
+
201
+ __slots__ = ("_proxies",)
202
+
203
+ def __init__(self) -> None:
204
+ self._proxies: list[JsProxy] = []
205
+
206
+ def proxy(self, fn: Callable[..., Any]) -> JsProxy:
207
+ """Wrap ``fn`` so JS can call it until the scope ends.
208
+
209
+ Parameters
210
+ ----------
211
+ fn
212
+ The Python callable to expose to JS.
213
+
214
+ Returns
215
+ -------
216
+ JsProxy
217
+ The proxy to pass to JS.
218
+ """
219
+ handle = create_proxy(fn)
220
+ self._proxies.append(handle)
221
+ return handle
222
+
223
+ def rpc(self, fn: Callable[..., Any]) -> JsProxy:
224
+ """Like `proxy`, for a function another object calls over RPC.
225
+
226
+ Its arguments arrive as JS values and are converted to Python first;
227
+ its result is converted back. ``fn`` must be synchronous.
228
+ """
229
+
230
+ def call(*args: Any) -> Any:
231
+ return to_rpc(fn(*(from_rpc(arg) for arg in args)))
232
+
233
+ return self.proxy(call)
234
+
235
+ def _destroy(self) -> None:
236
+ proxies, self._proxies = self._proxies, []
237
+ for handle in proxies:
238
+ handle.destroy()
239
+
240
+
241
+ @contextmanager
242
+ def proxies() -> Generator[ProxyScope]:
243
+ """Create JS callback proxies that are destroyed when the block exits.
244
+
245
+ Yields
246
+ ------
247
+ ProxyScope
248
+ Use `ProxyScope.proxy` to wrap each callback.
249
+ """
250
+ scope = ProxyScope()
251
+ try:
252
+ yield scope
253
+ finally:
254
+ scope._destroy()
255
+
256
+
257
+ async def block_concurrency_while[T](ctx: Any, fn: Callable[[], Awaitable[T]]) -> T:
258
+ """Run ``fn`` under ``ctx.blockConcurrencyWhile``, holding back other events.
259
+
260
+ ``fn`` must not raise: an exception inside the callback leaves the object
261
+ unusable (``.design/platform_verification.md`` §2.9), so callers catch
262
+ inside ``fn`` and re-raise after this returns.
263
+ """
264
+ with proxies() as scope:
265
+ return await ctx.blockConcurrencyWhile(scope.proxy(fn))
266
+
267
+
268
+ def abort_without_alarm_retry(ctx: Any, reason: str) -> None:
269
+ """Reset the object on the next tick, without the platform retrying an alarm.
270
+
271
+ The Workers SDK's ``ctx.abort(reason)`` takes no options, so this calls the
272
+ raw JS ``abort(reason, {retryAlarm: false})`` through the SDK's private
273
+ ``_ctx``; deferred so the current invocation settles first
274
+ (``.design/platform_verification.md`` §7.5).
275
+ """
276
+ raw = getattr(ctx, "_ctx", ctx)
277
+ options = py_to_js({"retryAlarm": False})
278
+ js.setTimeout(create_once_callable(lambda: raw.abort(reason, options)), 0)
279
+
280
+
281
+ def websocket_error_response(message: str) -> Any:
282
+ """Answer a failed WebSocket upgrade with a socket that reports the error.
283
+
284
+ Browser devtools don't show an HTTP error body for an upgrade, so (as
285
+ upstream) the error is sent as a frame and the socket closed with 1011.
286
+ """
287
+ client, server = js.WebSocketPair.new().object_values()
288
+ server.accept()
289
+ server.send(json.dumps({"error": message}))
290
+ server.close(1011, "Uncaught exception during session setup")
291
+ return Response(None, status=101, web_socket=client)
292
+
293
+
294
+ def websocket_pair() -> tuple[JsProxy, JsProxy]:
295
+ """Create a ``WebSocketPair``; return its ``(client, server)`` ends."""
296
+ client, server = js.WebSocketPair.new().object_values()
297
+ return client, server
298
+
299
+
300
+ def read_attachment(ws: JsProxy) -> Any:
301
+ """Return a socket's hibernation attachment as Python (``None`` if unset)."""
302
+ return js_to_py(ws.deserializeAttachment())
303
+
304
+
305
+ def write_attachment(ws: JsProxy, value: Any) -> None:
306
+ """Replace a socket's hibernation attachment (structured clone, 16 KiB max).
307
+
308
+ Raises
309
+ ------
310
+ JsException
311
+ If the serialized attachment exceeds the runtime's limit.
312
+ """
313
+ ws.serializeAttachment(py_to_js(value))
314
+
315
+
316
+ def send_message(ws: JsProxy, message: str | bytes) -> None:
317
+ """Send a text or binary frame.
318
+
319
+ Raises
320
+ ------
321
+ JsException
322
+ If the socket is no longer open.
323
+ """
324
+ ws.send(message if isinstance(message, str) else py_to_js(message))
325
+
326
+
327
+ class ConsoleHandler(logging.Handler):
328
+ """Write log records with the JS console method matching their level.
329
+
330
+ Python's own output reaches Workers Logs at level ``error`` whatever the
331
+ record's level; this keeps levels intact (``.design/observability.md``
332
+ §6). Installed only on the SDK's ``agents`` logger.
333
+ """
334
+
335
+ def emit(self, record: logging.LogRecord) -> None:
336
+ """Write ``record`` to ``console.error``/``warn``/``info``/``debug``."""
337
+ message = self.format(record)
338
+ if record.levelno >= logging.ERROR:
339
+ js.console.error(message)
340
+ elif record.levelno >= logging.WARNING:
341
+ js.console.warn(message)
342
+ elif record.levelno >= logging.INFO:
343
+ js.console.info(message)
344
+ else:
345
+ js.console.debug(message)
346
+
347
+
348
+ def _install_console_handler() -> None:
349
+ logger = logging.getLogger("agents")
350
+ if not any(isinstance(handler, ConsoleHandler) for handler in logger.handlers):
351
+ logger.addHandler(ConsoleHandler())
352
+
353
+
354
+ # This module only loads on Workers, so the handler is only installed there.
355
+ _install_console_handler()
356
+
357
+
358
+ def env_binding_names(env: Any) -> list[str]:
359
+ """Return the binding names on a Worker's ``env`` (the SDK's wrapper)."""
360
+ return [str(name) for name in js.Object.keys(env._env)]
361
+
362
+
363
+ def clone_request(request: Request) -> Request:
364
+ """Return a copy of ``request`` whose body can be read independently."""
365
+ return Request(request.js_object.clone())
366
+
367
+
368
+ def with_headers(response: Response, headers: dict[str, str]) -> Response:
369
+ """Return ``response`` with ``headers`` set (fetched responses are immutable)."""
370
+ copy = js.Response.new(response.js_object.body, response.js_object)
371
+ for name, value in headers.items():
372
+ copy.headers.set(name, value)
373
+ return Response(copy)
374
+
375
+
376
+ def streaming_response(
377
+ pieces: AsyncIterator[str], *, headers: dict[str, str]
378
+ ) -> Response:
379
+ """Return a response whose body is ``pieces``, pulled as the client reads.
380
+
381
+ Each piece is UTF-8 encoded as it's sent, so the body is never held whole.
382
+ The iterator is closed if the client goes away.
383
+ """
384
+ encoder = js.TextEncoder.new()
385
+ callbacks: list[JsProxy] = []
386
+
387
+ def release() -> None:
388
+ while callbacks:
389
+ callbacks.pop().destroy()
390
+
391
+ async def pull(controller: Any) -> None:
392
+ try:
393
+ piece = await anext(pieces)
394
+ except StopAsyncIteration:
395
+ controller.close()
396
+ release()
397
+ return
398
+ controller.enqueue(encoder.encode(piece))
399
+
400
+ async def cancel(_reason: Any) -> None:
401
+ release()
402
+ await pieces.aclose() # ty: ignore[unresolved-attribute]
403
+
404
+ callbacks.extend((create_proxy(pull), create_proxy(cancel)))
405
+ source = to_js(
406
+ {"pull": callbacks[0], "cancel": callbacks[1]},
407
+ dict_converter=js.Object.fromEntries,
408
+ )
409
+ return Response(js.ReadableStream.new(source), headers=headers)
410
+
411
+
412
+ # Facets (sub-agents; .design/subagents_engine.md §1)
413
+
414
+
415
+ def _raw_ctx(ctx: Any) -> Any:
416
+ """Return the JS ``DurableObjectState`` behind the SDK's context wrapper."""
417
+ return getattr(ctx, "_ctx", ctx)
418
+
419
+
420
+ def export_names(ctx: Any) -> list[str]:
421
+ """Return the names the Worker module exports (``ctx.exports``)."""
422
+ return [str(name) for name in js.Object.keys(_raw_ctx(ctx).exports)]
423
+
424
+
425
+ def has_namespace(ctx: Any, class_name: str) -> bool:
426
+ """Return whether ``class_name`` is exported as a Durable Object namespace."""
427
+ exported = getattr(_raw_ctx(ctx).exports, class_name, None)
428
+ return exported is not None and hasattr(exported, "idFromName")
429
+
430
+
431
+ def namespace_stub(ctx: Any, class_name: str, name: str) -> Any:
432
+ """Return a stub for ``class_name``'s instance ``name`` (via ``ctx.exports``)."""
433
+ namespace = getattr(_raw_ctx(ctx).exports, class_name)
434
+ return namespace.get(namespace.idFromName(name))
435
+
436
+
437
+ def facet_get(
438
+ ctx: Any, key: str, class_name: str, root_class: str, identity: str
439
+ ) -> Any:
440
+ """Return the facet ``key`` (creating it on first use).
441
+
442
+ Its id comes from the root's namespace, so the facet's ``ctx.id.name``
443
+ is ``identity``.
444
+ """
445
+ raw = _raw_ctx(ctx)
446
+
447
+ def getter() -> Any:
448
+ namespace = getattr(raw.exports, root_class)
449
+ return to_js(
450
+ {
451
+ "class": getattr(raw.exports, class_name),
452
+ "id": namespace.idFromName(identity),
453
+ },
454
+ dict_converter=js.Object.fromEntries,
455
+ )
456
+
457
+ # The runtime may call the getter whenever the facet restarts, so the
458
+ # proxy lives as long as the context.
459
+ getters: dict[str, Any] = ctx.__dict__.setdefault("_agents_facet_getters", {})
460
+ getters[key] = create_proxy(getter)
461
+ return raw.facets.get(key, getters[key])
462
+
463
+
464
+ def facet_abort(ctx: Any, key: str, reason: Exception) -> None:
465
+ """Stop facet ``key`` now; pending calls get ``reason``. Storage is kept."""
466
+ _raw_ctx(ctx).facets.abort(key, js.Error.new(str(reason)))
467
+
468
+
469
+ def facet_delete(ctx: Any, key: str) -> None:
470
+ """Stop facet ``key`` and wipe its storage (no-op if it doesn't exist)."""
471
+ with suppress(JsException): # thrown for a key that was never created
472
+ _raw_ctx(ctx).facets.delete(key)
473
+
474
+
475
+ async def call_rpc(stub: Any, method: str, *args: Any) -> Any:
476
+ """Call ``stub.method(*args)`` over native RPC, converting both ways.
477
+
478
+ Arguments go through the SDK's RPC converter: a raw ``dict`` passed to a
479
+ JS function would arrive as a Python proxy and fail to serialize.
480
+ """
481
+ result = await getattr(stub, method)(*(to_rpc(arg) for arg in args))
482
+ return from_rpc(result)
483
+
484
+
485
+ async def facet_fetch(stub: Any, request: Request) -> Response:
486
+ """Send ``request`` to a facet's ``fetch``."""
487
+ return Response(await stub.fetch(request.js_object))
488
+
489
+
490
+ def request_with(
491
+ request: Request, *, url: str | None = None, headers: dict[str, str] | None = None
492
+ ) -> Request:
493
+ """Return a copy of ``request`` with another URL and/or extra headers."""
494
+ copy = js.Request.new(url if url is not None else request.url, request.js_object)
495
+ for name, value in (headers or {}).items():
496
+ copy.headers.set(name, value)
497
+ return Request(copy)
498
+
499
+
500
+ def make_request(url: str, headers: list[list[str]] | None) -> Request:
501
+ """Build a ``GET`` request (a forwarded connection's upgrade request)."""
502
+ init = js.Object.new()
503
+ init.headers = js.Headers.new(to_js(headers or []))
504
+ return Request(js.Request.new(url, init))
505
+
506
+
507
+ def websocket_rejection(code: int, reason: str) -> Response:
508
+ """Answer an upgrade with a socket that closes at once with ``code``.
509
+
510
+ A browser can't read a failed handshake's response, so a refused
511
+ ``/sub/`` connection gets a close frame instead.
512
+ """
513
+ client, server = js.WebSocketPair.new().object_values()
514
+ server.accept()
515
+ server.close(code, reason)
516
+ return Response(None, status=101, web_socket=client)