roboz 0.1.2.dev3__tar.gz → 0.1.2.dev4__tar.gz

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 (66) hide show
  1. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/CHANGELOG.md +11 -0
  2. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/PKG-INFO +9 -8
  3. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/README.md +8 -7
  4. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/pyproject.toml +1 -1
  5. roboz-0.1.2.dev4/src/roboz/deployment.py +391 -0
  6. roboz-0.1.2.dev3/src/roboz/deployment.py +0 -193
  7. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/.gitignore +0 -0
  8. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/LICENSE +0 -0
  9. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/__init__.py +0 -0
  10. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/_naming.py +0 -0
  11. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/__init__.py +0 -0
  12. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/_execution_context.py +0 -0
  13. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/_identifiers.py +0 -0
  14. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/_notifications.py +0 -0
  15. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/_prompts.py +0 -0
  16. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/_tool_observer.py +0 -0
  17. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/background_agent.py +0 -0
  18. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/core.py +0 -0
  19. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/prompt_agent_tool.py +0 -0
  20. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/agent/subagent.py +0 -0
  21. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/dependencies.py +0 -0
  22. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/exceptions.py +0 -0
  23. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/__init__.py +0 -0
  24. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/_diagnostics.py +0 -0
  25. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/_retry.py +0 -0
  26. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/_truncation.py +0 -0
  27. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/binding.py +0 -0
  28. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/calls.py +0 -0
  29. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/completion.py +0 -0
  30. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/endpoints.py +0 -0
  31. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/openrouter.py +0 -0
  32. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/llm/prompts.py +0 -0
  33. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/models/__init__.py +0 -0
  34. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/models/_schema.py +0 -0
  35. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/models/_serialization.py +0 -0
  36. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/models/_telemetry.py +0 -0
  37. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/models/core.py +0 -0
  38. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/models/truncation.py +0 -0
  39. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/py.typed +0 -0
  40. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/__init__.py +0 -0
  41. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/_environment.py +0 -0
  42. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/_external.py +0 -0
  43. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/_logging.py +0 -0
  44. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/_paths.py +0 -0
  45. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/events.py +0 -0
  46. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/io.py +0 -0
  47. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/observability.py +0 -0
  48. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/persistence/__init__.py +0 -0
  49. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/persistence/activity.py +0 -0
  50. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/persistence/schema.py +0 -0
  51. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/pipe.py +0 -0
  52. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/runtime/sinks.py +0 -0
  53. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/skill/__init__.py +0 -0
  54. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/skill/_prompts.py +0 -0
  55. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/skill/core.py +0 -0
  56. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tooling/__init__.py +0 -0
  57. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tooling/_prompts.py +0 -0
  58. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tooling/_protocols.py +0 -0
  59. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tooling/_typing.py +0 -0
  60. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tooling/context.py +0 -0
  61. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tooling/core.py +0 -0
  62. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tooling/decorators.py +0 -0
  63. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tools/__init__.py +0 -0
  64. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tools/_identifiers.py +0 -0
  65. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tools/control.py +0 -0
  66. {roboz-0.1.2.dev3 → roboz-0.1.2.dev4}/src/roboz/tools/interaction.py +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.2.dev4 - 2026-09-12
6
+
7
+ - Breaking: make `DeployableAgent` an explicitly configured class. Constructor
8
+ capabilities are protected defaults; append capabilities and child agents with
9
+ the add methods, configure endpoints and runtime values separately, and unpack
10
+ `(agent, background_agents)` from `build()`. `build_graph()` is removed.
11
+ - Breaking: capability builders now declare concrete `required_attributes` and
12
+ receive `build(agent, pipe)`. Builds validate the complete graph before
13
+ constructing sinks, pipes, or tools and aggregate missing, `None`, and
14
+ incorrectly typed owner attributes.
15
+
5
16
  ## 0.1.2.dev3 - 2026-09-11
6
17
 
7
18
  - Breaking: remove the redundant `roboz[shed]` installation extra. Install
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: roboz
3
- Version: 0.1.2.dev3
3
+ Version: 0.1.2.dev4
4
4
  Import-Name: roboz
5
5
  Summary: Typed primitives for composable agentic workflows
6
6
  Project-URL: Homepage, https://github.com/Tachion-Oy/roboz
@@ -170,14 +170,15 @@ catalogues; it installs core automatically. See the
170
170
 
