zettcode-engine 0.3.4__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 (90) hide show
  1. zettcode/_compat.py +35 -0
  2. zettcode/budget.py +24 -0
  3. zettcode/config.py +1088 -0
  4. zettcode/config_edit.py +457 -0
  5. zettcode/engine/__init__.py +39 -0
  6. zettcode/engine/agent/__init__.py +81 -0
  7. zettcode/engine/agent/base.py +60 -0
  8. zettcode/engine/agent/context.py +166 -0
  9. zettcode/engine/agent/exports.py +73 -0
  10. zettcode/engine/agent/interaction.py +54 -0
  11. zettcode/engine/agent/sessions.py +218 -0
  12. zettcode/engine/agent/settings.py +269 -0
  13. zettcode/engine/agent/turns.py +308 -0
  14. zettcode/engine/core.py +819 -0
  15. zettcode/engine/dialogue/__init__.py +6 -0
  16. zettcode/engine/dialogue/ask.py +113 -0
  17. zettcode/engine/dialogue/side.py +197 -0
  18. zettcode/engine/dialogue/title.py +80 -0
  19. zettcode/engine/export/__init__.py +421 -0
  20. zettcode/engine/export/trace/script.js +18 -0
  21. zettcode/engine/export/trace/style.css +136 -0
  22. zettcode/engine/export/trace/template.html +31 -0
  23. zettcode/engine/extensions/__init__.py +8 -0
  24. zettcode/engine/extensions/capabilities.py +68 -0
  25. zettcode/engine/extensions/compaction.py +259 -0
  26. zettcode/engine/extensions/context.py +521 -0
  27. zettcode/engine/extensions/files.py +101 -0
  28. zettcode/engine/extensions/guidance.py +17 -0
  29. zettcode/engine/extensions/harness.py +208 -0
  30. zettcode/engine/extensions/harness_switch.py +18 -0
  31. zettcode/engine/extensions/mcp_notice.py +49 -0
  32. zettcode/engine/extensions/relay.py +93 -0
  33. zettcode/engine/extensions/subagents.py +391 -0
  34. zettcode/engine/extensions/tool_output.py +153 -0
  35. zettcode/engine/extensions/truncation.py +156 -0
  36. zettcode/engine/extensions/usage.py +304 -0
  37. zettcode/engine/harness_paths.py +74 -0
  38. zettcode/engine/permission/__init__.py +197 -0
  39. zettcode/engine/permission/approval.py +66 -0
  40. zettcode/engine/permission/readonly.py +435 -0
  41. zettcode/engine/permission/sandbox.py +140 -0
  42. zettcode/engine/pipeline.py +191 -0
  43. zettcode/engine/plugins/__init__.py +72 -0
  44. zettcode/engine/plugins/commands.py +152 -0
  45. zettcode/engine/plugins/context.py +251 -0
  46. zettcode/engine/plugins/extension.py +183 -0
  47. zettcode/engine/plugins/groups.py +74 -0
  48. zettcode/engine/plugins/loader.py +367 -0
  49. zettcode/engine/plugins/mixins.py +156 -0
  50. zettcode/engine/plugins/plugin.py +190 -0
  51. zettcode/engine/plugins/services.py +68 -0
  52. zettcode/engine/plugins/settings.py +117 -0
  53. zettcode/engine/plugins/state.py +64 -0
  54. zettcode/engine/plugins/switches.py +553 -0
  55. zettcode/engine/py.typed +0 -0
  56. zettcode/engine/server_tools.py +179 -0
  57. zettcode/engine/storage/__init__.py +51 -0
  58. zettcode/engine/storage/layout.py +161 -0
  59. zettcode/engine/storage/ledger.py +659 -0
  60. zettcode/engine/storage/locks.py +182 -0
  61. zettcode/engine/storage/metadata.py +278 -0
  62. zettcode/engine/storage/records.py +373 -0
  63. zettcode/engine/storage/store.py +1056 -0
  64. zettcode/engine/tools/__init__.py +7 -0
  65. zettcode/engine/tools/boundary.py +56 -0
  66. zettcode/engine/tools/files.py +262 -0
  67. zettcode/engine/tools/images.py +32 -0
  68. zettcode/engine/tools/output.py +81 -0
  69. zettcode/engine/tools/shell.py +181 -0
  70. zettcode/engine/tools/workspace.py +271 -0
  71. zettcode/error_text.py +217 -0
  72. zettcode/filelock.py +125 -0
  73. zettcode/logging_setup.py +196 -0
  74. zettcode/mention.py +124 -0
  75. zettcode/modes.py +18 -0
  76. zettcode/paths.py +113 -0
  77. zettcode/plugin_tui.py +133 -0
  78. zettcode/processes.py +126 -0
  79. zettcode/prompt.py +94 -0
  80. zettcode/providers.py +215 -0
  81. zettcode/py.typed +0 -0
  82. zettcode/registry.py +63 -0
  83. zettcode/relay.py +267 -0
  84. zettcode/statusline_parts.py +84 -0
  85. zettcode/version.py +11 -0
  86. zettcode/wire.py +30 -0
  87. zettcode/wording.py +11 -0
  88. zettcode_engine-0.3.4.dist-info/METADATA +23 -0
  89. zettcode_engine-0.3.4.dist-info/RECORD +90 -0
  90. zettcode_engine-0.3.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,251 @@
