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,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