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.
- zettcode/_compat.py +35 -0
- zettcode/budget.py +24 -0
- zettcode/config.py +1088 -0
- zettcode/config_edit.py +457 -0
- zettcode/engine/__init__.py +39 -0
- zettcode/engine/agent/__init__.py +81 -0
- zettcode/engine/agent/base.py +60 -0
- zettcode/engine/agent/context.py +166 -0
- zettcode/engine/agent/exports.py +73 -0
- zettcode/engine/agent/interaction.py +54 -0
- zettcode/engine/agent/sessions.py +218 -0
- zettcode/engine/agent/settings.py +269 -0
- zettcode/engine/agent/turns.py +308 -0
- zettcode/engine/core.py +819 -0
- zettcode/engine/dialogue/__init__.py +6 -0
- zettcode/engine/dialogue/ask.py +113 -0
- zettcode/engine/dialogue/side.py +197 -0
- zettcode/engine/dialogue/title.py +80 -0
- zettcode/engine/export/__init__.py +421 -0
- zettcode/engine/export/trace/script.js +18 -0
- zettcode/engine/export/trace/style.css +136 -0
- zettcode/engine/export/trace/template.html +31 -0
- zettcode/engine/extensions/__init__.py +8 -0
- zettcode/engine/extensions/capabilities.py +68 -0
- zettcode/engine/extensions/compaction.py +259 -0
- zettcode/engine/extensions/context.py +521 -0
- zettcode/engine/extensions/files.py +101 -0
- zettcode/engine/extensions/guidance.py +17 -0
- zettcode/engine/extensions/harness.py +208 -0
- zettcode/engine/extensions/harness_switch.py +18 -0
- zettcode/engine/extensions/mcp_notice.py +49 -0
- zettcode/engine/extensions/relay.py +93 -0
- zettcode/engine/extensions/subagents.py +391 -0
- zettcode/engine/extensions/tool_output.py +153 -0
- zettcode/engine/extensions/truncation.py +156 -0
- zettcode/engine/extensions/usage.py +304 -0
- zettcode/engine/harness_paths.py +74 -0
- zettcode/engine/permission/__init__.py +197 -0
- zettcode/engine/permission/approval.py +66 -0
- zettcode/engine/permission/readonly.py +435 -0
- zettcode/engine/permission/sandbox.py +140 -0
- zettcode/engine/pipeline.py +191 -0
- zettcode/engine/plugins/__init__.py +72 -0
- zettcode/engine/plugins/commands.py +152 -0
- zettcode/engine/plugins/context.py +251 -0
- zettcode/engine/plugins/extension.py +183 -0
- zettcode/engine/plugins/groups.py +74 -0
- zettcode/engine/plugins/loader.py +367 -0
- zettcode/engine/plugins/mixins.py +156 -0
- zettcode/engine/plugins/plugin.py +190 -0
- zettcode/engine/plugins/services.py +68 -0
- zettcode/engine/plugins/settings.py +117 -0
- zettcode/engine/plugins/state.py +64 -0
- zettcode/engine/plugins/switches.py +553 -0
- zettcode/engine/py.typed +0 -0
- zettcode/engine/server_tools.py +179 -0
- zettcode/engine/storage/__init__.py +51 -0
- zettcode/engine/storage/layout.py +161 -0
- zettcode/engine/storage/ledger.py +659 -0
- zettcode/engine/storage/locks.py +182 -0
- zettcode/engine/storage/metadata.py +278 -0
- zettcode/engine/storage/records.py +373 -0
- zettcode/engine/storage/store.py +1056 -0
- zettcode/engine/tools/__init__.py +7 -0
- zettcode/engine/tools/boundary.py +56 -0
- zettcode/engine/tools/files.py +262 -0
- zettcode/engine/tools/images.py +32 -0
- zettcode/engine/tools/output.py +81 -0
- zettcode/engine/tools/shell.py +181 -0
- zettcode/engine/tools/workspace.py +271 -0
- zettcode/error_text.py +217 -0
- zettcode/filelock.py +125 -0
- zettcode/logging_setup.py +196 -0
- zettcode/mention.py +124 -0
- zettcode/modes.py +18 -0
- zettcode/paths.py +113 -0
- zettcode/plugin_tui.py +133 -0
- zettcode/processes.py +126 -0
- zettcode/prompt.py +94 -0
- zettcode/providers.py +215 -0
- zettcode/py.typed +0 -0
- zettcode/registry.py +63 -0
- zettcode/relay.py +267 -0
- zettcode/statusline_parts.py +84 -0
- zettcode/version.py +11 -0
- zettcode/wire.py +30 -0
- zettcode/wording.py +11 -0
- zettcode_engine-0.3.4.dist-info/METADATA +23 -0
- zettcode_engine-0.3.4.dist-info/RECORD +90 -0
- zettcode_engine-0.3.4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""The plugin base class and the bundle of what the loaded plugins produced.
|
|
2
|
+
|
|
3
|
+
:class:`Plugin` is what an author subclasses: identity, one activation hook, and
|
|
4
|
+
the composed hook groups from :mod:`zettcode.engine.plugins.mixins`.
|
|
5
|
+
:class:`Plugins` is the other direction — the host extension, services, and
|
|
6
|
+
providers the loader hands the application, plus whatever failed to load.
|
|
7
|
+
|
|
8
|
+
A plugin is activated with one :class:`PluginContext`: the engine's own plugins take a
|
|
9
|
+
:class:`~zettcode.engine.plugins.container.PluginContext`, and the terminal's take its subclass, which also
|
|
10
|
+
registers commands, palettes, pets, presenters and rows.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from abc import ABC
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
|
|
18
|
+
|
|
19
|
+
from .context import PluginContext
|
|
20
|
+
from .mixins import PluginAgentMixin
|
|
21
|
+
from .services import lookup, require
|
|
22
|
+
from .state import AgentBuild, Contribution
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING: # pragma: no cover - annotations only, so the imports stay lazy
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
|
|
27
|
+
from zett_agent.extensions.base import AgentExtension
|
|
28
|
+
|
|
29
|
+
from zettcode.mention import MentionProvider
|
|
30
|
+
from zettcode.providers import Provider
|
|
31
|
+
|
|
32
|
+
from .extension import PluginExtension
|
|
33
|
+
|
|
34
|
+
_C = TypeVar("_C", bound=PluginContext)
|
|
35
|
+
_T = TypeVar("_T")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class PluginSetting:
|
|
40
|
+
"""The configuration key that decides whether a compiled-in plugin does its work.
|
|
41
|
+
|
|
42
|
+
A plugin installed as a distribution is switched by `[plugins] disable` in the configuration. One compiled into the program has no
|
|
43
|
+
file of its own: what turns its work off is a setting the reader already has, such as ``[skills] enabled``. A
|
|
44
|
+
plugin that declares one is listed by a panel with a switch, and the switch writes that key; a plugin that does
|
|
45
|
+
not is not listed, because there is nothing a reader could do about it.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
field: The attribute of :class:`~zettcode.config.ZettCodeConfig` that holds it now, for the state a list
|
|
49
|
+
shows and for a running process to keep up with.
|
|
50
|
+
table: The configuration table it is written to.
|
|
51
|
+
key: The key inside that table.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
field: str
|
|
55
|
+
table: str
|
|
56
|
+
key: str = "enabled"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def path(self) -> str:
|
|
60
|
+
"""Return it the way the configuration file spells it, ``skills.enabled``."""
|
|
61
|
+
return f"{self.table}.{self.key}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Plugin(PluginAgentMixin, Generic[_C], ABC):
|
|
65
|
+
"""Base class for one ZettCode plugin.
|
|
66
|
+
|
|
67
|
+
The hooks are grouped into mixins, and :class:`Plugin` composes the agent flow group
|
|
68
|
+
(:class:`PluginAgentMixin`). A subclass overrides only the stages it takes part in, and each default does
|
|
69
|
+
nothing; the finer agent groups (setup, run, turn, model, tool, maintenance, events, middleware) mirror
|
|
70
|
+
``zett-agent``'s extension mixins one for one, so an author who knows the extension docs already knows where a
|
|
71
|
+
hook belongs.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
name: Unique registration identity. Defaults to the class name; set it
|
|
75
|
+
when the entry-point name is not the label you want in
|
|
76
|
+
diagnostics. When it is empty the entry-point name is used, and a
|
|
77
|
+
plugin that declares neither falls back to the class name.
|
|
78
|
+
description: One-line summary of what the plugin adds, for diagnostics.
|
|
79
|
+
priority: Hook order against every other extension: lower runs first,
|
|
80
|
+
and equal priorities keep registration order.
|
|
81
|
+
setting: The configuration key that turns its work off, for a plugin compiled into the program; ``None`` when
|
|
82
|
+
the plugin is installed as a distribution, which `[plugins] disable` reaches instead.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
#: Identity the loader may resolve and record; a plain attribute, so a
|
|
86
|
+
#: nameless class can be given its entry-point name at load time.
|
|
87
|
+
name: str = ""
|
|
88
|
+
#: How the distribution that carries it is called — the entry-point name for an installed plugin, its own
|
|
89
|
+
#: name for a builtin. `/plugins` shows it when it differs from :attr:`name`, because `[plugins] disable` and the
|
|
90
|
+
#: switch file are matched against it.
|
|
91
|
+
origin: str = ""
|
|
92
|
+
description: ClassVar[str] = ""
|
|
93
|
+
priority: ClassVar[int] = 100
|
|
94
|
+
#: The configuration key that decides whether a compiled-in plugin does its work, or ``None`` when there is none:
|
|
95
|
+
#: a plugin the reader cannot turn off is left out of the list rather than shown without a switch.
|
|
96
|
+
setting: ClassVar[PluginSetting | None] = None
|
|
97
|
+
|
|
98
|
+
def activate(self, container: _C) -> None:
|
|
99
|
+
"""Run once at startup, before any agent client is built.
|
|
100
|
+
|
|
101
|
+
This is the place to register what the plugin contributes, and to read the
|
|
102
|
+
configuration. It is synchronous on purpose: the shell must know the
|
|
103
|
+
full command list before it paints, and a plugin that needs asynchronous
|
|
104
|
+
setup can do it in the lifecycle hooks, which all run on the agent loop.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
container: The resolved settings and the registration surface.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class Plugins:
|
|
113
|
+
"""Everything the installed plugins contributed to the agent, and what failed to load.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
host: The single extension that drives every loaded plugin; ``None``
|
|
117
|
+
when none loaded. The agent is handed one extension, not one per
|
|
118
|
+
plugin, so plugin-wide behaviour has one place to live.
|
|
119
|
+
mentions: ``@`` resource providers the plugins registered, in load and
|
|
120
|
+
registration order; the shell merges them with the builtin skills.
|
|
121
|
+
providers: Model providers the plugins described, in load and registration order; the
|
|
122
|
+
shell offers them in ``/config`` after the built-in ones.
|
|
123
|
+
contributed: The ``zett-agent`` extensions the plugins registered through ``register_extension`` and
|
|
124
|
+
``register_extension_factory``, in load and registration order. :meth:`agent_extensions` turns them
|
|
125
|
+
into the extensions the agent is given, beside the host, so each keeps the priority it declares.
|
|
126
|
+
services: Objects the runtime handed over and the plugins provided, by name; read them with
|
|
127
|
+
:meth:`service` and :meth:`find`.
|
|
128
|
+
plain_commands: Commands a plugin registered on ``ctx.commands``, in load and registration order, each
|
|
129
|
+
typed ``"plugin"``. The terminal turns them into shell commands; another front end reads them here.
|
|
130
|
+
failures: One message per plugin that could not be loaded or activated, ``origin: why``, read from
|
|
131
|
+
:attr:`failed`. A plugin that cannot be loaded is reported rather than taking the whole application
|
|
132
|
+
with it; the shell reports these where the user can see them.
|
|
133
|
+
failed: The failures themselves, one ``(origin, why)`` pair each, for a list that groups them by the plugin
|
|
134
|
+
they belong to. This is the one record; :attr:`failures` is written from it.
|
|
135
|
+
builtins: The origins of the plugins compiled into the program, in load order. They always load, so a switch
|
|
136
|
+
cannot leave one out, and a list can say which rows are the program's own.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
host: PluginExtension | None = None
|
|
140
|
+
mentions: tuple[MentionProvider, ...] = ()
|
|
141
|
+
providers: tuple[Provider, ...] = ()
|
|
142
|
+
contributed: tuple[Contribution, ...] = ()
|
|
143
|
+
services: Mapping[str, object] = field(default_factory=dict)
|
|
144
|
+
plain_commands: tuple[object, ...] = ()
|
|
145
|
+
failed: tuple[tuple[str, str], ...] = ()
|
|
146
|
+
builtins: tuple[str, ...] = ()
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def failures(self) -> tuple[str, ...]:
|
|
150
|
+
"""Return each failure as the one line a transcript shows, ``origin: why``."""
|
|
151
|
+
return tuple(f"{origin}: {why}" for origin, why in self.failed)
|
|
152
|
+
|
|
153
|
+
def find(self, name: str, kind: type[_T]) -> _T | None:
|
|
154
|
+
"""Return the service under ``name``, or ``None`` when no loaded plugin provides it.
|
|
155
|
+
|
|
156
|
+
For what the application can do without: the plan panel has nothing to show when nobody keeps a
|
|
157
|
+
plan. A service that exists but is the wrong type is still an error, because that is a plugin
|
|
158
|
+
bug and not an absence.
|
|
159
|
+
"""
|
|
160
|
+
return lookup(self.services, name, kind)
|
|
161
|
+
|
|
162
|
+
def service(self, name: str, kind: type[_T]) -> _T:
|
|
163
|
+
"""Return the service under ``name``, checked to be a ``kind``.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
LookupError: When nothing provides it; a plugin that failed to load provides nothing, and
|
|
167
|
+
the message says which service is missing.
|
|
168
|
+
TypeError: When the name is taken by an object of another type.
|
|
169
|
+
"""
|
|
170
|
+
return require(self.services, name, kind)
|
|
171
|
+
|
|
172
|
+
def agent_extensions(self, build: AgentBuild | None = None) -> tuple[AgentExtension, ...]:
|
|
173
|
+
"""Return the extensions the plugins hand the agent, in registration order.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
build: What exists while the agent is being built. Factories are called with it; without it
|
|
177
|
+
(outside a build) only the extensions registered whole are returned.
|
|
178
|
+
"""
|
|
179
|
+
built = (contribution.build(build) for contribution in self.contributed)
|
|
180
|
+
return tuple(extension for extension in built if extension is not None)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def plugins(self) -> tuple[Plugin[Any], ...]:
|
|
184
|
+
"""Return the loaded plugins, in the order the host will call them."""
|
|
185
|
+
return self.host.plugins if self.host is not None else ()
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def extensions(self) -> tuple[PluginExtension, ...]:
|
|
189
|
+
"""Return the one extension to register, or nothing when no plugin loaded."""
|
|
190
|
+
return (self.host,) if self.host is not None else ()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""The names objects are shared under between the runtime and the plugins, and the lookup both use.
|
|
2
|
+
|
|
3
|
+
A service is an object found by name. Two kinds travel this way:
|
|
4
|
+
|
|
5
|
+
* **What the runtime hands the plugins** before any of them activates: the state of the session that
|
|
6
|
+
extensions are built on (``POLICY``, ``PERSISTENCE``, ``MODELS``, ``COMPACTION_COUNTER``,
|
|
7
|
+
``MESSENGER``, ``SELECTED_MODEL``). The runtime owns them and keeps using them itself.
|
|
8
|
+
* **What a plugin provides** for the rest of the application, and for the plugins loaded after it
|
|
9
|
+
(``APPROVAL``, ``USAGE``, ``CONTEXT``, ``SIDES``, ``COMPACTION``, ``CAPABILITIES``, ``TODOS``).
|
|
10
|
+
|
|
11
|
+
The names are constants so that a misspelt one is an import error rather than a lookup that fails
|
|
12
|
+
only when the feature is first used.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover - annotations only
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
|
|
22
|
+
#: Handed over by the runtime.
|
|
23
|
+
POLICY = "policy"
|
|
24
|
+
HARNESS = "harness"
|
|
25
|
+
PERSISTENCE = "persistence"
|
|
26
|
+
MODELS = "models"
|
|
27
|
+
COMPACTION_COUNTER = "compaction-counter"
|
|
28
|
+
MESSENGER = "messenger"
|
|
29
|
+
SELECTED_MODEL = "selected-model"
|
|
30
|
+
|
|
31
|
+
#: Provided by the built-in plugins.
|
|
32
|
+
APPROVAL = "approval"
|
|
33
|
+
USAGE = "usage"
|
|
34
|
+
CONTEXT = "context"
|
|
35
|
+
SIDES = "sides"
|
|
36
|
+
COMPACTION = "compaction"
|
|
37
|
+
CAPABILITIES = "capabilities"
|
|
38
|
+
TODOS = "todos"
|
|
39
|
+
|
|
40
|
+
_T = TypeVar("_T")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def lookup(services: Mapping[str, object], name: str, kind: type[_T]) -> _T | None:
|
|
44
|
+
"""Return the service called ``name`` as a ``kind``, or ``None`` when nobody provides it.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
TypeError: When the name is taken by an object of another type; that is a plugin bug, not an
|
|
48
|
+
absence, and hiding it as ``None`` would send the search somewhere else.
|
|
49
|
+
"""
|
|
50
|
+
if name not in services:
|
|
51
|
+
return None
|
|
52
|
+
found = services[name]
|
|
53
|
+
if not isinstance(found, kind):
|
|
54
|
+
raise TypeError(f"Service {name!r} is a {type(found).__name__}, not a {kind.__name__}")
|
|
55
|
+
return found
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def require(services: Mapping[str, object], name: str, kind: type[_T]) -> _T:
|
|
59
|
+
"""Return the service called ``name`` as a ``kind``.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
LookupError: When nobody provides it; the message names the service.
|
|
63
|
+
TypeError: When the name is taken by an object of another type.
|
|
64
|
+
"""
|
|
65
|
+
found = lookup(services, name, kind)
|
|
66
|
+
if found is None:
|
|
67
|
+
raise LookupError(f"No plugin provides the {name!r} service")
|
|
68
|
+
return found
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""The keys one plugin reads from the configuration, and the defaults it supplies for them.
|
|
2
|
+
|
|
3
|
+
The configuration file cannot know which plugins are installed or what their keys mean, so it keeps each plugin's
|
|
4
|
+
table as it was written (``[plugins.recap] after_seconds = 180``). A plugin says what it reads and what the value is
|
|
5
|
+
when the reader has not written one; from then on the reader's value wins, checked against the default's type, and a
|
|
6
|
+
key the plugin never declared is an error rather than a silent default.
|
|
7
|
+
|
|
8
|
+
::
|
|
9
|
+
|
|
10
|
+
given = ctx.settings.declare(after_seconds=180, cache_ttl_seconds=300)
|
|
11
|
+
after = given["after_seconds"] # 180, or what [plugins.recap] says
|
|
12
|
+
|
|
13
|
+
A type is the default's: a whole number for an ``int``, true or false for a ``bool``, text for a ``str``, and a number
|
|
14
|
+
for a ``float`` (a whole one is accepted and comes back as a float). Anything else — a list, a table, a string where a
|
|
15
|
+
number was declared — is refused when the plugin activates, naming the plugin, the key and the file it came from.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING: # pragma: no cover - annotations only
|
|
23
|
+
from collections.abc import Mapping
|
|
24
|
+
|
|
25
|
+
#: What a plugin's settings may hold: what TOML calls a scalar.
|
|
26
|
+
Value: TypeAlias = bool | int | float | str
|
|
27
|
+
|
|
28
|
+
__all__ = ["PluginSettings", "Value"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _matches(value: object, default: Value) -> bool:
|
|
32
|
+
"""Return whether a value from the file has the type its declared default has."""
|
|
33
|
+
if isinstance(default, bool):
|
|
34
|
+
return isinstance(value, bool)
|
|
35
|
+
if isinstance(default, int):
|
|
36
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
37
|
+
if isinstance(default, float):
|
|
38
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
39
|
+
return isinstance(value, str)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
#: How each type is described in a refusal, in the words a reader of the file would use.
|
|
43
|
+
_WORDS = {bool: "true or false", int: "a whole number", float: "a number", str: "text"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _as_written(value: object, default: Value) -> Value:
|
|
47
|
+
"""Return the reader's value as the default's type, or raise naming what was expected."""
|
|
48
|
+
if not _matches(value, default):
|
|
49
|
+
expected = _WORDS[type(default)]
|
|
50
|
+
raise ValueError(f"must be {expected}, not {type(value).__name__}")
|
|
51
|
+
if isinstance(default, float) and isinstance(value, int):
|
|
52
|
+
return float(value)
|
|
53
|
+
return value # type: ignore[return-value] # narrowed by `_matches`
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PluginSettings:
|
|
57
|
+
"""The keys one plugin declares, with the reader's values in front of its defaults.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
plugin: The plugin's name, which is the entry-point name a ``[plugins.<name>]`` table is written under.
|
|
61
|
+
given: What the reader wrote in that table, checked as each key is declared.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, plugin: str = "", given: Mapping[str, object] | None = None) -> None:
|
|
65
|
+
"""Keep the plugin's name and the values the reader wrote, if any."""
|
|
66
|
+
self.plugin = plugin
|
|
67
|
+
self._given: dict[str, object] = dict(given or {})
|
|
68
|
+
self._defaults: dict[str, Value] = {}
|
|
69
|
+
|
|
70
|
+
def declare(self, **defaults: object) -> dict[str, object]:
|
|
71
|
+
"""Declare the keys the plugin reads and return the values in effect.
|
|
72
|
+
|
|
73
|
+
Declaring the same key twice keeps the first default, so two call sites cannot disagree about what the
|
|
74
|
+
value means; the reader's value is applied to whichever default was declared first.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
**defaults: One default per key, whose type is the type the reader's value must have.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Each declared key with the reader's value when there is one, else the default.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: A default is not a scalar, or the reader's value for a key is not the declared type.
|
|
84
|
+
"""
|
|
85
|
+
for key, default in defaults.items():
|
|
86
|
+
if not isinstance(default, (bool, int, float, str)):
|
|
87
|
+
raise ValueError(f"{self._where(key)} declares a default that is not a string, a number or a boolean")
|
|
88
|
+
self._defaults.setdefault(key, default)
|
|
89
|
+
return {key: self.get(key) for key in defaults}
|
|
90
|
+
|
|
91
|
+
def get(self, key: str) -> Value:
|
|
92
|
+
"""Return the value in effect for one declared key.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: The plugin never declared that key (reading it would always be a default it never chose),
|
|
96
|
+
or the reader's value is not the type the default declares.
|
|
97
|
+
"""
|
|
98
|
+
if key not in self._defaults:
|
|
99
|
+
raise ValueError(f"{self._where(key)} is not a setting this plugin declared")
|
|
100
|
+
default = self._defaults[key]
|
|
101
|
+
if key not in self._given:
|
|
102
|
+
return default
|
|
103
|
+
value = self._given[key]
|
|
104
|
+
try:
|
|
105
|
+
return _as_written(value, default)
|
|
106
|
+
except ValueError as error:
|
|
107
|
+
hint = f" (declared as {type(default).__name__})" if isinstance(default, float) else ""
|
|
108
|
+
raise ValueError(f"{self._where(key)} {error}{hint}") from None
|
|
109
|
+
|
|
110
|
+
def values(self) -> dict[str, object]:
|
|
111
|
+
"""Return every declared key with the value in effect, for a plugin that wants them all at once."""
|
|
112
|
+
return {key: self.get(key) for key in self._defaults}
|
|
113
|
+
|
|
114
|
+
def _where(self, key: str) -> str:
|
|
115
|
+
"""Return the place a problem is reported against: the plugin's table when it has a name."""
|
|
116
|
+
table = f"[plugins.{self.plugin}]" if self.plugin else "the plugin settings"
|
|
117
|
+
return f"Config key {key!r} in {table}"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""What a plugin hands the agent: extensions, ready now or built when the agent is."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING: # pragma: no cover - annotations only, so the imports stay lazy
|
|
10
|
+
from zett_agent.extensions.base import AgentExtension
|
|
11
|
+
from zett_agent.model import AgentModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class AgentBuild:
|
|
16
|
+
"""What exists when the agent is being built, and not before.
|
|
17
|
+
|
|
18
|
+
A plugin activates while the shell is still being drawn, before any provider exists. An extension
|
|
19
|
+
that needs one, or that is costly to import, is registered as a factory and called with this when
|
|
20
|
+
the runtime builds the agent.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
model: The provider the agent starts on.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model: AgentModel
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
#: Builds an extension once the agent is being built; ``None`` means this run has none to add.
|
|
30
|
+
ExtensionFactory: TypeAlias = Callable[[AgentBuild], "AgentExtension | None"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class Contribution:
|
|
35
|
+
"""One ``zett-agent`` extension a plugin hands the agent: ready now, or built when the agent is.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
name: The extension's name, known before it exists so that a clash is caught at load.
|
|
39
|
+
extension: The extension itself, when it was registered whole.
|
|
40
|
+
factory: What builds it, when it was registered as a factory.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
extension: AgentExtension | None = None
|
|
45
|
+
factory: ExtensionFactory | None = None
|
|
46
|
+
|
|
47
|
+
def build(self, build: AgentBuild | None) -> AgentExtension | None:
|
|
48
|
+
"""Return the extension, building it if it is a factory and the agent is being built.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
build: What exists at build time; ``None`` outside a build, where a factory yields nothing.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: When the factory built an extension under another name than it announced; the
|
|
55
|
+
load-time clash check would have been checking the wrong name.
|
|
56
|
+
"""
|
|
57
|
+
if self.extension is not None:
|
|
58
|
+
return self.extension
|
|
59
|
+
if build is None or self.factory is None:
|
|
60
|
+
return None
|
|
61
|
+
made = self.factory(build)
|
|
62
|
+
if made is not None and made.name != self.name:
|
|
63
|
+
raise ValueError(f"Extension registered as {self.name!r} was built as {made.name!r}")
|
|
64
|
+
return made
|