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.
- agents/__init__.py +175 -0
- agents/_ffi.py +516 -0
- agents/agent/__init__.py +31 -0
- agents/agent/agent.py +1201 -0
- agents/agent/current.py +34 -0
- agents/agent/errors.py +12 -0
- agents/agent/routing.py +343 -0
- agents/agent/types.py +139 -0
- agents/chat/__init__.py +73 -0
- agents/chat/_json.py +16 -0
- agents/chat/accumulator.py +254 -0
- agents/chat/agent.py +1565 -0
- agents/chat/builder.py +436 -0
- agents/chat/chunks.py +369 -0
- agents/chat/codec.py +241 -0
- agents/chat/concurrency.py +148 -0
- agents/chat/errors.py +17 -0
- agents/chat/handshake.py +265 -0
- agents/chat/messages.py +312 -0
- agents/chat/persistence.py +56 -0
- agents/chat/protocol.py +248 -0
- agents/chat/reconciler.py +287 -0
- agents/chat/repair.py +128 -0
- agents/chat/resumable_stream.py +653 -0
- agents/chat/terminal.py +51 -0
- agents/chat/tool_state.py +238 -0
- agents/chat/turn_queue.py +118 -0
- agents/chat/types.py +112 -0
- agents/core/__init__.py +24 -0
- agents/core/encoding.py +56 -0
- agents/core/errors.py +31 -0
- agents/core/events.py +95 -0
- agents/core/methods.py +81 -0
- agents/core/naming.py +21 -0
- agents/core/platform_errors.py +110 -0
- agents/core/records.py +20 -0
- agents/core/retry.py +143 -0
- agents/core/sql.py +100 -0
- agents/core/timing.py +74 -0
- agents/core/types.py +50 -0
- agents/dynamic_agents/__init__.py +18 -0
- agents/dynamic_agents/api.py +77 -0
- agents/dynamic_agents/connections.py +130 -0
- agents/dynamic_agents/dynamic_agents.py +997 -0
- agents/dynamic_agents/errors.py +12 -0
- agents/dynamic_agents/paths.py +258 -0
- agents/dynamic_agents/registry.py +104 -0
- agents/dynamic_agents/stubs.py +115 -0
- agents/dynamic_agents/types.py +131 -0
- agents/fibers/__init__.py +34 -0
- agents/fibers/errors.py +25 -0
- agents/fibers/fibers.py +898 -0
- agents/fibers/keep_alive.py +197 -0
- agents/fibers/store.py +352 -0
- agents/fibers/types.py +218 -0
- agents/lifecycle/__init__.py +34 -0
- agents/lifecycle/capability.py +132 -0
- agents/lifecycle/capability_runner.py +149 -0
- agents/lifecycle/errors.py +28 -0
- agents/lifecycle/host_context.py +65 -0
- agents/lifecycle/job_driver.py +475 -0
- agents/lifecycle/job_queue.py +497 -0
- agents/lifecycle/lifecycle.py +596 -0
- agents/lifecycle/services.py +144 -0
- agents/lifecycle/types.py +290 -0
- agents/observability/__init__.py +6 -0
- agents/observability/observability.py +88 -0
- agents/observability/types.py +40 -0
- agents/py.typed +0 -0
- agents/queue/__init__.py +6 -0
- agents/queue/queue.py +561 -0
- agents/queue/types.py +121 -0
- agents/schedules/__init__.py +7 -0
- agents/schedules/cron.py +270 -0
- agents/schedules/errors.py +13 -0
- agents/schedules/scheduler.py +754 -0
- agents/schedules/timing.py +96 -0
- agents/schedules/types.py +171 -0
- agents/sessions/__init__.py +32 -0
- agents/sessions/chunking.py +31 -0
- agents/sessions/sanitize.py +66 -0
- agents/sessions/sessions.py +552 -0
- agents/sessions/store.py +331 -0
- agents/sessions/tokens.py +113 -0
- agents/sessions/types.py +119 -0
- agents/state/__init__.py +6 -0
- agents/state/state.py +147 -0
- agents/state/types.py +18 -0
- agents/streams/__init__.py +17 -0
- agents/streams/errors.py +38 -0
- agents/streams/store.py +275 -0
- agents/streams/streams.py +561 -0
- agents/streams/types.py +93 -0
- agents/tasks/__init__.py +54 -0
- agents/tasks/decorator.py +132 -0
- agents/tasks/engine.py +265 -0
- agents/tasks/errors.py +183 -0
- agents/tasks/registry.py +26 -0
- agents/tasks/replay.py +366 -0
- agents/tasks/serialization.py +50 -0
- agents/tasks/store.py +223 -0
- agents/tasks/tasks.py +1116 -0
- agents/tasks/types.py +354 -0
- agents/websockets/__init__.py +19 -0
- agents/websockets/connection.py +187 -0
- agents/websockets/errors.py +31 -0
- agents/websockets/rpc.py +286 -0
- agents/websockets/types.py +123 -0
- agents/websockets/websockets.py +513 -0
- cf_agents-0.1.0.dist-info/METADATA +24 -0
- cf_agents-0.1.0.dist-info/RECORD +112 -0
- 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)
|