zettcode-sdk 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/daemon/__init__.py +17 -0
- zettcode/daemon/__main__.py +10 -0
- zettcode/daemon/cli.py +693 -0
- zettcode/daemon/companions.py +63 -0
- zettcode/daemon/process.py +559 -0
- zettcode/daemon/run.py +678 -0
- zettcode/daemon/state.py +356 -0
- zettcode/doctor.py +167 -0
- zettcode/mentions.py +320 -0
- zettcode/sdk/__init__.py +195 -0
- zettcode/sdk/builtins.py +608 -0
- zettcode/sdk/client.py +1810 -0
- zettcode/sdk/commands.py +9 -0
- zettcode/sdk/daemons.py +244 -0
- zettcode/sdk/errors.py +132 -0
- zettcode/sdk/events.py +434 -0
- zettcode/sdk/extensions/__init__.py +6 -0
- zettcode/sdk/extensions/mcp.py +276 -0
- zettcode/sdk/extensions/messaging.py +281 -0
- zettcode/sdk/handlers.py +126 -0
- zettcode/sdk/messenger.py +299 -0
- zettcode/sdk/models.py +156 -0
- zettcode/sdk/py.typed +0 -0
- zettcode/sdk/session.py +1212 -0
- zettcode/sdk/translation.py +384 -0
- zettcode/sdk/types.py +907 -0
- zettcode/table.py +95 -0
- zettcode_sdk-0.3.4.dist-info/METADATA +21 -0
- zettcode_sdk-0.3.4.dist-info/RECORD +30 -0
- zettcode_sdk-0.3.4.dist-info/WHEEL +4 -0
zettcode/sdk/builtins.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""The agent-side extensions ZettCode ships with, offered the way any plugin offers one.
|
|
2
|
+
|
|
3
|
+
Each class here owns one ``zett-agent`` extension and the switch in the configuration that turns it on, so
|
|
4
|
+
the runtime lists none of them by hand: it asks the loaded plugins for what they contributed. They register
|
|
5
|
+
through :meth:`~zettcode.engine.plugins.context.PluginContext` and
|
|
6
|
+
:meth:`~zettcode.engine.plugins.context.PluginContext`, which are also what a third-party
|
|
7
|
+
plugin uses, so a plugin that wraps a whole extension is not a second-class citizen.
|
|
8
|
+
|
|
9
|
+
Two rules decide how one registers:
|
|
10
|
+
|
|
11
|
+
* An extension that costs something to import, or needs the provider the agent starts on, is registered as
|
|
12
|
+
a **factory**: a plugin activates while the first frame is still being drawn, and the factory is called
|
|
13
|
+
only when the agent is built, after the imports the runtime warms up. The ``zett_agent`` imports are inside
|
|
14
|
+
the factories for that reason.
|
|
15
|
+
* An object the application also has to reach (the plan, the usage counters, the compaction) is **provided**
|
|
16
|
+
as a service by name, and the runtime reads it from there instead of keeping a field for it.
|
|
17
|
+
|
|
18
|
+
The order of :data:`AGENT_PLUGINS` is the order the agent receives the extensions in; extensions that share a
|
|
19
|
+
priority keep it, so it is the order they were listed in when the runtime did the listing. What a plugin
|
|
20
|
+
builds on (the session store, the permission policy) is handed over by the runtime by name, see
|
|
21
|
+
:mod:`zettcode.engine.plugins.services`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
from zettcode.config import ModelConfig
|
|
30
|
+
from zettcode.engine.dialogue.side import SideQuestions
|
|
31
|
+
from zettcode.engine.extensions.capabilities import ModelCapabilities
|
|
32
|
+
from zettcode.engine.extensions.compaction import CompactionCounter, OnDemandCompaction
|
|
33
|
+
from zettcode.engine.extensions.context import ContextExtension
|
|
34
|
+
from zettcode.engine.extensions.harness_switch import HarnessSwitch
|
|
35
|
+
from zettcode.engine.extensions.tool_output import ToolOutputExtension
|
|
36
|
+
from zettcode.engine.extensions.truncation import LongOutputExtension
|
|
37
|
+
from zettcode.engine.extensions.usage import UsageExtension
|
|
38
|
+
from zettcode.engine.permission import ShellDenyExtension, ShellPolicy
|
|
39
|
+
from zettcode.engine.permission.approval import ShellApprovalMemory
|
|
40
|
+
from zettcode.engine.plugins.context import PluginContext
|
|
41
|
+
from zettcode.engine.plugins.plugin import Plugin, PluginSetting
|
|
42
|
+
from zettcode.engine.plugins.services import (
|
|
43
|
+
APPROVAL,
|
|
44
|
+
CAPABILITIES,
|
|
45
|
+
COMPACTION,
|
|
46
|
+
COMPACTION_COUNTER,
|
|
47
|
+
CONTEXT,
|
|
48
|
+
HARNESS,
|
|
49
|
+
MESSENGER,
|
|
50
|
+
MODELS,
|
|
51
|
+
PERSISTENCE,
|
|
52
|
+
POLICY,
|
|
53
|
+
SELECTED_MODEL,
|
|
54
|
+
SIDES,
|
|
55
|
+
TODOS,
|
|
56
|
+
USAGE,
|
|
57
|
+
)
|
|
58
|
+
from zettcode.engine.storage import SessionStore
|
|
59
|
+
from zettcode.mentions import FileMentions, SkillMentions
|
|
60
|
+
|
|
61
|
+
from .messenger import Messenger
|
|
62
|
+
|
|
63
|
+
if TYPE_CHECKING: # pragma: no cover - annotations only
|
|
64
|
+
from collections.abc import Mapping
|
|
65
|
+
|
|
66
|
+
from zett_agent.extensions.base import AgentExtension
|
|
67
|
+
from zett_agent.jit_harness import JITToolHost
|
|
68
|
+
|
|
69
|
+
from zettcode.config import ZettCodeConfig
|
|
70
|
+
from zettcode.engine.plugins import Plugins
|
|
71
|
+
from zettcode.engine.plugins.state import AgentBuild
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SkillResources(Plugin):
|
|
75
|
+
"""The builtin ``@`` resource provider: the skills on this machine.
|
|
76
|
+
|
|
77
|
+
Registering it here rather than inside the shell keeps the shell generic: it
|
|
78
|
+
merges whatever providers the bundle holds, and the skills are simply the
|
|
79
|
+
one the repository ships.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
name = "skills"
|
|
83
|
+
setting = PluginSetting("skills_enabled", "skills")
|
|
84
|
+
|
|
85
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
86
|
+
"""Offer the discovered skills as ``@`` resources, when skills are on."""
|
|
87
|
+
if ctx.config.skills_enabled:
|
|
88
|
+
ctx.agent.register_mention(SkillMentions(ctx.config))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FileResources(Plugin):
|
|
92
|
+
"""The ``@`` resource provider for the workspace's own files.
|
|
93
|
+
|
|
94
|
+
Registered after the skills, so a name both could offer is the skill: a
|
|
95
|
+
repository holds a file called ``test`` and a skill is the more specific
|
|
96
|
+
thing to mean. Nothing turns this off, because a workspace always has files.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
name = "files"
|
|
100
|
+
|
|
101
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
102
|
+
"""Offer the workspace's files as ``@`` resources."""
|
|
103
|
+
ctx.agent.register_mention(FileMentions(ctx.config.workspace))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CodingTools(Plugin):
|
|
107
|
+
"""The coding bundle: the file tools, search and the shell tool."""
|
|
108
|
+
|
|
109
|
+
name = "coding"
|
|
110
|
+
description = "The file, search and shell tools"
|
|
111
|
+
|
|
112
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
113
|
+
"""Register the bundle; it is the largest import the agent needs, so it waits for the build."""
|
|
114
|
+
|
|
115
|
+
policy = ctx.service(POLICY, ShellPolicy)
|
|
116
|
+
|
|
117
|
+
def build(_: AgentBuild) -> AgentExtension:
|
|
118
|
+
from zettcode.engine.extensions.files import CodingBundle
|
|
119
|
+
|
|
120
|
+
return CodingBundle(policy)
|
|
121
|
+
|
|
122
|
+
ctx.agent.register_extension_factory("CodingBundle", build)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ToolOutput(Plugin):
|
|
126
|
+
"""Moves a long tool result into the session's folder before it is stored or sent anywhere."""
|
|
127
|
+
|
|
128
|
+
name = "tool-output"
|
|
129
|
+
description = "Keeps long tool output in the session folder"
|
|
130
|
+
|
|
131
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
132
|
+
"""Register the extension over the session store."""
|
|
133
|
+
ctx.agent.register_extension(ToolOutputExtension(ctx.service(PERSISTENCE, SessionStore)))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class LongOutput(Plugin):
|
|
137
|
+
"""Keeps a result over a tool's budget in a file, and sends its start and its end instead.
|
|
138
|
+
|
|
139
|
+
The tools in this repository bound what they return themselves; this is the backstop for the ones that come
|
|
140
|
+
from elsewhere — an MCP server, a subagent, a plugin — so one long answer cannot fill the context of every
|
|
141
|
+
later request in the session.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
name = "long-output"
|
|
145
|
+
description = "Keeps over-long tool results in the session folder"
|
|
146
|
+
|
|
147
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
148
|
+
"""Register the extension over the session store."""
|
|
149
|
+
ctx.agent.register_extension(LongOutputExtension(ctx.service(PERSISTENCE, SessionStore)))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Capabilities(Plugin):
|
|
153
|
+
"""What the configured models can do, so a tool is not offered to one that cannot use it."""
|
|
154
|
+
|
|
155
|
+
name = "capabilities"
|
|
156
|
+
description = "Filters tools by what the model takes"
|
|
157
|
+
|
|
158
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
159
|
+
"""Provide the filter, over the runtime's own table of the models it has built."""
|
|
160
|
+
capabilities = ModelCapabilities(ctx.service(MODELS, dict))
|
|
161
|
+
ctx.agent.provide(CAPABILITIES, capabilities)
|
|
162
|
+
ctx.agent.register_extension(capabilities)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Subagents(Plugin):
|
|
166
|
+
"""The ``task`` tool: children that work on a delegated piece of the job in their own sessions."""
|
|
167
|
+
|
|
168
|
+
name = "subagents"
|
|
169
|
+
description = "The task tool and its profiles"
|
|
170
|
+
setting = PluginSetting("subagent_enabled", "harness", "subagent")
|
|
171
|
+
|
|
172
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
173
|
+
"""Register the tool when ``[harness] subagent`` is on, built once the provider exists."""
|
|
174
|
+
config = ctx.config
|
|
175
|
+
if not config.subagent_enabled:
|
|
176
|
+
return
|
|
177
|
+
persistence = ctx.service(PERSISTENCE, SessionStore)
|
|
178
|
+
capabilities = ctx.service(CAPABILITIES, ModelCapabilities)
|
|
179
|
+
messaging = config.subagent_messaging
|
|
180
|
+
|
|
181
|
+
def build(agent: AgentBuild) -> AgentExtension:
|
|
182
|
+
# Imported here: the module pulls in the SQLite store and SQLAlchemy, which a launch that
|
|
183
|
+
# never runs a child should not pay for on the way to the first frame.
|
|
184
|
+
from zettcode.engine.extensions.subagents import subagent_extension
|
|
185
|
+
|
|
186
|
+
return subagent_extension(
|
|
187
|
+
model=agent.model,
|
|
188
|
+
persistence=persistence,
|
|
189
|
+
capabilities=capabilities,
|
|
190
|
+
# The parent's project instructions go to its children too.
|
|
191
|
+
workspace=config.workspace if config.agents_md_enabled else None,
|
|
192
|
+
max_iterations=config.subagent_max_iterations,
|
|
193
|
+
shares=_shared_with_children(messaging=messaging),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
ctx.agent.register_extension_factory("ParentModelSubAgents", build)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _shared_with_children(*, messaging: bool) -> Callable[[AgentExtension], bool]:
|
|
200
|
+
"""Return what decides, for each extension the parent runs, whether a ``coding`` child gets it too.
|
|
201
|
+
|
|
202
|
+
A child is the parent without the complicated parts, so this lists what is *not* shared:
|
|
203
|
+
|
|
204
|
+
* what is part of every agent and is not its to share: the framework's own internal-message and steering
|
|
205
|
+
extensions, which an agent adds for itself and refuses to be given;
|
|
206
|
+
* ``task``, so a child cannot start another child;
|
|
207
|
+
* the just-in-time harness, whose tools are code the model writes and runs in this process;
|
|
208
|
+
* ``list_sessions`` and ``send_message``, unless ``[harness] subagent_messaging`` asks for them;
|
|
209
|
+
* ``ask_user``: a child works until it has finished and answers the parent, so a question put to the person
|
|
210
|
+
in the middle of it would be a conversation the person never started. What it needs to know has to be in
|
|
211
|
+
the task;
|
|
212
|
+
* what belongs to the conversation the person is having: the plan panel (``todo_write``, whose list the shell
|
|
213
|
+
draws for the parent's session and would never show for a child's), the ``/btw`` boundary, the token and
|
|
214
|
+
context counters of the session, the plugin host that drives third-party hooks;
|
|
215
|
+
* what a child is given of its own in another form: its session store, the capability filter, and the
|
|
216
|
+
project instructions and tool guidance (each child reads and renders its own).
|
|
217
|
+
|
|
218
|
+
Everything else is shared as it is, which is what puts the child's shell under the parent's permission
|
|
219
|
+
level and approval memory, and gives it the parent's skills, questions and MCP servers.
|
|
220
|
+
"""
|
|
221
|
+
# Imported here: the framework's subagent and harness modules are what a launch that never delegates
|
|
222
|
+
# should not load.
|
|
223
|
+
from zett_agent.extensions.agents_md import AgentsMdExtension
|
|
224
|
+
from zett_agent.extensions.ask_user import AskUserExtension
|
|
225
|
+
from zett_agent.extensions.internal_message import InternalMessageExtension
|
|
226
|
+
from zett_agent.extensions.steering import SteeringExtension
|
|
227
|
+
from zett_agent.extensions.subagent import SubAgentExtension
|
|
228
|
+
from zett_agent.extensions.todo import TodoWriteExtension
|
|
229
|
+
from zett_agent.extensions.tool_guidelines import ToolGuidelinesExtension
|
|
230
|
+
|
|
231
|
+
from zettcode.engine.extensions.harness import SwitchedHarness
|
|
232
|
+
from zettcode.engine.plugins.extension import PluginExtension
|
|
233
|
+
|
|
234
|
+
from .extensions.messaging import SessionMessagingExtension
|
|
235
|
+
|
|
236
|
+
private: tuple[type, ...] = (
|
|
237
|
+
InternalMessageExtension,
|
|
238
|
+
SteeringExtension,
|
|
239
|
+
SubAgentExtension,
|
|
240
|
+
SwitchedHarness,
|
|
241
|
+
AskUserExtension,
|
|
242
|
+
TodoWriteExtension,
|
|
243
|
+
SideQuestions,
|
|
244
|
+
UsageExtension,
|
|
245
|
+
ContextExtension,
|
|
246
|
+
PluginExtension,
|
|
247
|
+
SessionStore,
|
|
248
|
+
ModelCapabilities,
|
|
249
|
+
AgentsMdExtension,
|
|
250
|
+
ToolGuidelinesExtension,
|
|
251
|
+
)
|
|
252
|
+
if not messaging:
|
|
253
|
+
private = (*private, SessionMessagingExtension)
|
|
254
|
+
return lambda extension: not isinstance(extension, private)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class Messaging(Plugin):
|
|
258
|
+
"""The tools that talk to the other sessions open on this machine."""
|
|
259
|
+
|
|
260
|
+
name = "messaging"
|
|
261
|
+
description = "The session messaging tools"
|
|
262
|
+
|
|
263
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
264
|
+
"""Register the tools when the configuration gave the runtime a line to the other sessions."""
|
|
265
|
+
messenger = ctx.find(MESSENGER, Messenger)
|
|
266
|
+
if messenger is None:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
def build(_: AgentBuild) -> AgentExtension:
|
|
270
|
+
from .extensions.messaging import SessionMessagingExtension
|
|
271
|
+
|
|
272
|
+
return SessionMessagingExtension(messenger)
|
|
273
|
+
|
|
274
|
+
ctx.agent.register_extension_factory("SessionMessagingExtension", build)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class ShellPermission(Plugin):
|
|
278
|
+
"""Decides which shell commands run: ``deny`` refuses them, anything else is asked about or let through.
|
|
279
|
+
|
|
280
|
+
The deny extension runs before the approval one (its priority is lower), because the approval can ask
|
|
281
|
+
but cannot refuse on the reader's behalf. Both read the one policy the runtime owns, so changing the
|
|
282
|
+
level in ``/permission`` applies to the next command.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
name = "shell-permission"
|
|
286
|
+
description = "Approval and refusal of shell commands"
|
|
287
|
+
|
|
288
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
289
|
+
"""Register the deny and approval extensions and provide the approval, whose memory the shell reads."""
|
|
290
|
+
from zett_agent.extensions.shell_approval import ShellApprovalExtension
|
|
291
|
+
|
|
292
|
+
policy = ctx.service(POLICY, ShellPolicy)
|
|
293
|
+
approval = ShellApprovalExtension(ShellApprovalMemory(policy))
|
|
294
|
+
ctx.agent.provide(APPROVAL, approval)
|
|
295
|
+
ctx.agent.register_extension(ShellDenyExtension(policy))
|
|
296
|
+
ctx.agent.register_extension(approval)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class Harness(Plugin):
|
|
300
|
+
"""The just-in-time harness: the model keeps small Python tools of its own, when the reader has it on.
|
|
301
|
+
|
|
302
|
+
The extension is always registered and inert until the switch is on, so ``/harness on`` reaches the next
|
|
303
|
+
request of a client that was already built. Each session keeps its tools in its own folder under
|
|
304
|
+
``config.harness_dir``, which is ``~/.zettcode/harness`` unless the configuration object says otherwise.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
name = "harness"
|
|
308
|
+
description = "The just-in-time tool harness, off by default"
|
|
309
|
+
setting = PluginSetting("jit_harness", "harness", "jit")
|
|
310
|
+
|
|
311
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
312
|
+
"""Register the switchable extension; the framework's harness is imported when the agent is built."""
|
|
313
|
+
from zettcode.engine.harness_paths import harness_root
|
|
314
|
+
|
|
315
|
+
switch = ctx.service(HARNESS, HarnessSwitch)
|
|
316
|
+
root = harness_root(ctx.config)
|
|
317
|
+
policy = ctx.service(POLICY, ShellPolicy)
|
|
318
|
+
|
|
319
|
+
def host() -> JITToolHost:
|
|
320
|
+
"""Where one session's repository code runs.
|
|
321
|
+
|
|
322
|
+
A child process either way, so a crash, a hang or a runaway allocation in code the model wrote stays
|
|
323
|
+
out of this window. The boundary comes from the policy — none at `full`, the boundary `safe` uses below it — and `None` on a machine
|
|
324
|
+
that has no sandbox.
|
|
325
|
+
"""
|
|
326
|
+
from zett_agent.jit_harness import SubprocessToolHost
|
|
327
|
+
|
|
328
|
+
return SubprocessToolHost(sandbox=policy.jit_sandbox())
|
|
329
|
+
|
|
330
|
+
def boundary() -> str:
|
|
331
|
+
"""What a session's child was built around: the sandbox itself, or none.
|
|
332
|
+
|
|
333
|
+
The *level* is not the key. `ask` and `safe` hand the harness the same sandbox, and restarting a child for
|
|
334
|
+
nothing costs the model its loaded repository — the next tool call fails until it is loaded again — so this
|
|
335
|
+
changes only when the boundary does.
|
|
336
|
+
"""
|
|
337
|
+
sandbox = policy.jit_sandbox()
|
|
338
|
+
return "none" if sandbox is None else type(sandbox).__name__ + str(id(sandbox))
|
|
339
|
+
|
|
340
|
+
def build(_: AgentBuild) -> AgentExtension:
|
|
341
|
+
from zettcode.engine.extensions.harness import SwitchedHarness
|
|
342
|
+
|
|
343
|
+
return SwitchedHarness(
|
|
344
|
+
root,
|
|
345
|
+
switch,
|
|
346
|
+
host_factory=host,
|
|
347
|
+
boundary=boundary,
|
|
348
|
+
unbounded=policy.sandbox_missing,
|
|
349
|
+
legacy_root=ctx.config.harness_dir,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
ctx.agent.register_extension_factory("SwitchedHarness", build)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class SessionLog(Plugin):
|
|
356
|
+
"""Writes the conversation to the session's log as it happens, and restores it on resume."""
|
|
357
|
+
|
|
358
|
+
name = "session-log"
|
|
359
|
+
description = "Session persistence"
|
|
360
|
+
|
|
361
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
362
|
+
"""Register the store, which is both what the shell reads sessions from and an extension."""
|
|
363
|
+
ctx.agent.register_extension(ctx.service(PERSISTENCE, SessionStore))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class UsageTracking(Plugin):
|
|
367
|
+
"""Counts the tokens each session has spent, for the status line and ``/usage``."""
|
|
368
|
+
|
|
369
|
+
name = "usage-tracking"
|
|
370
|
+
description = "Token accounting"
|
|
371
|
+
|
|
372
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
373
|
+
"""Provide the counters and register the extension that fills them."""
|
|
374
|
+
# The count comes from the encoding compaction has already warmed, and answers for a provider that reports no
|
|
375
|
+
# usage of its own; the rate's numerator is the provider's count.
|
|
376
|
+
counter = ctx.service(COMPACTION_COUNTER, CompactionCounter)
|
|
377
|
+
usage = UsageExtension(count_context=counter.count)
|
|
378
|
+
ctx.agent.provide(USAGE, usage)
|
|
379
|
+
ctx.agent.register_extension(usage)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class ContextTracking(Plugin):
|
|
383
|
+
"""Remembers the last assembled request of each session, for ``/context``."""
|
|
384
|
+
|
|
385
|
+
name = "context-tracking"
|
|
386
|
+
description = "What the last request was made of"
|
|
387
|
+
|
|
388
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
389
|
+
"""Provide the recorder and register the extension that fills it."""
|
|
390
|
+
context = ContextExtension()
|
|
391
|
+
ctx.agent.provide(CONTEXT, context)
|
|
392
|
+
ctx.agent.register_extension(context)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class SideQuestionBoundary(Plugin):
|
|
396
|
+
"""``/btw``: questions answered on a read-only run beside the conversation."""
|
|
397
|
+
|
|
398
|
+
name = "side-questions"
|
|
399
|
+
description = "The read-only boundary side questions run behind"
|
|
400
|
+
|
|
401
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
402
|
+
"""Provide the boundary, which the shell marks before it starts a side run, and register it."""
|
|
403
|
+
sides = SideQuestions()
|
|
404
|
+
ctx.agent.provide(SIDES, sides)
|
|
405
|
+
ctx.agent.register_extension(sides)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class ToolGuidance(Plugin):
|
|
409
|
+
"""The guidance each tool carries: its call shapes, guidelines and return type, in the tool's own description."""
|
|
410
|
+
|
|
411
|
+
name = "tool-guidelines"
|
|
412
|
+
description = "Per-tool guidance in each tool's description"
|
|
413
|
+
|
|
414
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
415
|
+
"""Register the extension; it is small, but it belongs with the others that wait for the build."""
|
|
416
|
+
|
|
417
|
+
def build(_: AgentBuild) -> AgentExtension:
|
|
418
|
+
from zettcode.engine.extensions.guidance import tool_guidance
|
|
419
|
+
|
|
420
|
+
return tool_guidance()
|
|
421
|
+
|
|
422
|
+
ctx.agent.register_extension_factory("ToolGuidelinesExtension", build)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class AskUser(Plugin):
|
|
426
|
+
"""The ``ask_user`` tool: how the model puts a question to the reader instead of guessing."""
|
|
427
|
+
|
|
428
|
+
name = "ask-user"
|
|
429
|
+
description = "The ask_user tool"
|
|
430
|
+
setting = PluginSetting("ask_user_enabled", "ask_user")
|
|
431
|
+
|
|
432
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
433
|
+
"""Hand the agent the tool, unless the configuration turned it off."""
|
|
434
|
+
if not ctx.config.ask_user_enabled:
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
def build(_: AgentBuild) -> AgentExtension:
|
|
438
|
+
from zett_agent.extensions.ask_user import AskUserExtension
|
|
439
|
+
|
|
440
|
+
return AskUserExtension()
|
|
441
|
+
|
|
442
|
+
ctx.agent.register_extension_factory("AskUserExtension", build)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class ProjectInstructions(Plugin):
|
|
446
|
+
"""The workspace's ``AGENTS.md`` files, read again for every request so an edit applies at once."""
|
|
447
|
+
|
|
448
|
+
name = "project-instructions"
|
|
449
|
+
description = "The workspace's AGENTS.md instructions"
|
|
450
|
+
setting = PluginSetting("agents_md_enabled", "agents_md")
|
|
451
|
+
|
|
452
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
453
|
+
"""Hand the agent the instructions, unless the configuration turned them off."""
|
|
454
|
+
config = ctx.config
|
|
455
|
+
if not config.agents_md_enabled:
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
def build(_: AgentBuild) -> AgentExtension:
|
|
459
|
+
from zett_agent.extensions.agents_md import AgentsMdExtension
|
|
460
|
+
|
|
461
|
+
return AgentsMdExtension(config.workspace)
|
|
462
|
+
|
|
463
|
+
ctx.agent.register_extension_factory("AgentsMdExtension", build)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class SkillTool(Plugin):
|
|
467
|
+
"""The skills the model can load: their list in the prompt and the tool that reads one."""
|
|
468
|
+
|
|
469
|
+
name = "skill-tool"
|
|
470
|
+
description = "Skills the model can load"
|
|
471
|
+
|
|
472
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
473
|
+
"""Hand the agent the skills found under the configured roots, when skills are on."""
|
|
474
|
+
config = ctx.config
|
|
475
|
+
if not config.skills_enabled:
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
def build(_: AgentBuild) -> AgentExtension:
|
|
479
|
+
from zett_agent.extensions.skill import SkillExtension
|
|
480
|
+
|
|
481
|
+
return SkillExtension(config.skill_search_roots())
|
|
482
|
+
|
|
483
|
+
ctx.agent.register_extension_factory("SkillExtension", build)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class McpTools(Plugin):
|
|
487
|
+
"""The tools MCP servers offer. A server that is down is skipped, never fatal."""
|
|
488
|
+
|
|
489
|
+
name = "mcp"
|
|
490
|
+
description = "MCP servers"
|
|
491
|
+
setting = PluginSetting("mcp_enabled", "mcp")
|
|
492
|
+
|
|
493
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
494
|
+
"""Register the extension when MCP is on; a run with no server file gets none at all."""
|
|
495
|
+
config = ctx.config
|
|
496
|
+
if not config.mcp_enabled:
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
def build(_: AgentBuild) -> AgentExtension | None:
|
|
500
|
+
from .extensions.mcp import build_scoped_mcp_extension
|
|
501
|
+
|
|
502
|
+
return build_scoped_mcp_extension(config.mcp_files())
|
|
503
|
+
|
|
504
|
+
ctx.agent.register_extension_factory("ReportingMcpExtension", build)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class TodoPlan(Plugin):
|
|
508
|
+
"""The ``todo_write`` tool, and the plan it keeps, shown in the shell's panel.
|
|
509
|
+
|
|
510
|
+
The extension is provided as a service whether or not the tool is on, because the shell asks it for the
|
|
511
|
+
plan on every turn and an extension nobody registered simply has none to show. Only
|
|
512
|
+
``[harness] todowrite`` decides whether the model is given the tool.
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
name = "todos"
|
|
516
|
+
description = "The todo_write tool and the plan it keeps"
|
|
517
|
+
setting = PluginSetting("todowrite_enabled", "harness", "todowrite")
|
|
518
|
+
|
|
519
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
520
|
+
"""Provide the extension, and hand it to the agent when the tool is on."""
|
|
521
|
+
from zett_agent.extensions.todo import TodoWriteExtension
|
|
522
|
+
|
|
523
|
+
extension = TodoWriteExtension()
|
|
524
|
+
ctx.agent.provide(TODOS, extension)
|
|
525
|
+
if ctx.config.todowrite_enabled:
|
|
526
|
+
ctx.agent.register_extension(extension)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class SelfCompaction(Plugin):
|
|
530
|
+
"""The switch for letting the model compact its own context.
|
|
531
|
+
|
|
532
|
+
The work is the runtime's: ``[compaction] self`` is the default, and a session that chose otherwise keeps its
|
|
533
|
+
choice. This plugin exists so `/plugins` can show and write that default like any other plugin setting.
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
name = "self-compaction"
|
|
537
|
+
description = "Let the model compact its own context"
|
|
538
|
+
setting = PluginSetting("self_compaction", "compaction", "self")
|
|
539
|
+
|
|
540
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
541
|
+
"""Nothing to register: the setting decides the runtime's policy, not an object here."""
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class Compaction(Plugin):
|
|
545
|
+
"""Shortens the conversation when it nears the model's window, or when the reader asks."""
|
|
546
|
+
|
|
547
|
+
name = "compaction"
|
|
548
|
+
description = "Context compaction"
|
|
549
|
+
|
|
550
|
+
def activate(self, ctx: PluginContext) -> None:
|
|
551
|
+
"""Provide the policy, sized for the model the run starts on; the runtime resizes it on a switch.
|
|
552
|
+
|
|
553
|
+
A machine with no model configured has nothing to size it from: the policy is provided with the
|
|
554
|
+
extension's own defaults — nothing runs to compact before there is a model — and ``use_model`` sizes it
|
|
555
|
+
the moment the first entry is written.
|
|
556
|
+
"""
|
|
557
|
+
selected = ctx.find(SELECTED_MODEL, ModelConfig)
|
|
558
|
+
counter = ctx.service(COMPACTION_COUNTER, CompactionCounter)
|
|
559
|
+
if selected is None:
|
|
560
|
+
compaction = OnDemandCompaction(None, count_tokens=counter.count)
|
|
561
|
+
else:
|
|
562
|
+
compaction = OnDemandCompaction(
|
|
563
|
+
None,
|
|
564
|
+
max_tokens=selected.compaction_max_tokens,
|
|
565
|
+
keep_recent_tokens=selected.compaction_keep_tokens,
|
|
566
|
+
count_tokens=counter.count,
|
|
567
|
+
)
|
|
568
|
+
ctx.agent.provide(COMPACTION, compaction)
|
|
569
|
+
ctx.agent.register_extension(compaction)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
#: The agent-side plugins in the order the agent receives their extensions.
|
|
573
|
+
AGENT_PLUGINS: tuple[Plugin[PluginContext], ...] = (
|
|
574
|
+
SkillResources(),
|
|
575
|
+
FileResources(),
|
|
576
|
+
CodingTools(),
|
|
577
|
+
ToolOutput(),
|
|
578
|
+
LongOutput(),
|
|
579
|
+
Capabilities(),
|
|
580
|
+
Subagents(),
|
|
581
|
+
Messaging(),
|
|
582
|
+
ShellPermission(),
|
|
583
|
+
Harness(),
|
|
584
|
+
SessionLog(),
|
|
585
|
+
TodoPlan(),
|
|
586
|
+
UsageTracking(),
|
|
587
|
+
ContextTracking(),
|
|
588
|
+
SideQuestionBoundary(),
|
|
589
|
+
ToolGuidance(),
|
|
590
|
+
AskUser(),
|
|
591
|
+
ProjectInstructions(),
|
|
592
|
+
SkillTool(),
|
|
593
|
+
McpTools(),
|
|
594
|
+
SelfCompaction(),
|
|
595
|
+
Compaction(),
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def load_agent_plugins(config: ZettCodeConfig, shared: Mapping[str, object]) -> Plugins:
|
|
600
|
+
"""Load these builtins, plus the distributions a reader enabled: the loader a runtime is built with.
|
|
601
|
+
|
|
602
|
+
The engine's own default loads the installed plugins and no built-in at all, because the engine does not ship
|
|
603
|
+
this list. Whoever builds the runtime says which agent it wants: the SDK passes this, and the terminal passes
|
|
604
|
+
:func:`zettcode.app.plugins.loader.terminal_plugins`, which adds the plugins only a screen can hold.
|
|
605
|
+
"""
|
|
606
|
+
from zettcode.engine.plugins import load_plugins
|
|
607
|
+
|
|
608
|
+
return load_plugins(config, builtins=AGENT_PLUGINS, shared=shared)
|