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,64 @@
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
+ from typing import ClassVar, Generic, TypeVar, cast
16
+
17
+ from ._types import SupportsDataclass
18
+
19
+ ParamsT = TypeVar("ParamsT", bound=SupportsDataclass, covariant=True)
20
+
21
+ SelfClass = TypeVar(
22
+ "SelfClass",
23
+ bound="GenericParamsSpecializer[SupportsDataclass]",
24
+ covariant=True,
25
+ )
26
+
27
+
28
+ class GenericParamsSpecializer(Generic[ParamsT]): # noqa: UP046
29
+ """Mixin providing ``ParamsT`` specialization for prompt components."""
30
+
31
+ _params_type: ClassVar[type[SupportsDataclass] | None] = None
32
+ _generic_owner_name: ClassVar[str | None] = None
33
+
34
+ @classmethod
35
+ def __class_getitem__(cls: type[SelfClass], item: object) -> type[SelfClass]:
36
+ params_type = cls._normalize_generic_argument(item)
37
+ specialized = cast(
38
+ "type[SelfClass]",
39
+ type(cls.__name__, (cls,), {}),
40
+ )
41
+ specialized.__name__ = cls.__name__
42
+ specialized.__qualname__ = cls.__qualname__
43
+ specialized.__module__ = cls.__module__
44
+ specialized._params_type = cast(type[SupportsDataclass], params_type)
45
+ return specialized
46
+
47
+ @classmethod
48
+ def _normalize_generic_argument(cls, item: object) -> object:
49
+ if isinstance(item, tuple):
50
+ raise TypeError(f"{cls._owner_name()}[...] expects a single type argument.")
51
+ return item
52
+
53
+ @classmethod
54
+ def _owner_name(cls) -> str:
55
+ owner_name = getattr(cls, "_generic_owner_name", None)
56
+ if isinstance(owner_name, str) and owner_name:
57
+ return owner_name
58
+ name = getattr(cls, "__name__", None)
59
+ if isinstance(name, str) and name:
60
+ return name
61
+ return "Component"
62
+
63
+
64
+ __all__ = ["GenericParamsSpecializer"]
@@ -0,0 +1,48 @@
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 utilities for normalizing prompt component identifiers."""
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from typing import Final
19
+
20
+ COMPONENT_KEY_PATTERN: Final[re.Pattern[str]] = re.compile(
21
+ r"^[a-z0-9][a-z0-9._-]{0,63}$"
22
+ )
23
+
24
+
25
+ def normalize_component_key(key: str, *, owner: str) -> str:
26
+ """Normalize component keys across prompt primitives.
27
+
28
+ Args:
29
+ key: The user supplied identifier that should be normalized.
30
+ owner: Human-friendly label for the component requesting normalization.
31
+
32
+ Returns:
33
+ The sanitized key that downstream consumers may safely use.
34
+
35
+ Raises:
36
+ ValueError: When ``key`` is empty or fails to match
37
+ :data:`COMPONENT_KEY_PATTERN`.
38
+ """
39
+
40
+ normalized = key.strip().lower()
41
+ if not normalized:
42
+ raise ValueError(f"{owner} key must be a non-empty string.")
43
+ if not COMPONENT_KEY_PATTERN.match(normalized):
44
+ raise ValueError(f"{owner} key must match {COMPONENT_KEY_PATTERN.pattern}.")
45
+ return normalized
46
+
47
+
48
+ __all__ = ["COMPONENT_KEY_PATTERN", "normalize_component_key"]
@@ -0,0 +1,33 @@
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
+ """Lightweight interfaces shared between prompt overrides and protocols."""
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING, Protocol
18
+
19
+ if TYPE_CHECKING: # pragma: no cover - typing only
20
+ from .overrides import PromptDescriptor, PromptOverride
21
+
22
+
23
+ class PromptOverridesStoreProtocol(Protocol):
24
+ """Structural interface satisfied by prompt overrides stores."""
25
+
26
+ def resolve(
27
+ self,
28
+ descriptor: PromptDescriptor,
29
+ tag: str = "latest",
30
+ ) -> PromptOverride | None: ...
31
+
32
+
33
+ __all__ = ["PromptOverridesStoreProtocol"]
@@ -0,0 +1,34 @@
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
+ """Internal typing helpers for the :mod:`weakincentives.prompt` package."""
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Sequence
18
+ from dataclasses import Field
19
+ from typing import Any, ClassVar, Protocol, runtime_checkable
20
+
21
+ type DataclassFieldMapping = dict[str, Field[Any]]
22
+
23
+
24
+ @runtime_checkable
25
+ class SupportsDataclass(Protocol):
26
+ """Protocol satisfied by dataclass types and instances."""
27
+
28
+ __dataclass_fields__: ClassVar[DataclassFieldMapping]
29
+
30
+
31
+ SupportsToolResult = SupportsDataclass | Sequence[SupportsDataclass]
32
+
33
+
34
+ __all__ = ["DataclassFieldMapping", "SupportsDataclass", "SupportsToolResult"]
@@ -0,0 +1,146 @@
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
+ """Chapter primitives controlling coarse-grained prompt visibility."""
14
+
15
+ from __future__ import annotations
16
+
17
+ import inspect
18
+ from collections.abc import Callable
19
+ from dataclasses import dataclass, field
20
+ from enum import StrEnum
21
+ from typing import ClassVar, TypeVar, cast
22
+
23
+ from ._generic_params_specializer import GenericParamsSpecializer
24
+ from ._normalization import normalize_component_key
25
+ from ._types import SupportsDataclass
26
+ from .section import Section
27
+
28
+
29
+ class ChaptersExpansionPolicy(StrEnum):
30
+ """Strategies describing how adapters may open prompt chapters."""
31
+
32
+ ALL_INCLUDED = "all_included"
33
+ INTENT_CLASSIFIER = "intent_classifier"
34
+
35
+
36
+ ChapterParamsT = TypeVar("ChapterParamsT", bound=SupportsDataclass, covariant=True)
37
+ EnabledPredicate = Callable[[SupportsDataclass], bool] | Callable[[], bool]
38
+
39
+
40
+ @dataclass
41
+ class Chapter(GenericParamsSpecializer[ChapterParamsT]):
42
+ """Container grouping sections under a shared visibility boundary."""
43
+
44
+ key: str
45
+ title: str
46
+ description: str | None = None
47
+ sections: tuple[Section[SupportsDataclass], ...] = ()
48
+ default_params: ChapterParamsT | None = None
49
+ enabled: EnabledPredicate | None = None
50
+ _enabled_callable: Callable[[SupportsDataclass | None], bool] | None = field(
51
+ init=False, repr=False, default=None
52
+ )
53
+
54
+ _generic_owner_name: ClassVar[str | None] = "Chapter"
55
+
56
+ def __post_init__(self) -> None:
57
+ params_candidate = getattr(self.__class__, "_params_type", None)
58
+ candidate_type = (
59
+ params_candidate if isinstance(params_candidate, type) else None
60
+ )
61
+ params_type = cast(type[SupportsDataclass] | None, candidate_type)
62
+ self.key = self._normalize_key(self.key)
63
+
64
+ self.sections = tuple(self.sections or ())
65
+
66
+ self._enabled_callable = self._normalize_enabled(self.enabled, params_type)
67
+
68
+ if params_type is None:
69
+ if self.default_params is not None:
70
+ raise TypeError(
71
+ "Chapter without parameters cannot define default_params."
72
+ )
73
+ elif self.default_params is not None and not isinstance(
74
+ self.default_params, params_type
75
+ ):
76
+ raise TypeError(
77
+ "Chapter default_params must match the declared ParamsT type."
78
+ )
79
+
80
+ @property
81
+ def params_type(self) -> type[ChapterParamsT] | None:
82
+ params_candidate = getattr(self.__class__, "_params_type", None)
83
+ candidate_type = (
84
+ params_candidate if isinstance(params_candidate, type) else None
85
+ )
86
+ return cast(type[ChapterParamsT] | None, candidate_type)
87
+
88
+ @property
89
+ def param_type(self) -> type[ChapterParamsT] | None:
90
+ return self.params_type
91
+
92
+ def is_enabled(self, params: SupportsDataclass | None) -> bool:
93
+ """Return True when the chapter should open for the provided params."""
94
+
95
+ if self._enabled_callable is None:
96
+ return True
97
+ if params is None and self.param_type is not None:
98
+ raise TypeError("Chapter parameters are required for enabled predicates.")
99
+ return bool(self._enabled_callable(params))
100
+
101
+ @staticmethod
102
+ def _normalize_key(key: str) -> str:
103
+ return normalize_component_key(key, owner="Chapter")
104
+
105
+ @staticmethod
106
+ def _normalize_enabled(
107
+ enabled: EnabledPredicate | None,
108
+ params_type: type[SupportsDataclass] | None,
109
+ ) -> Callable[[SupportsDataclass | None], bool] | None:
110
+ if enabled is None:
111
+ return None
112
+ if params_type is None and not _callable_requires_positional_argument(enabled):
113
+ zero_arg = cast(Callable[[], bool], enabled)
114
+
115
+ def _without_params(_: SupportsDataclass | None) -> bool:
116
+ return bool(zero_arg())
117
+
118
+ return _without_params
119
+
120
+ coerced = cast(Callable[[SupportsDataclass], bool], enabled)
121
+
122
+ def _with_params(value: SupportsDataclass | None) -> bool:
123
+ return bool(coerced(cast(SupportsDataclass, value)))
124
+
125
+ return _with_params
126
+
127
+
128
+ def _callable_requires_positional_argument(callback: EnabledPredicate) -> bool:
129
+ try:
130
+ signature = inspect.signature(callback)
131
+ except (TypeError, ValueError):
132
+ return True
133
+ for parameter in signature.parameters.values():
134
+ if (
135
+ parameter.kind
136
+ in (
137
+ inspect.Parameter.POSITIONAL_ONLY,
138
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
139
+ )
140
+ and parameter.default is inspect.Signature.empty
141
+ ):
142
+ return True
143
+ return False
144
+
145
+
146
+ __all__ = ["Chapter", "ChaptersExpansionPolicy"]
@@ -0,0 +1,281 @@
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
+ """Utilities for composing delegation prompts from rendered parents."""
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Mapping, Sequence
18
+ from dataclasses import dataclass, field, replace
19
+ from types import MappingProxyType
20
+ from typing import Any, ClassVar, Generic, TypeVar, cast, override
21
+
22
+ from ._types import SupportsDataclass
23
+ from .errors import PromptRenderError
24
+ from .markdown import MarkdownSection
25
+ from .prompt import Prompt, RenderedPrompt
26
+ from .protocols import PromptProtocol
27
+ from .response_format import ResponseFormatParams, ResponseFormatSection
28
+ from .section import Section
29
+
30
+ ParentOutputT = TypeVar("ParentOutputT")
31
+ DelegationOutputT = TypeVar("DelegationOutputT")
32
+
33
+
34
+ @dataclass(slots=True)
35
+ class DelegationParams:
36
+ """Delegation summary fields surfaced to the delegated agent."""
37
+
38
+ reason: str
39
+ expected_result: str
40
+ may_delegate_further: str
41
+ recap_lines: tuple[str, ...] = field(default_factory=tuple)
42
+
43
+ def __post_init__(self) -> None:
44
+ self.recap_lines = tuple(self.recap_lines)
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class ParentPromptParams:
49
+ """Container for the verbatim parent prompt body."""
50
+
51
+ body: str
52
+
53
+
54
+ @dataclass(slots=True)
55
+ class RecapParams:
56
+ """Bullet-style recap directives rendered after the parent prompt."""
57
+
58
+ bullets: tuple[str, ...]
59
+
60
+
61
+ class DelegationSummarySection(MarkdownSection[DelegationParams]):
62
+ """Delegation summary rendered as a fixed bullet list."""
63
+
64
+ def __init__(self) -> None:
65
+ super().__init__(
66
+ title="Delegation Summary",
67
+ key="delegation-summary",
68
+ template=(
69
+ "- **Reason** - ${reason}\n"
70
+ "- **Expected result** - ${expected_result}\n"
71
+ "- **May delegate further?** - ${may_delegate_further}"
72
+ ),
73
+ )
74
+
75
+
76
+ class ParentPromptSection(Section[ParentPromptParams]):
77
+ """Embed the parent prompt verbatim between explicit markers."""
78
+
79
+ def __init__(
80
+ self,
81
+ *,
82
+ tools: Sequence[object] | None = None,
83
+ default_params: ParentPromptParams | None = None,
84
+ ) -> None:
85
+ super().__init__(
86
+ title="Parent Prompt (Verbatim)",
87
+ key="parent-prompt",
88
+ tools=tools,
89
+ default_params=default_params,
90
+ )
91
+
92
+ @override
93
+ def render(self, params: SupportsDataclass | None, depth: int) -> str:
94
+ if not isinstance(params, ParentPromptParams):
95
+ raise PromptRenderError(
96
+ "Parent prompt section requires parameters.",
97
+ dataclass_type=ParentPromptParams,
98
+ )
99
+ heading = "#" * (depth + 2)
100
+ prefix = f"{heading} {self.title}"
101
+ body = params.body
102
+ suffix = "" if body.endswith("\n") else "\n"
103
+ return (
104
+ f"{prefix}\n\n"
105
+ "<!-- PARENT PROMPT START -->\n"
106
+ f"{body}{suffix}"
107
+ "<!-- PARENT PROMPT END -->"
108
+ )
109
+
110
+
111
+ class RecapSection(Section[RecapParams]):
112
+ """Render a concise recap of inherited parent prompt directives."""
113
+
114
+ def __init__(self) -> None:
115
+ super().__init__(title="Recap", key="recap")
116
+
117
+ @override
118
+ def render(self, params: SupportsDataclass | None, depth: int) -> str:
119
+ if not isinstance(params, RecapParams):
120
+ raise PromptRenderError(
121
+ "Recap section requires parameters.",
122
+ dataclass_type=RecapParams,
123
+ )
124
+ heading = "#" * (depth + 2)
125
+ prefix = f"{heading} {self.title}"
126
+ bullets = params.bullets
127
+ bullet_lines = "\n".join(f"- {line}" for line in bullets)
128
+ if bullet_lines:
129
+ return f"{prefix}\n\n{bullet_lines}"
130
+ return prefix
131
+
132
+
133
+ class DelegationPrompt(Generic[ParentOutputT, DelegationOutputT]): # noqa: UP046
134
+ """Wrap a rendered parent prompt for subagent delegation."""
135
+
136
+ _parent_output_type: ClassVar[type[Any] | None] = None
137
+ _delegation_output_type: ClassVar[type[Any] | None] = None
138
+
139
+ def __class_getitem__(
140
+ cls, item: tuple[type[Any], type[Any]]
141
+ ) -> type["DelegationPrompt[Any, Any]"]: # noqa: UP037
142
+ parent_output, delegation_output = item
143
+
144
+ name = f"{cls.__name__}[{parent_output.__name__}, {delegation_output.__name__}]"
145
+ namespace = {
146
+ "__module__": cls.__module__,
147
+ "_parent_output_type": parent_output,
148
+ "_delegation_output_type": delegation_output,
149
+ }
150
+ specialized = type(name, (cls,), namespace)
151
+ return cast("type[DelegationPrompt[Any, Any]]", specialized)
152
+
153
+ def __init__(
154
+ self,
155
+ parent_prompt: PromptProtocol[ParentOutputT] | Prompt[ParentOutputT],
156
+ rendered_parent: RenderedPrompt[ParentOutputT],
157
+ *,
158
+ include_response_format: bool = False,
159
+ ) -> None:
160
+ super().__init__()
161
+ self._rendered_parent = rendered_parent
162
+ summary_section = DelegationSummarySection()
163
+ parent_section = ParentPromptSection(
164
+ tools=rendered_parent.tools,
165
+ default_params=ParentPromptParams(body=rendered_parent.text),
166
+ )
167
+ sections: list[Section[SupportsDataclass]] = [summary_section]
168
+
169
+ if include_response_format:
170
+ response_section = self._build_response_format_section(rendered_parent)
171
+ if response_section is not None:
172
+ sections.append(response_section)
173
+
174
+ sections.append(parent_section)
175
+
176
+ self._recap_section = RecapSection()
177
+ sections.append(self._recap_section)
178
+
179
+ delegation_output_type = self._resolve_delegation_output_type()
180
+ prompt_cls: type[Prompt[DelegationOutputT]] = Prompt[delegation_output_type]
181
+ self._prompt = prompt_cls(
182
+ ns=f"{parent_prompt.ns}.delegation",
183
+ key=f"{parent_prompt.key}-wrapper",
184
+ sections=tuple(sections),
185
+ inject_output_instructions=False,
186
+ allow_extra_keys=bool(rendered_parent.allow_extra_keys),
187
+ )
188
+
189
+ def _resolve_delegation_output_type(self) -> type[DelegationOutputT]:
190
+ candidate = getattr(type(self), "_delegation_output_type", None)
191
+ if isinstance(candidate, type):
192
+ return cast(type[DelegationOutputT], candidate)
193
+
194
+ msg = "Specialize DelegationPrompt with an explicit output type"
195
+ raise TypeError(msg)
196
+
197
+ def _build_response_format_section(
198
+ self,
199
+ rendered_parent: RenderedPrompt[ParentOutputT],
200
+ ) -> ResponseFormatSection | None:
201
+ container = rendered_parent.container
202
+ if container is None:
203
+ return None
204
+
205
+ article = "an" if container.startswith(tuple("aeiou")) else "a"
206
+ extra_clause = (
207
+ "." if rendered_parent.allow_extra_keys else ". Do not add extra keys."
208
+ )
209
+
210
+ return ResponseFormatSection(
211
+ params=ResponseFormatParams(
212
+ article=article, container=container, extra_clause=extra_clause
213
+ )
214
+ )
215
+
216
+ @property
217
+ def prompt(self) -> Prompt[DelegationOutputT]:
218
+ """Expose the composed prompt for direct access when required."""
219
+
220
+ return self._prompt
221
+
222
+ def render(
223
+ self,
224
+ summary: DelegationParams,
225
+ parent: ParentPromptParams | None = None,
226
+ recap: RecapParams | None = None,
227
+ ) -> RenderedPrompt[DelegationOutputT]:
228
+ params: list[SupportsDataclass] = [summary]
229
+ parent_params = parent or ParentPromptParams(body=self._rendered_parent.text)
230
+ params.append(parent_params)
231
+
232
+ default_recap = RecapParams(bullets=tuple(summary.recap_lines))
233
+ recap_params = recap or default_recap
234
+ params.append(recap_params)
235
+
236
+ rendered = self._prompt.render(*tuple(params))
237
+ parent_deadline = self._rendered_parent.deadline
238
+ if parent_deadline is not None and rendered.deadline is not parent_deadline:
239
+ rendered = replace(rendered, deadline=parent_deadline)
240
+ merged_descriptions = _merge_tool_param_descriptions(
241
+ self._rendered_parent.tool_param_descriptions,
242
+ rendered.tool_param_descriptions,
243
+ )
244
+ if merged_descriptions is rendered.tool_param_descriptions:
245
+ return rendered
246
+ return replace(rendered, _tool_param_descriptions=merged_descriptions)
247
+
248
+
249
+ def _merge_tool_param_descriptions(
250
+ parent_descriptions: Mapping[str, Mapping[str, str]],
251
+ rendered_descriptions: Mapping[str, Mapping[str, str]],
252
+ ) -> Mapping[str, Mapping[str, str]]:
253
+ if not parent_descriptions:
254
+ return rendered_descriptions
255
+
256
+ merged: dict[str, dict[str, str]] = {
257
+ name: dict(fields) for name, fields in parent_descriptions.items()
258
+ }
259
+ for name, fields in rendered_descriptions.items():
260
+ if name not in merged:
261
+ merged[name] = dict(fields)
262
+ else:
263
+ merged[name].update(fields)
264
+
265
+ return MappingProxyType(
266
+ {
267
+ name: MappingProxyType(dict(field_mapping))
268
+ for name, field_mapping in merged.items()
269
+ }
270
+ )
271
+
272
+
273
+ __all__ = [
274
+ "DelegationParams",
275
+ "DelegationPrompt",
276
+ "DelegationSummarySection",
277
+ "ParentPromptParams",
278
+ "ParentPromptSection",
279
+ "RecapParams",
280
+ "RecapSection",
281
+ ]
@@ -0,0 +1,57 @@
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
+ from collections.abc import Sequence
16
+
17
+ SectionPath = tuple[str, ...]
18
+
19
+
20
+ def _normalize_section_path(section_path: Sequence[str] | None) -> SectionPath:
21
+ if section_path is None:
22
+ return ()
23
+ return tuple(section_path)
24
+
25
+
26
+ class PromptError(Exception):
27
+ """Base class for prompt-related failures providing structured context."""
28
+
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ *,
33
+ section_path: Sequence[str] | None = None,
34
+ dataclass_type: type | None = None,
35
+ placeholder: str | None = None,
36
+ ) -> None:
37
+ super().__init__(message)
38
+ self.message = message
39
+ self.section_path: SectionPath = _normalize_section_path(section_path)
40
+ self.dataclass_type = dataclass_type
41
+ self.placeholder = placeholder
42
+
43
+
44
+ class PromptValidationError(PromptError):
45
+ """Raised when prompt construction validation fails."""
46
+
47
+
48
+ class PromptRenderError(PromptError):
49
+ """Raised when rendering a prompt fails."""
50
+
51
+
52
+ __all__ = [
53
+ "PromptError",
54
+ "PromptRenderError",
55
+ "PromptValidationError",
56
+ "SectionPath",
57
+ ]