171
171
  `roboz.deployment` supplies recursive `DeployableAgent` definitions and
172
172
  capability contracts. Put definitions directly in `subagents` or
173
- `background_agents`. Capabilities accept tool-specific endpoints and use the
174
- owning agent's endpoint as their default. `roboshed` supplies reusable
173
+ `background_agents`, append extensions with the explicit add methods, and unpack
174
+ `agent, background_agents = definition.build()`. Capabilities declare required
175
+ owner attributes and bind with `build(agent, pipe)`. `roboshed` supplies reusable
175
176
  orchestrator and Librarian constructors, concrete capabilities, memory tools,
176
- and sandbox structure. Import the constructors from `roboshed.agents`; concrete
177
- RoboSprawl configuration belongs to the external application. Sandbox-aware
178
- assembly lives in `roboshed.deployments.Deployment`: configure
179
- `deployment.sandbox.configure_scope(folder)` and `deployment.event_sinks`, then
180
- unpack `agent, background_agents = deployment.build()`.
177
+ and sandbox structure. Import the constructors from `roboshed.agents`; the fixed
178
+ lazy RoboSprawl recipe is `roboshed.deployments.robosprawl.RoboSprawl`. Construct
179
+ it without inputs, configure it through setters, then call argument-free
180
+ `build()` with all required inputs supplied. The recipe owns project context
181
+ and persistence; the caller owns invocation.
181
182
 
182
183
  ## Documentation
183
184
 
@@ -141,14 +141,15 @@ catalogues; it installs core automatically. See the
141
141
 
142
142
  `roboz.deployment` supplies recursive `DeployableAgent` definitions and
143
143
  capability contracts. Put definitions directly in `subagents` or
144
- `background_agents`. Capabilities accept tool-specific endpoints and use the
145
- owning agent's endpoint as their default. `roboshed` supplies reusable
144
+ `background_agents`, append extensions with the explicit add methods, and unpack
145
+ `agent, background_agents = definition.build()`. Capabilities declare required
146
+ owner attributes and bind with `build(agent, pipe)`. `roboshed` supplies reusable
146
147
  orchestrator and Librarian constructors, concrete capabilities, memory tools,
147
- and sandbox structure. Import the constructors from `roboshed.agents`; concrete
148
- RoboSprawl configuration belongs to the external application. Sandbox-aware
149
- assembly lives in `roboshed.deployments.Deployment`: configure
150
- `deployment.sandbox.configure_scope(folder)` and `deployment.event_sinks`, then
151
- unpack `agent, background_agents = deployment.build()`.
148
+ and sandbox structure. Import the constructors from `roboshed.agents`; the fixed
149
+ lazy RoboSprawl recipe is `roboshed.deployments.robosprawl.RoboSprawl`. Construct
150
+ it without inputs, configure it through setters, then call argument-free
151
+ `build()` with all required inputs supplied. The recipe owns project context
152
+ and persistence; the caller owns invocation.
152
153
 
153
154
  ## Documentation
154
155
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "roboz"
3
- version = "0.1.2.dev3"
3
+ version = "0.1.2.dev4"
4
4
  description = "Typed primitives for composable agentic workflows"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Tachion Oy" }]
