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,346 @@
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
+ """Tooling for dispatching subagents in parallel."""
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from collections.abc import Iterable
19
+ from concurrent.futures import ThreadPoolExecutor
20
+ from dataclasses import dataclass, field, is_dataclass
21
+ from enum import Enum, auto
22
+ from typing import Any, Final, cast, override
23
+
24
+ from ..prompt import SupportsDataclass
25
+ from ..prompt.composition import DelegationParams, DelegationPrompt, RecapParams
26
+ from ..prompt.errors import PromptRenderError
27
+ from ..prompt.markdown import MarkdownSection
28
+ from ..prompt.prompt import RenderedPrompt
29
+ from ..prompt.protocols import PromptResponseProtocol
30
+ from ..prompt.tool import Tool, ToolContext
31
+ from ..prompt.tool_result import ToolResult
32
+ from ..runtime.events import InProcessEventBus
33
+ from ..runtime.events._types import EventBus
34
+ from ..runtime.session.protocols import SessionProtocol
35
+ from ..serde import dump
36
+
37
+
38
+ class SubagentIsolationLevel(Enum):
39
+ """Isolation modes describing how children interact with parent state."""
40
+
41
+ NO_ISOLATION = auto()
42
+ FULL_ISOLATION = auto()
43
+
44
+
45
+ def _default_max_workers() -> int:
46
+ with ThreadPoolExecutor() as executor:
47
+ return executor._max_workers
48
+
49
+
50
+ _DEFAULT_MAX_WORKERS: Final[int] = _default_max_workers()
51
+
52
+
53
+ @dataclass(
54
+ slots=True,
55
+ )
56
+ class DispatchSubagentsParams:
57
+ """Parameters describing the delegations to execute."""
58
+
59
+ delegations: tuple[DelegationParams, ...] = field(
60
+ default_factory=tuple,
61
+ metadata={
62
+ "description": (
63
+ "Ordered delegations to evaluate. Each entry carries the recap "
64
+ "lines and expected output for a child agent."
65
+ )
66
+ },
67
+ )
68
+
69
+ def __post_init__(self) -> None:
70
+ self.delegations = tuple(self.delegations)
71
+
72
+
73
+ @dataclass(
74
+ slots=True,
75
+ )
76
+ class SubagentResult:
77
+ """Outcome captured for an individual delegation."""
78
+
79
+ output: str = field(
80
+ metadata={
81
+ "description": (
82
+ "Rendered response from the delegated prompt, including parsed "
83
+ "output when structured modes are enabled."
84
+ )
85
+ }
86
+ )
87
+ success: bool = field(
88
+ metadata={
89
+ "description": (
90
+ "Flag indicating whether the delegated run completed without "
91
+ "adapter or rendering errors."
92
+ )
93
+ }
94
+ )
95
+ error: str | None = field(
96
+ default=None,
97
+ metadata={
98
+ "description": (
99
+ "Optional diagnostic message when the delegation fails. Null "
100
+ "when the run succeeds."
101
+ )
102
+ },
103
+ )
104
+
105
+
106
+ def _extract_output_text(response: PromptResponseProtocol[Any]) -> str:
107
+ if response.text:
108
+ return response.text
109
+ if response.output is not None:
110
+ try:
111
+ rendered = dump(response.output, exclude_none=True)
112
+ return json.dumps(rendered, ensure_ascii=False)
113
+ except TypeError:
114
+ return str(response.output)
115
+ return ""
116
+
117
+
118
+ def _build_error(message: str) -> str:
119
+ cleaned = message.strip()
120
+ return cleaned or "Subagent execution failed"
121
+
122
+
123
+ def _clone_session(
124
+ session: SessionProtocol,
125
+ *,
126
+ bus: EventBus,
127
+ ) -> SessionProtocol | None:
128
+ clone_method = getattr(session, "clone", None)
129
+ if not callable(clone_method):
130
+ return None
131
+ return cast(SessionProtocol, clone_method(bus=bus))
132
+
133
+
134
+ def _prepare_child_contexts(
135
+ *,
136
+ delegations: Iterable[DelegationParams],
137
+ session: SessionProtocol,
138
+ bus: EventBus,
139
+ isolation_level: SubagentIsolationLevel,
140
+ ) -> tuple[tuple[SessionProtocol, EventBus], ...] | str:
141
+ if isolation_level is SubagentIsolationLevel.NO_ISOLATION:
142
+ return tuple((session, bus) for _ in delegations)
143
+
144
+ child_pairs: list[tuple[SessionProtocol, EventBus]] = []
145
+ for _ in delegations:
146
+ child_bus = InProcessEventBus()
147
+ try:
148
+ cloned = _clone_session(session, bus=child_bus)
149
+ except Exception as error: # pragma: no cover - defensive
150
+ return _build_error(str(error))
151
+ if cloned is None:
152
+ return "Parent session does not support cloning for full isolation."
153
+ child_pairs.append((cloned, child_bus))
154
+ return tuple(child_pairs)
155
+
156
+
157
+ def build_dispatch_subagents_tool(
158
+ *,
159
+ isolation_level: SubagentIsolationLevel = SubagentIsolationLevel.NO_ISOLATION,
160
+ accepts_overrides: bool = False,
161
+ ) -> Tool[DispatchSubagentsParams, tuple[SubagentResult, ...]]:
162
+ """Return a configured dispatch tool bound to the desired isolation level."""
163
+
164
+ def _dispatch_subagents(
165
+ params: DispatchSubagentsParams,
166
+ *,
167
+ context: ToolContext,
168
+ ) -> ToolResult[tuple[SubagentResult, ...]]:
169
+ rendered_parent = context.rendered_prompt
170
+ if rendered_parent is None:
171
+ return ToolResult(
172
+ message="dispatch_subagents requires the parent prompt to be rendered.",
173
+ value=None,
174
+ success=False,
175
+ )
176
+
177
+ parent_prompt = context.prompt
178
+ parent_output_type = rendered_parent.output_type
179
+ if not isinstance(parent_output_type, type) or not is_dataclass(
180
+ parent_output_type
181
+ ):
182
+ return ToolResult(
183
+ message="Parent prompt must declare a dataclass output type for delegation.",
184
+ value=None,
185
+ success=False,
186
+ )
187
+
188
+ delegation_prompt_cls: type[DelegationPrompt[Any, Any]] = (
189
+ DelegationPrompt.__class_getitem__((parent_output_type, parent_output_type))
190
+ )
191
+ delegation_prompt = delegation_prompt_cls(
192
+ parent_prompt,
193
+ cast(RenderedPrompt[Any], rendered_parent),
194
+ include_response_format=rendered_parent.container is not None,
195
+ )
196
+
197
+ delegations = tuple(params.delegations)
198
+ if not delegations:
199
+ empty_results = cast(tuple[SubagentResult, ...], ())
200
+ return ToolResult(
201
+ message="No delegations supplied.",
202
+ value=empty_results,
203
+ )
204
+
205
+ contexts = _prepare_child_contexts(
206
+ delegations=delegations,
207
+ session=context.session,
208
+ bus=context.event_bus,
209
+ isolation_level=isolation_level,
210
+ )
211
+ if isinstance(contexts, str):
212
+ return ToolResult(
213
+ message=contexts,
214
+ value=None,
215
+ success=False,
216
+ )
217
+
218
+ adapter = context.adapter
219
+ parse_output = rendered_parent.container is not None
220
+ parent_deadline = rendered_parent.deadline
221
+
222
+ def _run_child(
223
+ payload: tuple[DelegationParams, SessionProtocol, EventBus],
224
+ ) -> SubagentResult:
225
+ delegation, child_session, child_bus = payload
226
+ recap = RecapParams(bullets=delegation.recap_lines)
227
+ try:
228
+ response = adapter.evaluate(
229
+ delegation_prompt.prompt,
230
+ delegation,
231
+ recap,
232
+ parse_output=parse_output,
233
+ bus=child_bus,
234
+ session=child_session,
235
+ deadline=parent_deadline,
236
+ )
237
+ except Exception as error: # pragma: no cover - defensive
238
+ return SubagentResult(
239
+ output="",
240
+ success=False,
241
+ error=_build_error(str(error)),
242
+ )
243
+ return SubagentResult(
244
+ output=_extract_output_text(response),
245
+ success=True,
246
+ )
247
+
248
+ payloads = [
249
+ (delegation, child_session, child_bus)
250
+ for delegation, (child_session, child_bus) in zip(
251
+ delegations, contexts, strict=True
252
+ )
253
+ ]
254
+
255
+ max_workers = min(len(payloads), _DEFAULT_MAX_WORKERS) or 1
256
+ results: list[SubagentResult] = []
257
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
258
+ futures = [executor.submit(_run_child, payload) for payload in payloads]
259
+ for future in futures:
260
+ try:
261
+ results.append(future.result())
262
+ except Exception as error: # pragma: no cover - defensive
263
+ results.append(
264
+ SubagentResult(
265
+ output="",
266
+ success=False,
267
+ error=_build_error(str(error)),
268
+ )
269
+ )
270
+
271
+ return ToolResult(
272
+ message=f"Dispatched {len(results)} subagents.",
273
+ value=tuple(results),
274
+ )
275
+
276
+ return Tool[DispatchSubagentsParams, tuple[SubagentResult, ...]](
277
+ name="dispatch_subagents",
278
+ description="Run delegated child prompts in parallel.",
279
+ handler=_dispatch_subagents,
280
+ accepts_overrides=accepts_overrides,
281
+ )
282
+
283
+
284
+ @dataclass(slots=True, frozen=True)
285
+ class _SubagentsSectionParams:
286
+ """Placeholder params container for the subagents section."""
287
+
288
+ pass
289
+
290
+
291
+ _DELEGATION_BODY: Final[str] = (
292
+ "Use `dispatch_subagents` to offload work that can proceed in parallel.\n"
293
+ "Each delegation must include recap bullet points so the parent can audit\n"
294
+ "the child plan. Prefer dispatching concurrent tasks over running them\n"
295
+ "sequentially yourself."
296
+ )
297
+
298
+
299
+ class SubagentsSection(MarkdownSection[_SubagentsSectionParams]):
300
+ """Explain the delegation workflow and expose the dispatch tool."""
301
+
302
+ def __init__(
303
+ self,
304
+ *,
305
+ isolation_level: SubagentIsolationLevel | None = None,
306
+ accepts_overrides: bool = False,
307
+ ) -> None:
308
+ effective_level = (
309
+ isolation_level
310
+ if isolation_level is not None
311
+ else SubagentIsolationLevel.NO_ISOLATION
312
+ )
313
+ tool = build_dispatch_subagents_tool(
314
+ isolation_level=effective_level,
315
+ accepts_overrides=accepts_overrides,
316
+ )
317
+ super().__init__(
318
+ title="Delegation",
319
+ key="subagents",
320
+ template=_DELEGATION_BODY,
321
+ default_params=_SubagentsSectionParams(),
322
+ tools=(tool,),
323
+ accepts_overrides=accepts_overrides,
324
+ )
325
+
326
+ @override
327
+ def render(self, params: SupportsDataclass | None, depth: int) -> str:
328
+ if not isinstance(params, _SubagentsSectionParams):
329
+ raise PromptRenderError(
330
+ "Subagents section requires parameters.",
331
+ dataclass_type=_SubagentsSectionParams,
332
+ )
333
+ return super().render(params, depth)
334
+
335
+
336
+ dispatch_subagents = build_dispatch_subagents_tool()
337
+
338
+
339
+ __all__ = [
340
+ "DispatchSubagentsParams",
341
+ "SubagentIsolationLevel",
342
+ "SubagentResult",
343
+ "SubagentsSection",
344
+ "build_dispatch_subagents_tool",
345
+ "dispatch_subagents",
346
+ ]