1
+ """What a plugin receives when it activates: ``ctx``, and the groups it registers into.
2
+
3
+ The context is the plugin's only channel to the application. It exposes the resolved configuration, and three groups:
4
+ ``ctx.agent`` for what every front end has (extensions, providers, ``@`` providers, services), ``ctx.commands`` for
5
+ plain commands that say nothing about a UI, and ``ctx.tui`` for what only the terminal can hold — ``None`` everywhere
6
+ else. The context itself keeps the accumulated registrations; the groups are the stable names a plugin writes
7
+ against, so what is added later lands in a group rather than in another method on the context.
8
+
9
+ The terminal's own group is :class:`zettcode.app.plugins.groups.TuiGroups`; the engine cannot name it, since the
10
+ terminal is a layer above this one, which is why the attribute is untyped here and documented instead.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING, TypeVar
16
+
17
+ from zettcode.plugin_tui import TuiGroup
18
+ from zettcode.providers import Provider
19
+
20
+ from .commands import CommandGroups
21
+ from .groups import AgentGroups
22
+ from .services import lookup, require
23
+ from .settings import PluginSettings
24
+ from .state import Contribution, ExtensionFactory
25
+
26
+ if TYPE_CHECKING: # pragma: no cover - annotations only, so the imports stay lazy
27
+ from collections.abc import Mapping
28
+
29
+ from zett_agent.extensions.base import AgentExtension
30
+
31
+ from zettcode.config import ZettCodeConfig
32
+ from zettcode.mention import MentionProvider
33
+
34
+ _T = TypeVar("_T")
35
+
36
+
37
+ class PluginContext:
38
+ """What a plugin is handed when it activates, and the groups it registers into.
39
+
40
+ The container is the plugin's only channel to the application: it exposes the resolved configuration, and
41
+ collects what the plugin registers. They are accumulated here rather than returned, so a plugin can decide what
42
+ to register from the configuration it is given.
43
+ """
44
+
45
+ def __init__(self, config: ZettCodeConfig, services: Mapping[str, object] | None = None, *, name: str = "") -> None:
46
+ """Keep the resolved settings and open empty registries.
47
+
48
+ Args:
49
+ config: The resolved settings.
50
+ services: What is already findable by name: the objects the runtime hands over, and what the
51
+ plugins loaded before this one provided. Read with :meth:`service` and :meth:`find`.
52
+ name: The plugin this context belongs to, which is the name its ``[plugins.<name>]`` table is written
53
+ under. The loader passes it; a test that builds a context by hand may leave it out.
54
+ """
55
+ self.config = config
56
+ self.workspace = config.workspace
57
+ #: The keys this plugin reads from the configuration, and the defaults it gives them.
58
+ self.settings = PluginSettings(name, config.plugin_settings.get(name) if name else None)
59
+ #: What every front end has: extensions, providers, ``@`` providers and services.
60
+ self.agent = AgentGroups(self)
61
+ #: Plain commands: text and a toast, no widget and no shell.
62
+ self.commands = CommandGroups()
63
+ self._tui: TuiGroup | None = None
64
+ self._available: Mapping[str, object] = services if services is not None else {}
65
+ self._mentions: list[MentionProvider] = []
66
+ self._providers: list[Provider] = []
67
+ self._extensions: list[Contribution] = []
68
+ self._services: dict[str, object] = {}
69
+
70
+ @property
71
+ def tui(self) -> TuiGroup | None:
72
+ """Return the terminal's group, or ``None`` in a process without a screen.
73
+
74
+ The loader attaches it once, before the plugin activates; a plugin reads it and never replaces it.
75
+ :class:`zettcode.app.plugins.groups.TuiGroups` is the implementation.
76
+ """
77
+ return self._tui
78
+
79
+ def attach_tui(self, group: TuiGroup) -> None:
80
+ """Give this context its terminal group; the terminal's loader is the caller."""
81
+ self._tui = group
82
+
83
+ @property
84
+ def extensions(self) -> tuple[Contribution, ...]:
85
+ """Return the agent extensions registered so far, in registration order."""
86
+ return tuple(self._extensions)
87
+
88
+ @property
89
+ def services(self) -> dict[str, object]:
90
+ """Return a copy of the services provided so far, by name."""
91
+ return dict(self._services)
92
+
93
+ @property
94
+ def mentions(self) -> tuple[MentionProvider, ...]:
95
+ """Return the ``@`` resource providers registered so far."""
96
+ return tuple(self._mentions)
97
+
98
+ @property
99
+ def providers(self) -> tuple[Provider, ...]:
100
+ """Return the model providers registered so far, in registration order."""
101
+ return tuple(self._providers)
102
+
103
+ def register_provider(self, provider: Provider) -> Provider:
104
+ """Describe a host models can be reached at, so ``/config`` offers it.
105
+
106
+ A provider is an address and a protocol; the reader brings the token and the model id. It
107
+ appears in the ``/config`` picker beside the built-in ones and ``/config add <name>``
108
+ takes it. Nothing is called or connected to: this only describes a host.
109
+
110
+ Args:
111
+ provider: The host. Its ``name`` must be unique across the application, because it is
112
+ what ``/config add`` is typed with.
113
+
114
+ Returns:
115
+ The registered provider, in case the plugin wants to keep it.
116
+
117
+ Raises:
118
+ ValueError: When the name is already registered by this plugin, or is one of the
119
+ built-in providers'.
120
+ """
121
+ if not isinstance(provider, Provider):
122
+ raise ValueError("A provider must be a zettcode.providers.Provider")
123
+ if provider.name in {existing.name for existing in self._providers}:
124
+ raise ValueError(f"Provider already registered: {provider.name}")
125
+ self._providers.append(provider)
126
+ return provider
127
+
128
+ def register_extension(self, extension: AgentExtension) -> AgentExtension:
129
+ """Hand the agent one ``zett-agent`` extension of the plugin's own.
130
+
131
+ Use this when the plugin is, or wraps, a whole extension rather than a set of hooks: the
132
+ agent receives it as a separate extension, ordered by the priority it declares and, among
133
+ equals, by registration order, exactly as if the application had listed it itself. The hook
134
+ methods on :class:`~zettcode.engine.plugins.Plugin` are the other route, for a plugin that only
135
+ needs to watch a stage; they run inside the one host extension.
136
+
137
+ Args:
138
+ extension: The extension to add. Its ``name`` must be unique among the agent's
139
+ extensions; a clash fails the plugin when it loads.
140
+
141
+ Returns:
142
+ The extension, in case the plugin wants to keep it.
143
+
144
+ Raises:
145
+ ValueError: When the name is blank or this plugin already registered it.
146
+ """
147
+ name = getattr(extension, "name", "")
148
+ if not isinstance(name, str) or not name.strip():
149
+ raise ValueError("An extension must have a non-empty name")
150
+ self._claim_extension(name)
151
+ self._extensions.append(Contribution(name, extension=extension))
152
+ return extension
153
+
154
+ def register_extension_factory(self, name: str, factory: ExtensionFactory) -> None:
155
+ """Hand the agent an extension that is built when the agent is, not when the plugin activates.
156
+
157
+ Use it for an extension that needs something only a running agent has (the provider it starts
158
+ on), or that is costly to import: a plugin activates while the shell is still being drawn, so
159
+ importing a large module there delays the first frame. The factory is called with an
160
+ :class:`~zettcode.engine.plugins.AgentBuild` after the imports the runtime warms up have finished,
161
+ and may return ``None`` for a run that has nothing to add. Where the extension lands in the
162
+ order is where it was registered, as for :meth:`register_extension`.
163
+
164
+ Args:
165
+ name: The name of the extension the factory builds, given up front so that a clash is
166
+ caught when the plugin loads. A factory that builds it under another name fails the build.
167
+ factory: Called once per agent build.
168
+
169
+ Raises:
170
+ ValueError: When the name is blank or this plugin already registered it.
171
+ """
172
+ if not name.strip():
173
+ raise ValueError("An extension must have a non-empty name")
174
+ self._claim_extension(name)
175
+ self._extensions.append(Contribution(name, factory=factory))
176
+
177
+ def _claim_extension(self, name: str) -> None:
178
+ """Refuse an extension name this plugin has already registered."""
179
+ if any(existing.name == name for existing in self._extensions):
180
+ raise ValueError(f"Extension already registered: {name}")
181
+
182
+ def service(self, name: str, kind: type[_T]) -> _T:
183
+ """Return an object another part of the application or an earlier plugin made available.
184
+
185
+ This is how a plugin gets hold of the state it builds on: the session's permission policy, the
186
+ session store, or a service a plugin loaded before it provided. Order matters, because only
187
+ what is already there can be found.
188
+
189
+ Raises:
190
+ LookupError: When nothing is available under the name.
191
+ TypeError: When what is there is not a ``kind``.
192
+ """
193
+ return require({**self._available, **self._services}, name, kind)
194
+
195
+ def find(self, name: str, kind: type[_T]) -> _T | None:
196
+ """Return the object available under ``name``, or ``None`` when there is none (see :meth:`service`)."""
197
+ return lookup({**self._available, **self._services}, name, kind)
198
+
199
+ def provide(self, name: str, service: object) -> object:
200
+ """Register an object under a name, so other parts of the application can look it up.
201
+
202
+ Use it when the plugin creates something that the shell or the runtime also needs to reach,
203
+ for example the todo extension, whose plan the shell draws. The application looks it up with
204
+ ``Plugins.service(name, kind)`` or ``Plugins.find(name, kind)``, so it needs no field of its
205
+ own for it. This is separate from :meth:`register_extension`: that one gives the agent an
206
+ extension to run, this one only makes an object findable. A plugin can provide an object
207
+ it does not register with the agent, such as the todo extension while the tool is switched off.
208
+
209
+ Args:
210
+ name: Identity of the service; unique across the loaded plugins.
211
+ service: The object itself.
212
+
213
+ Returns:
214
+ The service, in case the plugin wants to keep it.
215
+
216
+ Raises:
217
+ ValueError: When the name is blank or this plugin already provided it.
218
+ """
219
+ name = name.strip()
220
+ if not name:
221
+ raise ValueError("Service name cannot be empty")
222
+ if name in self._services:
223
+ raise ValueError(f"Service already provided: {name}")
224
+ self._services[name] = service
225
+ return service
226
+
227
+ def register_mention(self, provider: MentionProvider) -> MentionProvider:
228
+ """Register one kind of ``@`` resource the composer can offer.
229
+
230
+ A provider lists candidates for the completion menu and expands a
231
+ token into the text the model sees; the builtin provider offers skills,
232
+ and a plugin adds its own kind the same way.
233
+
234
+ Args:
235
+ provider: Provider that owns one resource kind. Its ``kind`` must be
236
+ unique across the application, because a token has no prefix to
237
+ say which provider should resolve it.
238
+
239
+ Returns:
240
+ The registered provider, in case the plugin wants to keep it.
241
+
242
+ Raises:
243
+ ValueError: When the kind is blank or already registered.
244
+ """
245
+ kind = getattr(provider, "kind", "").strip()
246
+ if not kind:
247
+ raise ValueError("Mention provider kind cannot be empty")
248
+ if kind in {existing.kind for existing in self._mentions}:
249
+ raise ValueError(f"Mention provider already registered: {kind}")
250
+ self._mentions.append(provider)
251
+ return provider
@@ -0,0 +1,183 @@
1
+ """Host every application plugin inside one ``zett-agent`` extension.
2
+
3
+ Plugins are this application's own concept; the agent only knows extensions. A
4
+ single host is registered instead of one adapter per plugin, so the application
5
+ keeps one place to grow plugin-wide behaviour — ordering, shared state, loading
6
+ and unloading — without changing how the agent sees the bundle. The host
7
+ forwards every lifecycle hook to each plugin in priority order.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from contextlib import aclosing
13
+ from typing import TYPE_CHECKING, Any, cast
14
+
15
+ from zett_agent.extensions.base import AgentExtension
16
+
17
+ if TYPE_CHECKING: # pragma: no cover - annotations only
18
+ from collections.abc import AsyncGenerator, AsyncIterator, Sequence
19
+
20
+ from zett_agent.agent import AgentRunConfig, AgentRunContext
21
+ from zett_agent.extensions.base import ModelRequestNext, ToolCallNext
22
+ from zett_agent.extensions.events import ExtensionEvent
23
+ from zett_agent.extensions.external import ExternalEvent
24
+ from zett_agent.messages import AssistantMessage, ToolCall, ToolMessage
25
+ from zett_agent.model import ModelEvent, ModelRequest, ModelResponse
26
+ from zett_agent.tools.base import ToolResult
27
+
28
+ from .plugin import Plugin
29
+
30
+ #: Default priority of the host when it carries no plugins; the same band as the
31
+ #: built-in extensions, so an empty bundle changes nothing.
32
+ DEFAULT_PRIORITY = 100
33
+
34
+ #: The host's name as the agent sees it; an extension a plugin registers cannot take it.
35
+ HOST_NAME = "plugins"
36
+
37
+
38
+ class PluginExtension(AgentExtension):
39
+ """One extension that drives every installed plugin.
40
+
41
+ ``plugins`` holds the plugins in priority order: lower first, equal
42
+ priorities in load order. The host fans each hook out in that order and
43
+ nests the model and tool middleware with it — the first plugin is the
44
+ outermost wrapper, exactly as if the plugins were separate extensions.
45
+
46
+ The host takes the earliest priority any plugin asked for, so a plugin that
47
+ needs to observe before a built-in still can. The whole bundle moves with
48
+ it, and the plugins' order among themselves stays their own.
49
+ """
50
+
51
+ def __init__(self, plugins: Sequence[Plugin[Any]]) -> None:
52
+ """Keep the plugins in hook order and adopt their earliest priority."""
53
+ self.plugins = tuple(sorted(plugins, key=lambda plugin: plugin.priority))
54
+ self.name = HOST_NAME
55
+ self.priority = min((plugin.priority for plugin in self.plugins), default=DEFAULT_PRIORITY)
56
+
57
+ # -- setup -------------------------------------------------------------
58
+
59
+ async def on_tool(self, context: AgentRunContext) -> None:
60
+ for plugin in self.plugins:
61
+ await plugin.on_tool(context)
62
+
63
+ async def on_state(self, context: AgentRunContext) -> None:
64
+ for plugin in self.plugins:
65
+ await plugin.on_state(context)
66
+
67
+ async def on_message(self, context: AgentRunContext) -> None:
68
+ for plugin in self.plugins:
69
+ await plugin.on_message(context)
70
+
71
+ # -- run ---------------------------------------------------------------
72
+
73
+ async def before_run(self, context: AgentRunContext) -> None:
74
+ for plugin in self.plugins:
75
+ await plugin.before_run(context)
76
+
77
+ async def after_run(self, context: AgentRunContext, result: AssistantMessage) -> None:
78
+ for plugin in self.plugins:
79
+ await plugin.after_run(context, result)
80
+
81
+ async def on_success(self, context: AgentRunContext, result: AssistantMessage) -> None:
82
+ for plugin in self.plugins:
83
+ await plugin.on_success(context, result)
84
+
85
+ async def on_error(self, context: AgentRunContext, error: Exception) -> None:
86
+ for plugin in self.plugins:
87
+ await plugin.on_error(context, error)
88
+
89
+ # -- turn --------------------------------------------------------------
90
+
91
+ async def before_turn(self, context: AgentRunContext) -> None:
92
+ for plugin in self.plugins:
93
+ await plugin.before_turn(context)
94
+
95
+ async def after_turn(self, context: AgentRunContext, result: AssistantMessage) -> None:
96
+ for plugin in self.plugins:
97
+ await plugin.after_turn(context, result)
98
+
99
+ # -- model -------------------------------------------------------------
100
+
101
+ async def before_model(self, context: AgentRunContext, request: ModelRequest) -> None:
102
+ for plugin in self.plugins:
103
+ await plugin.before_model(context, request)
104
+
105
+ async def after_model(self, context: AgentRunContext, response: ModelResponse) -> None:
106
+ for plugin in self.plugins:
107
+ await plugin.after_model(context, response)
108
+
109
+ # -- tools -------------------------------------------------------------
110
+
111
+ async def before_tool(self, context: AgentRunContext, call: ToolCall) -> None:
112
+ for plugin in self.plugins:
113
+ await plugin.before_tool(context, call)
114
+
115
+ async def after_tool(
116
+ self,
117
+ context: AgentRunContext,
118
+ call: ToolCall,
119
+ result: ToolMessage,
120
+ error: Exception | None,
121
+ ) -> None:
122
+ for plugin in self.plugins:
123
+ await plugin.after_tool(context, call, result, error)
124
+
125
+ # -- maintenance and events -------------------------------------------
126
+
127
+ async def on_compact(self, context: AgentRunContext) -> None:
128
+ for plugin in self.plugins:
129
+ await plugin.on_compact(context)
130
+
131
+ def accept(self, config: AgentRunConfig | None, event: ExternalEvent) -> bool:
132
+ """Offer the event to every plugin; accepted when any of them claims it.
133
+
134
+ Every plugin is asked even after one accepts — the agent broadcasts to
135
+ all of them, and a plugin must not lose an event to another's answer.
136
+ """
137
+ answers = [plugin.accept(config, event) for plugin in self.plugins]
138
+ return any(answers)
139
+
140
+ async def on_event(self, context: AgentRunContext, event: ExtensionEvent) -> None:
141
+ for plugin in self.plugins:
142
+ await plugin.on_event(context, event)
143
+
144
+ # -- middleware --------------------------------------------------------
145
+
146
+ async def on_model_request(
147
+ self,
148
+ context: AgentRunContext,
149
+ request: ModelRequest,
150
+ call_next: ModelRequestNext,
151
+ ) -> AsyncIterator[ModelEvent]:
152
+ """Nest the plugins around the provider stream, first plugin outermost."""
153
+
154
+ async def run(index: int, current: ModelRequest) -> AsyncIterator[ModelEvent]:
155
+ if index >= len(self.plugins):
156
+ # ``call_next`` is typed as AsyncIterator; only the provider
157
+ # stream behind it can actually be closed, so cast for aclosing.
158
+ stream = cast("AsyncGenerator[ModelEvent]", call_next(current))
159
+ async with aclosing(stream) as events:
160
+ async for event in events:
161
+ yield event
162
+ return
163
+ plugin = self.plugins[index]
164
+ async for event in plugin.on_model_request(context, current, lambda request: run(index + 1, request)):
165
+ yield event
166
+
167
+ async for event in run(0, request):
168
+ yield event
169
+
170
+ async def on_tool_call(
171
+ self,
172
+ context: AgentRunContext,
173
+ call: ToolCall,
174
+ call_next: ToolCallNext,
175
+ ) -> ToolResult:
176
+ """Nest the plugins around the tool handler, first plugin outermost."""
177
+
178
+ async def run(index: int) -> ToolResult:
179
+ if index >= len(self.plugins):
180
+ return await call_next()
181
+ return await self.plugins[index].on_tool_call(context, call, lambda: run(index + 1))
182
+
183
+ return await run(0)
@@ -0,0 +1,74 @@
1
+ """The group a plugin registers everything a front end shares into.
2
+
3
+ ``ctx.agent`` is the one group every process has: an extension the model runs, a model provider, a kind of ``@``
4
+ resource, or an object other parts look up. The context keeps the registries; this is the name a plugin writes, and the
5
+ place a later addition lands — so the context does not grow a method per feature.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, TypeVar
11
+
12
+ if TYPE_CHECKING: # pragma: no cover - annotations only
13
+ from zett_agent.extensions.base import AgentExtension
14
+
15
+ from zettcode.providers import Provider
16
+
17
+ from .context import PluginContext
18
+ from .state import Contribution, ExtensionFactory
19
+
20
+ _T = TypeVar("_T")
21
+
22
+
23
+ class AgentGroups:
24
+ """What every front end has: the agent's own extensions, providers, ``@`` resources and services.
25
+
26
+ Args:
27
+ context: The context these registrations accumulate in.
28
+ """
29
+
30
+ def __init__(self, context: PluginContext) -> None:
31
+ self._context = context
32
+
33
+ @property
34
+ def extensions(self) -> tuple[Contribution, ...]:
35
+ """Return the extensions registered so far, in registration order."""
36
+ return self._context.extensions
37
+
38
+ @property
39
+ def mentions(self) -> tuple[object, ...]:
40
+ """Return the ``@`` resource providers registered so far."""
41
+ return tuple(self._context.mentions)
42
+
43
+ @property
44
+ def providers(self) -> tuple[object, ...]:
45
+ """Return the model providers registered so far, in registration order."""
46
+ return tuple(self._context.providers)
47
+
48
+ def register_extension(self, extension: AgentExtension) -> AgentExtension:
49
+ """Hand the agent one extension of the plugin's own."""
50
+ return self._context.register_extension(extension)
51
+
52
+ def register_extension_factory(self, name: str, factory: ExtensionFactory) -> None:
53
+ """Hand the agent an extension built when the agent is built, not when the plugin activates."""
54
+ return self._context.register_extension_factory(name, factory)
55
+
56
+ def register_provider(self, provider: Provider) -> Provider:
57
+ """Describe a host models can be reached at, so ``/config`` offers it."""
58
+ return self._context.register_provider(provider)
59
+
60
+ def register_mention(self, provider: object) -> object:
61
+ """Register one kind of ``@`` resource the composer can offer."""
62
+ return self._context.register_mention(provider) # type: ignore[arg-type]
63
+
64
+ def provide(self, name: str, service: object) -> object:
65
+ """Make an object findable by name, for other parts of the application."""
66
+ return self._context.provide(name, service)
67
+
68
+ def service(self, name: str, kind: type[_T]) -> _T:
69
+ """Return the object available under ``name``, checked to be a ``kind``."""
70
+ return self._context.service(name, kind)
71
+
72
+ def find(self, name: str, kind: type[_T]) -> _T | None:
73
+ """Return the object under ``name``, or ``None`` when there is none."""
74
+ return self._context.find(name, kind)