weakincentives 0.9.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 (73) hide show
  1. weakincentives/__init__.py +67 -0
  2. weakincentives/adapters/__init__.py +37 -0
  3. weakincentives/adapters/_names.py +32 -0
  4. weakincentives/adapters/_provider_protocols.py +69 -0
  5. weakincentives/adapters/_tool_messages.py +80 -0
  6. weakincentives/adapters/core.py +102 -0
  7. weakincentives/adapters/litellm.py +254 -0
  8. weakincentives/adapters/openai.py +254 -0
  9. weakincentives/adapters/shared.py +1021 -0
  10. weakincentives/cli/__init__.py +23 -0
  11. weakincentives/cli/wink.py +58 -0
  12. weakincentives/dbc/__init__.py +412 -0
  13. weakincentives/deadlines.py +58 -0
  14. weakincentives/prompt/__init__.py +105 -0
  15. weakincentives/prompt/_generic_params_specializer.py +64 -0
  16. weakincentives/prompt/_normalization.py +48 -0
  17. weakincentives/prompt/_overrides_protocols.py +33 -0
  18. weakincentives/prompt/_types.py +34 -0
  19. weakincentives/prompt/chapter.py +146 -0
  20. weakincentives/prompt/composition.py +281 -0
  21. weakincentives/prompt/errors.py +57 -0
  22. weakincentives/prompt/markdown.py +108 -0
  23. weakincentives/prompt/overrides/__init__.py +59 -0
  24. weakincentives/prompt/overrides/_fs.py +164 -0
  25. weakincentives/prompt/overrides/inspection.py +141 -0
  26. weakincentives/prompt/overrides/local_store.py +275 -0
  27. weakincentives/prompt/overrides/validation.py +534 -0
  28. weakincentives/prompt/overrides/versioning.py +269 -0
  29. weakincentives/prompt/prompt.py +353 -0
  30. weakincentives/prompt/protocols.py +103 -0
  31. weakincentives/prompt/registry.py +375 -0
  32. weakincentives/prompt/rendering.py +288 -0
  33. weakincentives/prompt/response_format.py +60 -0
  34. weakincentives/prompt/section.py +166 -0
  35. weakincentives/prompt/structured_output.py +179 -0
  36. weakincentives/prompt/tool.py +397 -0
  37. weakincentives/prompt/tool_result.py +30 -0
  38. weakincentives/py.typed +0 -0
  39. weakincentives/runtime/__init__.py +82 -0
  40. weakincentives/runtime/events/__init__.py +126 -0
  41. weakincentives/runtime/events/_types.py +110 -0
  42. weakincentives/runtime/logging.py +284 -0
  43. weakincentives/runtime/session/__init__.py +46 -0
  44. weakincentives/runtime/session/_slice_types.py +24 -0
  45. weakincentives/runtime/session/_types.py +55 -0
  46. weakincentives/runtime/session/dataclasses.py +29 -0
  47. weakincentives/runtime/session/protocols.py +34 -0
  48. weakincentives/runtime/session/reducer_context.py +40 -0
  49. weakincentives/runtime/session/reducers.py +82 -0
  50. weakincentives/runtime/session/selectors.py +56 -0
  51. weakincentives/runtime/session/session.py +387 -0
  52. weakincentives/runtime/session/snapshots.py +310 -0
  53. weakincentives/serde/__init__.py +19 -0
  54. weakincentives/serde/_utils.py +240 -0
  55. weakincentives/serde/dataclass_serde.py +55 -0
  56. weakincentives/serde/dump.py +189 -0
  57. weakincentives/serde/parse.py +417 -0
  58. weakincentives/serde/schema.py +260 -0
  59. weakincentives/tools/__init__.py +154 -0
  60. weakincentives/tools/_context.py +38 -0
  61. weakincentives/tools/asteval.py +853 -0
  62. weakincentives/tools/errors.py +26 -0
  63. weakincentives/tools/planning.py +831 -0
  64. weakincentives/tools/podman.py +1655 -0
  65. weakincentives/tools/subagents.py +346 -0
  66. weakincentives/tools/vfs.py +1390 -0
  67. weakincentives/types/__init__.py +35 -0
  68. weakincentives/types/json.py +45 -0
  69. weakincentives-0.9.0.dist-info/METADATA +775 -0
  70. weakincentives-0.9.0.dist-info/RECORD +73 -0
  71. weakincentives-0.9.0.dist-info/WHEEL +4 -0
  72. weakincentives-0.9.0.dist-info/entry_points.txt +2 -0
  73. weakincentives-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,397 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ from __future__ import annotations