@@ -0,0 +1,391 @@
1
+ """Configurable agent definitions and runtime-bound capabilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterator, Mapping, Sequence
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Protocol
9
+
10
+ from roboz.agent import Agent, run_background_agent, run_subagent
11
+ from roboz.llm import EndpointLike
12
+ from roboz.runtime import EventPipe, EventSink, Output
13
+ from roboz.skill import Skill
14
+ from roboz.tooling import Tool
15
+ from roboz.tooling.context import Ctx
16
+
17
+ type RequiredAttributeType = type[object] | tuple[type[object], ...]
18
+ type RequiredAttributes = Mapping[str, RequiredAttributeType]
19
+
20
+
21
+ class AgentCapability(Protocol):
22
+ """A configured feature that binds its tools to one agent runtime."""
23
+
24
+ @property
25
+ def required_attributes(self) -> RequiredAttributes:
26
+ """Declare configuration values required from the owning agent."""
27
+ ...
28
+
29
+ def build(self, agent: "DeployableAgent", pipe: EventPipe) -> "Capability":
30
+ """Bind configured tools using the owning agent and its fresh pipe."""
31
+ ...
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class Capability(AgentCapability):
36
+ """Already-bound tools and skills that provide an agent feature.
37
+
38
+ Tools shared between a chain and default execution retain their identity.
39
+ Supplied objects remain caller-owned and require no owner configuration.
40
+ """
41
+
42
+ tools: tuple[Tool | Sequence[Tool], ...] = ()
43
+ default_tools: tuple[Tool, ...] = ()
44
+ skills: tuple[Skill, ...] = ()
45
+ auto_loaded_skills: tuple[Skill, ...] = ()
46
+
47
+ @property
48
+ def required_attributes(self) -> RequiredAttributes:
49
+ """Declare that already-bound objects need no owner configuration."""
50
+ return {}
51
+
52
+ def build(self, agent: "DeployableAgent", pipe: EventPipe) -> "Capability":
53
+ """Return these already-bound inputs without copying or starting work."""
54
+ return self
55
+
56
+
57
+ class DeployableAgent:
58
+ """Mutable configuration for one agent and its attached child graph.
59
+
60
+ Constructor capabilities are fixed defaults. Later capabilities and child
61
+ definitions can only be appended. Runtime-specific values can be supplied
62
+ after declaration with set_attributes(); they belong only to this node.
63
+ Each build validates the complete graph and creates fresh runtime agents,
64
+ pipes, and capability bindings without retaining execution state here.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ *,
70
+ name: str,
71
+ description: str = "",
72
+ system_prompt: str = "",
73
+ is_agentic: bool = True,
74
+ automatic_tool_prompt: bool = True,
75
+ default_capabilities: Sequence[AgentCapability] = (),
76
+ subagents: Sequence["DeployableAgent"] = (),
77
+ background_agents: Sequence["DeployableAgent"] = (),
78
+ ) -> None:
79
+ """Configure identity, behavior, fixed capabilities, and initial children."""
80
+ self._name = name
81
+ self._description = description
82
+ self._system_prompt = system_prompt
83
+ self._is_agentic = is_agentic
84
+ self._automatic_tool_prompt = automatic_tool_prompt
85
+ self._default_capabilities = tuple(default_capabilities)
86
+ self._additional_capabilities: list[AgentCapability] = []
87
+ self._subagents: list[DeployableAgent] = []
88
+ self._background_agents: list[DeployableAgent] = []
89
+ self._agent_endpoint: EndpointLike | None = None
90
+ self._interaction_mode: Output | None = Output.CLI
91
+ self._initial_messages: tuple[Path | str, ...] = ()
92
+ self._attributes: dict[str, object] = {}
93
+ self.add_subagents(*subagents)
94
+ self.add_background_agents(*background_agents)
95
+
96
+ @property
97
+ def name(self) -> str:
98
+ """Return this agent's runtime name."""
99
+ return self._name
100
+
101
+ @property
102
+ def description(self) -> str:
103
+ """Return this agent's delegation description."""
104
+ return self._description
105
+
106
+ @property
107
+ def system_prompt(self) -> str:
108
+ """Return this agent's system prompt."""
109
+ return self._system_prompt
110
+
111
+ @property
112
+ def is_agentic(self) -> bool:
113
+ """Return whether the runtime agent performs model-driven turns."""
114
+ return self._is_agentic
115
+
116
+ @property
117
+ def automatic_tool_prompt(self) -> bool:
118
+ """Return whether tool instructions are added automatically."""
119
+ return self._automatic_tool_prompt
120
+
121
+ @property
122
+ def default_capabilities(self) -> tuple[AgentCapability, ...]:
123
+ """Return the fixed capabilities supplied at construction."""
124
+ return self._default_capabilities
125
+
126
+ @property
127
+ def additional_capabilities(self) -> tuple[AgentCapability, ...]:
128
+ """Return capabilities appended after the fixed defaults."""
129
+ return tuple(self._additional_capabilities)
130
+
131
+ @property
132
+ def capabilities(self) -> tuple[AgentCapability, ...]:
133
+ """Return fixed and additional capabilities in build order."""
134
+ return (*self._default_capabilities, *self._additional_capabilities)
135
+
136
+ @property
137
+ def subagents(self) -> tuple["DeployableAgent", ...]:
138
+ """Return attached synchronous child definitions."""
139
+ return tuple(self._subagents)
140
+
141
+ @property
142
+ def background_agents(self) -> tuple["DeployableAgent", ...]:
143
+ """Return attached background child definitions."""
144
+ return tuple(self._background_agents)
145
+
146
+ @property
147
+ def agent_endpoint(self) -> EndpointLike | None:
148
+ """Return the endpoint configured for this node."""
149
+ return self._agent_endpoint
150
+
151
+ @property
152
+ def interaction_mode(self) -> Output | None:
153
+ """Return this node's selected interaction mode."""
154
+ return self._interaction_mode
155
+
156
+ @property
157
+ def initial_messages(self) -> tuple[Path | str, ...]:
158
+ """Return configured initial message sources."""
159
+ return self._initial_messages
160
+
161
+ def __getattr__(self, name: str) -> object:
162
+ """Expose explicitly supplied capability attributes for owner reads."""
163
+ try:
164
+ attributes: dict[str, object] = object.__getattribute__(self, "_attributes")
165
+ return attributes[name]
166
+ except (AttributeError, KeyError):
167
+ raise AttributeError(name) from None
168
+
169
+ def set_agent_endpoint(self, endpoint: EndpointLike | None) -> None:
170
+ """Set or defer this node's model endpoint."""
171
+ self._agent_endpoint = endpoint
172
+
173
+ def set_interaction_mode(self, interaction_mode: Output | None) -> None:
174
+ """Select this node's runtime interaction mode."""
175
+ self._interaction_mode = interaction_mode
176
+
177
+ def set_initial_messages(self, messages: Sequence[Path | str]) -> None:
178
+ """Replace this node's initial message sources before a build."""
179
+ self._initial_messages = tuple(messages)
180
+
181
+ def set_attributes(self, **values: object) -> None:
182
+ """Set capability-specific values without altering agent structure.
183
+
184
+ Existing capability attributes may be updated for a later build. Public
185
+ structural attributes, methods, and all private names are protected.
186
+ """
187
+ for name in values:
188
+ if name.startswith("_") or any(
189
+ name in cls.__dict__ for cls in type(self).__mro__
190
+ ):
191
+ raise ValueError(
192
+ f"capability attribute cannot overwrite agent structure: {name!r}"
193
+ )
194
+ self._attributes.update(values)
195
+
196
+ def add_capabilities(self, *capabilities: AgentCapability) -> None:
197
+ """Append capabilities after this node's fixed defaults."""
198
+ self._additional_capabilities.extend(capabilities)
199
+
200
+ def add_subagents(self, *subagents: "DeployableAgent") -> None:
201
+ """Append synchronous child definitions."""
202
+ self._subagents.extend(self._checked_agents(subagents))
203
+
204
+ def add_background_agents(self, *agents: "DeployableAgent") -> None:
205
+ """Append background child definitions."""
206
+ self._background_agents.extend(self._checked_agents(agents))
207
+
208
+ @staticmethod
209
+ def _checked_agents(
210
+ agents: Sequence["DeployableAgent"],
211
+ ) -> tuple["DeployableAgent", ...]:
212
+ checked = tuple(agents)
213
+ if any(not isinstance(agent, DeployableAgent) for agent in checked):
214
+ raise TypeError("child definitions must be DeployableAgent instances")
215
+ return checked
216
+
217
+ def agent_names(self, *, include_background: bool = True) -> frozenset[str]:
218
+ """Validate all names and return identities in the selected branches.
219
+
220
+ Excluding background agents also excludes their descendants, for
221
+ foreground-only consumers such as conversation maintenance.
222
+ """
223
+ if not include_background:
224
+ self.agent_names()
225
+ return frozenset(self._collect_names(include_background, frozenset()))
226
+
227
+ def _collect_names(
228
+ self, include_background: bool, ancestors: frozenset[int]
229
+ ) -> set[str]:
230
+ identity = id(self)
231
+ if identity in ancestors:
232
+ raise ValueError("agent graph must not contain cycles")
233
+ ancestors = ancestors | {identity}
234
+ names = {self.name}
235
+ definitions = self.subagents
236
+ if include_background:
237
+ definitions += self.background_agents
238
+ for definition in definitions:
239
+ children = definition._collect_names(include_background, ancestors)
240
+ if names & children:
241
+ raise ValueError("agent names must be unique")
242
+ names.update(children)
243
+ return names
244
+
245
+ def validate(self) -> None:
246
+ """Validate names and every capability requirement in the full graph."""
247
+ self.agent_names()
248
+ errors = list(self._configuration_errors())
249
+ if not errors:
250
+ return
251
+ details = "\n".join(f"- {error}" for error in errors)
252
+ raise ValueError(f"invalid agent configuration:\n{details}")
253
+
254
+ def _configuration_errors(self) -> Iterator[str]:
255
+ """Yield capability requirement failures in stable graph order."""
256
+ for agent in self._walk():
257
+ yield from _capability_errors(agent)
258
+
259
+ def _walk(self) -> tuple["DeployableAgent", ...]:
260
+ definitions: list[DeployableAgent] = [self]
261
+ walked: list[DeployableAgent] = []
262
+ while definitions:
263
+ agent = definitions.pop(0)
264
+ walked.append(agent)
265
+ definitions.extend((*agent.subagents, *agent.background_agents))
266
+ return tuple(walked)
267
+
268
+ def build(
269
+ self,
270
+ *,
271
+ event_sinks: Sequence[EventSink] = (),
272
+ event_sink_factory: Callable[[str], Sequence[EventSink]] | None = None,
273
+ ) -> tuple[Agent, tuple[Agent, ...]]:
274
+ """Validate and build a fresh root with every background handle.
275
+
276
+ Caller sinks reach foreground branches only. The optional factory gives
277
+ each named agent its own fresh sinks. Construction starts no agents.
278
+ """
279
+ self.validate()
280
+ return self._build(
281
+ event_sinks=event_sinks, event_sink_factory=event_sink_factory
282
+ )
283
+
284
+ def _build(
285
+ self,
286
+ *,
287
+ event_sinks: Sequence[EventSink] = (),
288
+ event_sink_factory: Callable[[str], Sequence[EventSink]] | None = None,
289
+ ) -> tuple[Agent, tuple[Agent, ...]]:
290
+ pipe = EventPipe(
291
+ event_sinks=(
292
+ *(event_sink_factory(self.name) if event_sink_factory else ()),
293
+ *event_sinks,
294
+ )
295
+ )
296
+ contributions = [
297
+ capability.build(self, pipe) for capability in self.capabilities
298
+ ]
299
+
300
+ tools = [tool for contribution in contributions for tool in contribution.tools]
301
+ default_tools = [tool for c in contributions for tool in c.default_tools]
302
+ background_agents: list[Agent] = []
303
+ for definition in self.subagents:
304
+ child, descendants = definition._build(
305
+ event_sinks=event_sinks, event_sink_factory=event_sink_factory
306
+ )
307
+ tools.append(
308
+ run_subagent(Ctx(agent=child)).copy(
309
+ name=child.name,
310
+ description=child.description,
311
+ )
312
+ )
313
+ background_agents.extend(descendants)
314
+ for definition in self.background_agents:
315
+ child, descendants = definition._build(
316
+ event_sink_factory=event_sink_factory
317
+ )
318
+ default_tools.append(
319
+ run_background_agent(Ctx(agent=child)).copy(
320
+ name=f"start_background_agent_{child.name}",
321
+ )
322
+ )
323
+ background_agents.append(child)
324
+ background_agents.extend(descendants)
325
+
326
+ agent = Agent(
327
+ name=self.name,
328
+ description=self.description,
329
+ agent_endpoint=self.agent_endpoint,
330
+ event_pipe=pipe,
331
+ interaction_mode=self.interaction_mode,
332
+ is_agentic=self.is_agentic,
333
+ automatic_tool_prompt=self.automatic_tool_prompt,
334
+ system_prompt=self.system_prompt,
335
+ tools=tools,
336
+ default_tools=default_tools,
337
+ skills=[skill for c in contributions for skill in c.skills],
338
+ auto_loaded_skills=[
339
+ skill for c in contributions for skill in c.auto_loaded_skills
340
+ ],
341
+ initial_messages=self.initial_messages,
342
+ )
343
+ return agent, tuple(background_agents)
344
+
345
+
346
+ _MISSING = object()
347
+
348
+
349
+ def _type_name(expected: RequiredAttributeType) -> str:
350
+ types = expected if isinstance(expected, tuple) else (expected,)
351
+ return " or ".join(item.__name__ for item in types)
352
+
353
+
354
+ def _capability_errors(agent: DeployableAgent) -> Iterator[str]:
355
+ """Yield unmet attribute requirements for one configuration node."""
356
+ for capability in agent.capabilities:
357
+ for name, expected in capability.required_attributes.items():
358
+ error = _required_attribute_error(agent, capability, name, expected)
359
+ if error is not None:
360
+ yield error
361
+
362
+
363
+ def _required_attribute_error(
364
+ agent: DeployableAgent,
365
+ capability: AgentCapability,
366
+ name: str,
367
+ expected: RequiredAttributeType,
368
+ ) -> str | None:
369
+ """Describe one unmet requirement, or return None when it is satisfied."""
370
+ value = getattr(agent, name, _MISSING)
371
+ if value is not _MISSING and value is not None and isinstance(value, expected):
372
+ return None
373
+
374
+ owner = f"agent {agent.name!r}, capability {type(capability).__name__}"
375
+ requirement = f"attribute {name!r} must be {_type_name(expected)}"
376
+ if value is _MISSING:
377
+ actual = "it is missing"
378
+ elif value is None:
379
+ actual = "it is None"
380
+ else:
381
+ actual = f"got {type(value).__name__}"
382
+ return f"{owner}: {requirement}; {actual}"
383
+
384
+
385
+ __all__ = [
386
+ "AgentCapability",
387
+ "Capability",
388
+ "DeployableAgent",
389
+ "RequiredAttributeType",
390
+ "RequiredAttributes",
391
+ ]
@@ -1,193 +0,0 @@
1
- """Data-driven agent definitions and runtime-bound capabilities."""
2
-
3
- from __future__ import annotations
4
-
5
- from collections.abc import Callable, Sequence
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
- from typing import Protocol
9
-
10
- from roboz.agent import Agent, run_background_agent, run_subagent
11
- from roboz.llm import EndpointLike
12
- from roboz.runtime import EventPipe, EventSink, Output
13
- from roboz.skill import Skill
14
- from roboz.tooling import Tool
15
- from roboz.tooling.context import Ctx
16
-
17
-
18
- class AgentCapability(Protocol):
19
- """A configured feature that binds its own tools to an agent runtime.
20
-
21
- Store tool-specific endpoints on the capability. The build input supplies
22
- the owning agent's endpoint as a default, independently of its event pipe.
23
- """
24
-
25
- def build(
26
- self, pipe: EventPipe, *, default_endpoint: EndpointLike | None
27
- ) -> "Capability":
28
- """Bind configured tools, using the supplied default for unset endpoints."""
29
- ...
30
-
31
-
32
- @dataclass(frozen=True)
33
- class Capability(AgentCapability):
34
- """Configured tools and skills that provide an agent feature.
35
-
36
- Tools shared between a chain and default execution retain their identity.
37
- Use this directly for already-bound inputs, or return one from a capability
38
- builder for runtime-bound inputs. Supplied objects remain caller-owned.
39
- """
40
-
41
- tools: tuple[Tool | Sequence[Tool], ...] = ()
42
- default_tools: tuple[Tool, ...] = ()
43
- skills: tuple[Skill, ...] = ()
44
- auto_loaded_skills: tuple[Skill, ...] = ()
45
-
46
- def build(
47
- self, pipe: EventPipe, *, default_endpoint: EndpointLike | None
48
- ) -> "Capability":
49
- """Return these already-bound inputs without copying or starting work."""
50
- return self
51
-
52
-
53
- @dataclass(frozen=True, kw_only=True)
54
- class DeployableAgent:
55
- """One agent's configuration, including its sub-agents and background agents.
56
-
57
- All tools and skills enter through capabilities. Already-bound inputs and
58
- endpoint dependencies are supplied objects; callers own their reuse. Scripted
59
- mock endpoints that consume responses must be recreated for each run.
60
- Configure definitions before building; do not mutate them concurrently with
61
- construction. Each capability selects its tool endpoints, using the agent
62
- endpoint as the default. Endpoint objects remain independent of runtime controls.
63
- Sub-agents become named delegation tools. Background agents are started
64
- through default tools when their parent runs. Both slots contain the same
65
- recursive definition type.
66
- """
67
-
68
- name: str
69
- agent_endpoint: EndpointLike | None
70
- description: str = ""
71
- system_prompt: str = ""
72
- interaction_mode: Output | None = Output.CLI
73
- is_agentic: bool = True
74
- automatic_tool_prompt: bool = True
75
- capabilities: tuple[AgentCapability, ...] = ()
76
- subagents: tuple["DeployableAgent", ...] = ()
77
- background_agents: tuple["DeployableAgent", ...] = ()
78
- initial_messages: tuple[Path | str, ...] = ()
79
-
80
- def agent_names(self, *, include_background: bool = True) -> frozenset[str]:
81
- """Validate all names and return identities in the selected branches.
82
-
83
- Excluding background agents also excludes their descendants, for
84
- foreground-only consumers such as conversation maintenance.
85
- """
86
- if not include_background:
87
- self.agent_names()
88
- names = {self.name}
89
- definitions = self.subagents
90
- if include_background:
91
- definitions += self.background_agents
92
- for definition in definitions:
93
- children = definition.agent_names(include_background=include_background)
94
- if names & children:
95
- raise ValueError("agent names must be unique")
96
- names.update(children)
97
- return frozenset(names)
98
-
99
- def build(
100
- self,
101
- *,
102
- event_sinks: Sequence[EventSink] = (),
103
- event_sink_factory: Callable[[str], Sequence[EventSink]] | None = None,
104
- ) -> Agent:
105
- """Create fresh pipes, resolve capabilities, and construct the agent once.
106
-
107
- Caller sinks follow synchronous children. The optional factory supplies fresh
108
- agent-specific sinks by name; these are never inherited by children.
109
- Without supplied sinks, construction selects no output or persistence.
110
- Use build_graph() to retain background agents for host control.
111
- """
112
- return self.build_graph(
113
- event_sinks=event_sinks, event_sink_factory=event_sink_factory
114
- )[0]
115
-
116
- def build_graph(
117
- self,
118
- *,
119
- event_sinks: Sequence[EventSink] = (),
120
- event_sink_factory: Callable[[str], Sequence[EventSink]] | None = None,
121
- ) -> tuple[Agent, tuple[Agent, ...]]:
122
- """Build fresh agents and return the root and all background handles.
123
-
124
- Validate names before constructing capabilities or sinks. Sink routing
125
- follows build(): caller sinks reach foreground branches only, and the
126
- factory supplies each agent's own sinks. Background handles include
127
- descendants of both child slots. No agents are started by construction;
128
- callers own their invocation and shutdown.
129
- """
130
- self.agent_names()
131
- pipe = EventPipe(
132
- event_sinks=(
133
- *(event_sink_factory(self.name) if event_sink_factory else ()),
134
- *event_sinks,
135
- )
136
- )
137
- contributions = []
138
- for capability in self.capabilities:
139
- contributions.append(
140
- capability.build(pipe, default_endpoint=self.agent_endpoint)
141
- )
142
-
143
- tools = [tool for contribution in contributions for tool in contribution.tools]
144
- default_tools = [tool for c in contributions for tool in c.default_tools]
145
- background_agents: list[Agent] = []
146
- for definition in self.subagents:
147
- child, descendants = definition.build_graph(
148
- event_sinks=event_sinks, event_sink_factory=event_sink_factory
149
- )
150
- tools.append(
151
- run_subagent(Ctx(agent=child)).copy(
152
- name=child.name,
153
- description=child.description,
154
- )
155
- )
156
- background_agents.extend(descendants)
157
- for definition in self.background_agents:
158
- child, descendants = definition.build_graph(
159
- event_sink_factory=event_sink_factory
160
- )
161
- default_tools.append(
162
- run_background_agent(Ctx(agent=child)).copy(
163
- name=f"start_background_agent_{child.name}",
164
- )
165
- )
166
- background_agents.append(child)
167
- background_agents.extend(descendants)
168
-
169
- agent = Agent(
170
- name=self.name,
171
- description=self.description,
172
- agent_endpoint=self.agent_endpoint,
173
- event_pipe=pipe,
174
- interaction_mode=self.interaction_mode,
175
- is_agentic=self.is_agentic,
176
- automatic_tool_prompt=self.automatic_tool_prompt,
177
- system_prompt=self.system_prompt,
178
- tools=tools,
179
- default_tools=default_tools,
180
- skills=[skill for c in contributions for skill in c.skills],
181
- auto_loaded_skills=[
182
- skill for c in contributions for skill in c.auto_loaded_skills
183
- ],
184
- initial_messages=self.initial_messages,
185
- )
186
- return agent, tuple(background_agents)
187
-
188
-
189
- __all__ = [
190
- "AgentCapability",
191
- "Capability",
192
- "DeployableAgent",
193
- ]
File without changes
File without changes