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.
@@ -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)