14
+
15
+ import inspect
16
+ import re
17
+ from collections.abc import Callable
18
+ from collections.abc import Sequence as SequenceABC
19
+ from dataclasses import dataclass, field
20
+ from typing import (
21
+ TYPE_CHECKING,
22
+ Annotated,
23
+ Any,
24
+ Final,
25
+ Literal,
26
+ Protocol,
27
+ TypeVar,
28
+ cast,
29
+ get_args,
30
+ get_origin,
31
+ get_type_hints,
32
+ )
33
+
34
+ from ..deadlines import Deadline
35
+ from ._types import SupportsDataclass, SupportsToolResult
36
+ from .errors import PromptValidationError
37
+ from .tool_result import ToolResult
38
+
39
+ _NAME_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9_-]{1,64}$")
40
+
41
+
42
+ if TYPE_CHECKING:
43
+ from ..runtime.events._types import EventBus
44
+ from ..runtime.session.protocols import SessionProtocol
45
+ from .protocols import (
46
+ PromptProtocol,
47
+ ProviderAdapterProtocol,
48
+ RenderedPromptProtocol,
49
+ )
50
+
51
+ ParamsT_contra = TypeVar("ParamsT_contra", bound=SupportsDataclass, contravariant=True)
52
+ ResultT_co = TypeVar("ResultT_co", bound=SupportsToolResult)
53
+
54
+
55
+ @dataclass(slots=True, frozen=True)
56
+ class ToolContext:
57
+ """Immutable container exposing prompt execution state to handlers."""
58
+
59
+ prompt: PromptProtocol[Any]
60
+ rendered_prompt: RenderedPromptProtocol[Any] | None
61
+ adapter: ProviderAdapterProtocol[Any]
62
+ session: SessionProtocol
63
+ event_bus: EventBus
64
+ deadline: Deadline | None = None
65
+
66
+
67
+ def _normalize_specialization(item: object) -> tuple[object, object]:
68
+ if not isinstance(item, tuple):
69
+ raise TypeError("Tool[...] expects two type arguments (ParamsT, ResultT).")
70
+ normalized = cast(SequenceABC[object], item)
71
+ if len(normalized) != 2:
72
+ raise TypeError("Tool[...] expects two type arguments (ParamsT, ResultT).")
73
+ return normalized[0], normalized[1]
74
+
75
+
76
+ class ToolHandler(Protocol[ParamsT_contra, ResultT_co]):
77
+ """Callable protocol implemented by tool handlers."""
78
+
79
+ def __call__(
80
+ self, params: ParamsT_contra, *, context: ToolContext
81
+ ) -> ToolResult[ResultT_co]: ...
82
+
83
+
84
+ @dataclass(slots=True)
85
+ class Tool[ParamsT: SupportsDataclass, ResultT: SupportsToolResult]:
86
+ """Describe a callable tool exposed by prompt sections."""
87
+
88
+ name: str
89
+ description: str
90
+ handler: ToolHandler[ParamsT, ResultT] | None
91
+ params_type: type[ParamsT] = field(init=False, repr=False)
92
+ result_type: type[SupportsDataclass] = field(init=False, repr=False)
93
+ result_container: Literal["object", "array"] = field(
94
+ init=False,
95
+ repr=False,
96
+ )
97
+ _result_annotation: ResultT = field(init=False, repr=False)
98
+ accepts_overrides: bool = True
99
+
100
+ def __post_init__(self) -> None:
101
+ params_attr = getattr(self, "params_type", None)
102
+ params_type: type[SupportsDataclass] | None = (
103
+ params_attr if isinstance(params_attr, type) else None
104
+ )
105
+ raw_result_annotation = getattr(self, "_result_annotation", None)
106
+ if params_type is None or raw_result_annotation is None:
107
+ origin = getattr(self, "__orig_class__", None)
108
+ if origin is not None: # pragma: no cover - interpreter-specific path
109
+ args = get_args(origin)
110
+ if len(args) == 2:
111
+ params_arg, result_arg = args
112
+ if isinstance(params_arg, type):
113
+ params_type = params_arg
114
+ raw_result_annotation = cast(ResultT, result_arg)
115
+ if params_type is None or raw_result_annotation is None:
116
+ raise PromptValidationError(
117
+ "Tool must be instantiated with concrete type arguments.",
118
+ placeholder="type_arguments",
119
+ )
120
+
121
+ result_type, result_container = self._normalize_result_annotation(
122
+ raw_result_annotation,
123
+ params_type,
124
+ )
125
+
126
+ self.params_type = cast(type[ParamsT], params_type)
127
+ self.result_type = result_type
128
+ self.result_container = result_container
129
+ self._result_annotation = raw_result_annotation
130
+
131
+ raw_name = self.name
132
+ stripped_name = raw_name.strip()
133
+ if raw_name != stripped_name:
134
+ normalized_name = stripped_name
135
+ raise PromptValidationError(
136
+ "Tool name must not contain surrounding whitespace.",
137
+ dataclass_type=params_type,
138
+ placeholder=normalized_name,
139
+ )
140
+
141
+ name_clean = raw_name
142
+ if not name_clean:
143
+ raise PromptValidationError(
144
+ "Tool name must match the OpenAI function name constraints (1-64 lowercase ASCII letters, digits, underscores, or hyphens).",
145
+ dataclass_type=params_type,
146
+ placeholder=stripped_name,
147
+ )
148
+ if len(name_clean) > 64 or not _NAME_PATTERN.fullmatch(name_clean):
149
+ raise PromptValidationError(
150
+ "Tool name must match the OpenAI function name constraints (pattern: ^[a-z0-9_-]{1,64}$).",
151
+ dataclass_type=params_type,
152
+ placeholder=name_clean,
153
+ )
154
+
155
+ description_clean = self.description.strip()
156
+ if not description_clean or len(description_clean) > 200:
157
+ raise PromptValidationError(
158
+ "Tool description must be 1-200 ASCII characters.",
159
+ dataclass_type=params_type,
160
+ placeholder="description",
161
+ )
162
+ try:
163
+ _ = description_clean.encode("ascii")
164
+ except UnicodeEncodeError as error:
165
+ raise PromptValidationError(
166
+ "Tool description must be ASCII.",
167
+ dataclass_type=params_type,
168
+ placeholder="description",
169
+ ) from error
170
+
171
+ handler = self.handler
172
+ if handler is not None:
173
+ self._validate_handler(
174
+ handler,
175
+ params_type,
176
+ raw_result_annotation,
177
+ )
178
+
179
+ self.name = name_clean
180
+ self.description = description_clean
181
+
182
+ def _validate_handler(
183
+ self,
184
+ handler: object,
185
+ params_type: type[SupportsDataclass],
186
+ result_annotation: object,
187
+ ) -> None:
188
+ if not callable(handler):
189
+ raise PromptValidationError(
190
+ "Tool handler must be callable.",
191
+ dataclass_type=params_type,
192
+ placeholder="handler",
193
+ )
194
+
195
+ callable_handler = cast(Callable[..., ToolResult[ResultT]], handler)
196
+ signature = inspect.signature(callable_handler)
197
+ parameters = list(signature.parameters.values())
198
+
199
+ if len(parameters) != 2:
200
+ raise PromptValidationError(
201
+ "Tool handler must accept exactly one positional argument and the keyword-only 'context' parameter.",
202
+ dataclass_type=params_type,
203
+ placeholder="handler",
204
+ )
205
+
206
+ parameter = parameters[0]
207
+ context_parameter = parameters[1]
208
+
209
+ if parameter.kind not in (
210
+ inspect.Parameter.POSITIONAL_ONLY,
211
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
212
+ ):
213
+ raise PromptValidationError(
214
+ "Tool handler parameter must be positional.",
215
+ dataclass_type=params_type,
216
+ placeholder="handler",
217
+ )
218
+
219
+ if context_parameter.kind is not inspect.Parameter.KEYWORD_ONLY:
220
+ raise PromptValidationError(
221
+ "Tool handler must declare a keyword-only 'context' parameter.",
222
+ dataclass_type=params_type,
223
+ placeholder="handler",
224
+ )
225
+ if context_parameter.default is not inspect.Signature.empty:
226
+ raise PromptValidationError(
227
+ "Tool handler 'context' parameter must not define a default value.",
228
+ dataclass_type=params_type,
229
+ placeholder="handler",
230
+ )
231
+ if context_parameter.name != "context":
232
+ raise PromptValidationError(
233
+ "Tool handler must name the keyword-only context parameter 'context'.",
234
+ dataclass_type=params_type,
235
+ placeholder="handler",
236
+ )
237
+
238
+ try:
239
+ hints = get_type_hints(callable_handler, include_extras=True)
240
+ except Exception:
241
+ hints = {}
242
+
243
+ annotation = hints.get(parameter.name, parameter.annotation)
244
+ if annotation is inspect.Parameter.empty:
245
+ raise PromptValidationError(
246
+ "Tool handler parameter must be annotated with ParamsT.",
247
+ dataclass_type=params_type,
248
+ placeholder="handler",
249
+ )
250
+ if get_origin(annotation) is Annotated:
251
+ annotation = get_args(annotation)[0]
252
+ if annotation is not params_type:
253
+ raise PromptValidationError(
254
+ "Tool handler parameter annotation must match ParamsT.",
255
+ dataclass_type=params_type,
256
+ placeholder="handler",
257
+ )
258
+
259
+ context_annotation = hints.get(
260
+ context_parameter.name, context_parameter.annotation
261
+ )
262
+ if context_annotation is inspect.Parameter.empty:
263
+ raise PromptValidationError(
264
+ "Tool handler must annotate the 'context' parameter with ToolContext.",
265
+ dataclass_type=params_type,
266
+ placeholder="handler",
267
+ )
268
+ if get_origin(context_annotation) is Annotated:
269
+ context_annotation = get_args(context_annotation)[0]
270
+ if context_annotation is not ToolContext:
271
+ raise PromptValidationError(
272
+ "Tool handler 'context' annotation must be ToolContext.",
273
+ dataclass_type=params_type,
274
+ placeholder="handler",
275
+ )
276
+
277
+ return_annotation = hints.get("return", signature.return_annotation)
278
+ if return_annotation is inspect.Signature.empty:
279
+ raise PromptValidationError(
280
+ "Tool handler must annotate its return value with ToolResult[ResultT].",
281
+ dataclass_type=params_type,
282
+ placeholder="return",
283
+ )
284
+ if get_origin(return_annotation) is Annotated:
285
+ return_annotation = get_args(return_annotation)[0]
286
+
287
+ origin = get_origin(return_annotation)
288
+ if origin is ToolResult:
289
+ result_args_raw = get_args(return_annotation)
290
+ if result_args_raw and self._matches_result_annotation(
291
+ result_args_raw[0],
292
+ result_annotation,
293
+ ):
294
+ return
295
+ raise PromptValidationError(
296
+ "Tool handler return annotation must be ToolResult[ResultT].",
297
+ dataclass_type=params_type,
298
+ placeholder="return",
299
+ )
300
+
301
+ @staticmethod
302
+ def _normalize_result_annotation(
303
+ annotation: ResultT,
304
+ params_type: type[SupportsDataclass],
305
+ ) -> tuple[type[SupportsDataclass], Literal["object", "array"]]:
306
+ if isinstance(annotation, type):
307
+ return cast(type[SupportsDataclass], annotation), "object"
308
+
309
+ origin = get_origin(annotation)
310
+ if origin not in {list, tuple, SequenceABC}:
311
+ raise PromptValidationError(
312
+ "Tool ResultT must be a dataclass type or a sequence of dataclasses.",
313
+ dataclass_type=params_type,
314
+ placeholder="ResultT",
315
+ )
316
+
317
+ args = get_args(annotation)
318
+ element: object | None = None
319
+ if origin is tuple:
320
+ if len(args) != 2 or args[1] is not Ellipsis:
321
+ raise PromptValidationError(
322
+ "Variadic Tuple[ResultT, ...] is required for Tool sequence results.",
323
+ dataclass_type=params_type,
324
+ placeholder="ResultT",
325
+ )
326
+ element = args[0]
327
+ elif len(args) == 1:
328
+ element = args[0]
329
+
330
+ if not isinstance(element, type):
331
+ raise PromptValidationError(
332
+ "Tool ResultT must be a dataclass type or a sequence of dataclasses.",
333
+ dataclass_type=params_type,
334
+ placeholder="ResultT",
335
+ )
336
+
337
+ return cast(type[SupportsDataclass], element), "array"
338
+
339
+ @staticmethod
340
+ def _matches_result_annotation(candidate: object, expected: object) -> bool:
341
+ if candidate is expected:
342
+ return True
343
+
344
+ candidate_origin = get_origin(candidate)
345
+ expected_origin = get_origin(expected)
346
+
347
+ if candidate_origin is None or expected_origin is None:
348
+ return False
349
+
350
+ sequence_origins = {list, tuple, SequenceABC}
351
+ if candidate_origin in sequence_origins and expected_origin in sequence_origins:
352
+ candidate_args = get_args(candidate)
353
+ expected_args = get_args(expected)
354
+ candidate_element = (
355
+ candidate_args[0]
356
+ if candidate_origin is not tuple
357
+ else candidate_args[0]
358
+ if len(candidate_args) == 2
359
+ else None
360
+ )
361
+ expected_element = (
362
+ expected_args[0]
363
+ if expected_origin is not tuple
364
+ else expected_args[0]
365
+ if len(expected_args) == 2
366
+ else None
367
+ )
368
+ return candidate_element is expected_element
369
+
370
+ return False
371
+
372
+ @classmethod
373
+ def __class_getitem__(
374
+ cls, item: object
375
+ ) -> type[Tool[SupportsDataclass, SupportsToolResult]]:
376
+ params_candidate, result_candidate = _normalize_specialization(item)
377
+ if not isinstance(params_candidate, type):
378
+ raise TypeError("Tool ParamsT type argument must be a type.")
379
+ params_type = cast(type[SupportsDataclass], params_candidate)
380
+ result_annotation = cast(ResultT, result_candidate)
381
+
382
+ class _SpecializedTool(cls):
383
+ def __post_init__(self) -> None:
384
+ self.params_type = cast(type[ParamsT], params_type)
385
+ self._result_annotation = result_annotation
386
+ super().__post_init__()
387
+
388
+ _SpecializedTool.__name__ = cls.__name__
389
+ _SpecializedTool.__qualname__ = cls.__qualname__
390
+ _SpecializedTool.__module__ = cls.__module__
391
+ return cast(
392
+ "type[Tool[SupportsDataclass, SupportsToolResult]]",
393
+ _SpecializedTool,
394
+ )
395
+
396
+
397
+ __all__ = ["Tool", "ToolContext", "ToolHandler", "ToolResult"]
@@ -0,0 +1,30 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ """Shared result container returned by tool handlers."""
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class ToolResult[ResultValueT]:
22
+ """Structured response emitted by a tool handler."""
23
+
24
+ message: str
25
+ value: ResultValueT | None
26
+ success: bool = True
27
+ exclude_value_from_context: bool = False
28
+
29
+
30
+ __all__ = ["ToolResult"]
File without changes
@@ -0,0 +1,82 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ """Runtime primitives for :mod:`weakincentives`."""
14
+
15
+ from __future__ import annotations
16
+
17
+ from . import events, session
18
+ from .events import (
19
+ EventBus,
20
+ HandlerFailure,
21
+ InProcessEventBus,
22
+ PromptExecuted,
23
+ PublishResult,
24
+ ToolInvoked,
25
+ )
26
+ from .logging import StructuredLogger, configure_logging, get_logger
27
+ from .session import (
28
+ DataEvent,
29
+ ReducerContext,
30
+ ReducerContextProtocol,
31
+ ReducerEvent,
32
+ Session,
33
+ SessionProtocol,
34
+ Snapshot,
35
+ SnapshotProtocol,
36
+ SnapshotRestoreError,
37
+ SnapshotSerializationError,
38
+ TypedReducer,
39
+ append,
40
+ build_reducer_context,
41
+ replace_latest,
42
+ select_all,
43
+ select_latest,
44
+ select_where,
45
+ upsert_by,
46
+ )
47
+
48
+ __all__ = [
49
+ "DataEvent",
50
+ "EventBus",
51
+ "HandlerFailure",
52
+ "InProcessEventBus",
53
+ "PromptExecuted",
54
+ "PublishResult",
55
+ "ReducerContext",
56
+ "ReducerContextProtocol",
57
+ "ReducerEvent",
58
+ "Session",
59
+ "SessionProtocol",
60
+ "Snapshot",
61
+ "SnapshotProtocol",
62
+ "SnapshotRestoreError",
63
+ "SnapshotSerializationError",
64
+ "StructuredLogger",
65
+ "ToolInvoked",
66
+ "TypedReducer",
67
+ "append",
68
+ "build_reducer_context",
69
+ "configure_logging",
70
+ "events",
71
+ "get_logger",
72
+ "replace_latest",
73
+ "select_all",
74
+ "select_latest",
75
+ "select_where",
76
+ "session",
77
+ "upsert_by",
78
+ ]
79
+
80
+
81
+ def __dir__() -> list[str]:
82
+ return sorted({*globals().keys(), *__all__}) # pragma: no cover - convenience shim
@@ -0,0 +1,126 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ """In-process event primitives for adapter telemetry."""
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime
19
+ from threading import RLock
20
+ from typing import TYPE_CHECKING
21
+ from uuid import UUID, uuid4
22
+
23
+ from ...prompt._types import SupportsDataclass
24
+ from ..logging import StructuredLogger, get_logger
25
+ from ._types import EventBus, EventHandler, HandlerFailure, PublishResult, ToolInvoked
26
+
27
+ if TYPE_CHECKING:
28
+ from ...adapters._names import AdapterName
29
+ from ...adapters.core import PromptResponse
30
+
31
+
32
+ def _describe_handler(handler: EventHandler) -> str:
33
+ module_name = getattr(handler, "__module__", None)
34
+ qualname = getattr(handler, "__qualname__", None)
35
+ if isinstance(qualname, str):
36
+ prefix = f"{module_name}." if isinstance(module_name, str) else ""
37
+ return f"{prefix}{qualname}"
38
+ return repr(handler) # pragma: no cover - defensive fallback
39
+
40
+
41
+ logger: StructuredLogger = get_logger(__name__, context={"component": "event_bus"})
42
+
43
+
44
+ class InProcessEventBus:
45
+ """Process-local event bus that delivers events synchronously."""
46
+
47
+ def __init__(self) -> None:
48
+ super().__init__()
49
+ self._handlers: dict[type[object], list[EventHandler]] = {}
50
+ self._lock = RLock()
51
+
52
+ def subscribe(self, event_type: type[object], handler: EventHandler) -> None:
53
+ with self._lock:
54
+ handlers = self._handlers.setdefault(event_type, [])
55
+ handlers.append(handler)
56
+
57
+ def publish(self, event: object) -> PublishResult:
58
+ with self._lock:
59
+ handlers = tuple(self._handlers.get(type(event), ()))
60
+ invoked: list[EventHandler] = []
61
+ failures: list[HandlerFailure] = []
62
+ for handler in handlers:
63
+ invoked.append(handler)
64
+ try:
65
+ handler(event)
66
+ except Exception as error:
67
+ logger.exception(
68
+ "Error delivering event.",
69
+ event="event_delivery_failed",
70
+ context={
71
+ "handler": _describe_handler(handler),
72
+ "event_type": type(event).__name__,
73
+ },
74
+ )
75
+ failures.append(HandlerFailure(handler=handler, error=error))
76
+
77
+ return PublishResult(
78
+ event=event,
79
+ handlers_invoked=tuple(invoked),
80
+ errors=tuple(failures),
81
+ )
82
+
83
+
84
+ @dataclass(slots=True, frozen=True)
85
+ class PromptExecuted:
86
+ """Event emitted after an adapter finishes evaluating a prompt."""
87
+
88
+ prompt_name: str
89
+ adapter: AdapterName
90
+ result: PromptResponse[object]
91
+ session_id: UUID | None
92
+ created_at: datetime
93
+ value: SupportsDataclass | None = None
94
+ event_id: UUID = field(default_factory=uuid4)
95
+
96
+
97
+ @dataclass(slots=True, frozen=True)
98
+ class PromptRendered:
99
+ """Event emitted immediately before dispatching a rendered prompt."""
100
+
101
+ prompt_ns: str
102
+ prompt_key: str
103
+ prompt_name: str | None
104
+ adapter: AdapterName
105
+ session_id: UUID | None
106
+ render_inputs: tuple[SupportsDataclass, ...]
107
+ rendered_prompt: str
108
+ created_at: datetime
109
+ event_id: UUID = field(default_factory=uuid4)
110
+
111
+ @property
112
+ def value(self) -> SupportsDataclass:
113
+ """Expose the dataclass instance for reducer compatibility."""
114
+
115
+ return self
116
+
117
+
118
+ __all__ = [
119
+ "EventBus",
120
+ "HandlerFailure",
121
+ "InProcessEventBus",
122
+ "PromptExecuted",
123
+ "PromptRendered",
124
+ "PublishResult",
125
+ "ToolInvoked",
126
+ ]