zettcode-engine 0.4.1__tar.gz → 0.4.2__tar.gz

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 (99) hide show
  1. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/.gitignore +5 -0
  2. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/PKG-INFO +3 -2
  3. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/pyproject.toml +3 -1
  4. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/budget.py +1 -1
  5. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/config.py +26 -12
  6. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/context.py +20 -37
  7. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/turns.py +61 -15
  8. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/core.py +193 -18
  9. zettcode_engine-0.4.2/src/zettcode/engine/delivery.py +51 -0
  10. zettcode_engine-0.4.2/src/zettcode/engine/delivery_runtime.py +56 -0
  11. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/dialogue/side.py +1 -1
  12. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/export/__init__.py +3 -3
  13. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/capabilities.py +1 -1
  14. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/compaction.py +4 -0
  15. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/files.py +4 -4
  16. zettcode_engine-0.4.2/src/zettcode/engine/inbox.py +131 -0
  17. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/permission/__init__.py +1 -1
  18. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/permission/sandbox.py +1 -1
  19. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/__init__.py +26 -1
  20. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/commands.py +35 -0
  21. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/context.py +89 -11
  22. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/extension.py +6 -1
  23. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/groups.py +37 -0
  24. zettcode_engine-0.4.2/src/zettcode/engine/plugins/llm.py +56 -0
  25. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/loader.py +166 -57
  26. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/plugin.py +46 -0
  27. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/services.py +32 -3
  28. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/switches.py +7 -1
  29. zettcode_engine-0.4.2/src/zettcode/engine/plugins/triggers.py +273 -0
  30. zettcode_engine-0.4.2/src/zettcode/engine/service_contracts.py +101 -0
  31. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/storage/__init__.py +2 -1
  32. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/storage/locks.py +7 -7
  33. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/storage/store.py +15 -2
  34. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/mention.py +9 -2
  35. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/paths.py +4 -0
  36. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/plugin_tui.py +1 -1
  37. zettcode_engine-0.4.2/src/zettcode/relay.py +35 -0
  38. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/statusline_parts.py +1 -1
  39. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/version.py +1 -1
  40. zettcode_engine-0.4.2/src/zettcode/wire.py +68 -0
  41. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/wording.py +1 -1
  42. zettcode_engine-0.4.1/src/zettcode/engine/extensions/harness.py +0 -208
  43. zettcode_engine-0.4.1/src/zettcode/engine/extensions/tool_output.py +0 -153
  44. zettcode_engine-0.4.1/src/zettcode/engine/extensions/truncation.py +0 -156
  45. zettcode_engine-0.4.1/src/zettcode/engine/tools/__init__.py +0 -7
  46. zettcode_engine-0.4.1/src/zettcode/engine/tools/boundary.py +0 -56
  47. zettcode_engine-0.4.1/src/zettcode/engine/tools/files.py +0 -262
  48. zettcode_engine-0.4.1/src/zettcode/engine/tools/images.py +0 -32
  49. zettcode_engine-0.4.1/src/zettcode/engine/tools/output.py +0 -81
  50. zettcode_engine-0.4.1/src/zettcode/engine/tools/shell.py +0 -181
  51. zettcode_engine-0.4.1/src/zettcode/engine/tools/workspace.py +0 -271
  52. zettcode_engine-0.4.1/src/zettcode/relay.py +0 -267
  53. zettcode_engine-0.4.1/src/zettcode/wire.py +0 -30
  54. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/_compat.py +0 -0
  55. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/config_edit.py +0 -0
  56. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/__init__.py +0 -0
  57. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/__init__.py +0 -0
  58. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/base.py +0 -0
  59. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/exports.py +0 -0
  60. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/interaction.py +0 -0
  61. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/sessions.py +0 -0
  62. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/agent/settings.py +0 -0
  63. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/dialogue/__init__.py +0 -0
  64. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/dialogue/ask.py +0 -0
  65. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/dialogue/title.py +0 -0
  66. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/export/trace/script.js +0 -0
  67. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/export/trace/style.css +0 -0
  68. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/export/trace/template.html +0 -0
  69. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/__init__.py +0 -0
  70. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/context.py +0 -0
  71. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/guidance.py +0 -0
  72. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/harness_switch.py +0 -0
  73. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/mcp_notice.py +0 -0
  74. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/relay.py +0 -0
  75. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/subagents.py +0 -0
  76. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/extensions/usage.py +0 -0
  77. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/harness_paths.py +0 -0
  78. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/permission/approval.py +0 -0
  79. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/permission/readonly.py +0 -0
  80. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/pipeline.py +0 -0
  81. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/graph.py +0 -0
  82. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/mixins.py +0 -0
  83. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/settings.py +0 -0
  84. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/plugins/state.py +0 -0
  85. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/py.typed +0 -0
  86. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/server_tools.py +0 -0
  87. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/storage/layout.py +0 -0
  88. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/storage/ledger.py +0 -0
  89. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/storage/metadata.py +0 -0
  90. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/engine/storage/records.py +0 -0
  91. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/error_text.py +0 -0
  92. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/filelock.py +0 -0
  93. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/logging_setup.py +0 -0
  94. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/modes.py +0 -0
  95. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/processes.py +0 -0
  96. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/prompt.py +0 -0
  97. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/providers.py +0 -0
  98. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/py.typed +0 -0
  99. {zettcode_engine-0.4.1 → zettcode_engine-0.4.2}/src/zettcode/registry.py +0 -0
@@ -35,3 +35,8 @@ docs/internal/palettes.html
35
35
 
36
36
  # The release order is generated from the pyproject.toml files (`make order`); it is not kept in git.
37
37
  packages/order.txt
38
+
39
+ # The desktop and phone apps build the gateway's page into their own folders, and electron-builder writes its
40
+ # installers under release/.
41
+ clients/desktop/renderer/
42
+ clients/desktop/release/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: zettcode-engine
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: The agent core of ZettCode: the runtime over zett-agent, and the leaf types it is written on
5
5
  Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
6
6
  Author: Chang-LeHung
@@ -18,6 +18,7 @@ Requires-Python: >=3.10
18
18
  Requires-Dist: exceptiongroup<2,>=1.2; python_version < '3.11'
19
19
  Requires-Dist: pydantic<3,>=2
20
20
  Requires-Dist: tomli<3,>=2; python_version < '3.11'
21
- Requires-Dist: zett-agent<0.3,>=0.2.7
21
+ Requires-Dist: zett-agent<0.3,>=0.2.10
22
+ Requires-Dist: zettcode-tools
22
23
  Provides-Extra: tiktoken
23
24
  Requires-Dist: tiktoken<1,>=0.12; extra == 'tiktoken'
@@ -17,8 +17,10 @@ classifiers = [
17
17
  "Typing :: Typed",
18
18
  ]
19
19
  dependencies = [
20
+ # The coding tools the runtime hands the model, a leaf of its own beneath the engine.
21
+ "zettcode-tools",
20
22
  # The runtime this is a core over. Imported directly: the agent, the message types, the approval mode.
21
- "zett-agent>=0.2.7,<0.3",
23
+ "zett-agent>=0.2.10,<0.3",
22
24
  # The session store's records are pydantic models.
23
25
  "pydantic>=2,<3",
24
26
  # The stdlib gained both of these in 3.11; older runtimes take the backports.
@@ -1,6 +1,6 @@
1
1
  """What a context budget is made of: the vocabulary the engine measures with and the views draw with.
2
2
 
3
- A leaf with no imports, shared like :mod:`zettcode.statusline` and :mod:`zettcode.providers`, so the engine that counts
3
+ A leaf with no imports, shared like :mod:`zettcode.statusline_parts` and :mod:`zettcode.providers`, so the engine that counts
4
4
  the request and the page that shows the count agree on the names without either importing the other.
5
5
  """
6
6
 
@@ -112,7 +112,7 @@ SCOPES = frozenset({"global", "workspace", "both"})
112
112
  DEFAULT_SCOPE = "both"
113
113
 
114
114
  #: Keys the ``[plugins]`` table may set.
115
- PLUGIN_KEYS = frozenset({"enabled", "disable"})
115
+ PLUGIN_KEYS = frozenset({"enabled", "disable", "enable"})
116
116
 
117
117
  #: Keys the ``[update]`` table may set.
118
118
  UPDATE_KEYS = frozenset({"enabled"})
@@ -448,16 +448,18 @@ class ZettCodeConfig:
448
448
  self_compaction: Whether a session lets the model request compaction unless it says otherwise;
449
449
  the default a new session starts with; the terminal's ``/plugins`` switch writes it.
450
450
  statusline_hidden: The names of the header and status-line parts the reader switched off in the
451
- ``[statusline]`` table (``ctx = false``); see :mod:`zettcode.statusline`. Every part not named
451
+ ``[statusline]`` table (``ctx = false``); see :mod:`zettcode.presenters.statusline`. Every part not named
452
452
  is shown.
453
453
  plugins_enabled: Load third-party plugins published under the
454
- ``zettcode.plugins`` entry-point group. The builtin rows always
454
+ ``zettcode.plugin`` entry-point group. The builtin rows always
455
455
  load, so switching this off leaves the shell looking the same.
456
456
  disabled_plugins: Entry-point names not to load when
457
457
  :attr:`plugins_enabled` is true; a way to switch off one plugin
458
458
  without uninstalling it. This is the configuration file's own
459
459
  ``[plugins] disable`` list; the names the older switch file adds are merged in by
460
460
  :attr:`off_names`, so a switch can tell the two apart.
461
+ enabled_plugins: Entry-point names to load although the plugin itself says it is off by default
462
+ (``Plugin.default_enabled``); this is the configuration file's ``[plugins] enable`` list.
461
463
  plugin_settings: The ``[plugins.<name>]`` tables, by plugin name, exactly as written. A plugin declares
462
464
  what its own keys mean and what they default to (``ctx.settings``); the configuration only carries the
463
465
  reader's values, so a key no plugin knows is harmless and a plugin that ships later needs no change
@@ -517,6 +519,8 @@ class ZettCodeConfig:
517
519
  statusline_hidden: tuple[str, ...] = ()
518
520
  plugins_enabled: bool = True
519
521
  disabled_plugins: tuple[str, ...] = ()
522
+ #: Default-off plugins the reader asked to load; this is ``[plugins] enable``.
523
+ enabled_plugins: tuple[str, ...] = ()
520
524
  #: The reader's own keys for a plugin, from its ``[plugins.<name>]`` table; the plugin supplies the defaults.
521
525
  plugin_settings: Mapping[str, Mapping[str, object]] = field(default_factory=dict)
522
526
  subagent_enabled: bool = True
@@ -917,17 +921,19 @@ def read_disabled_plugins(path: Path | None = None) -> tuple[str, ...]:
917
921
  return tuple(name.strip() for name in names if isinstance(name, str) and name.strip())
918
922
 
919
923
 
920
- def _read_disabled_plugins(raw: object, source: Path) -> tuple[str, ...]:
921
- """Return the entry-point names the ``[plugins]`` table switches off."""
924
+ def _read_plugin_names(raw: object, source: Path, key: str) -> tuple[str, ...]:
925
+ """Return the entry-point names a ``[plugins]`` list holds, validating the shape."""
922
926
  if raw is None:
923
927
  return ()
924
928
  if not isinstance(raw, list) or any(not isinstance(entry, str) or not entry.strip() for entry in raw):
925
- raise ValueError(f"Config key 'plugins.disable' in {source} must be an array of non-empty strings")
929
+ raise ValueError(f"Config key 'plugins.{key}' in {source} must be an array of non-empty strings")
926
930
  return tuple(entry.strip() for entry in raw)
927
931
 
928
932
 
929
- def read_plugins_table(raw: object, source: Path) -> tuple[bool, tuple[str, ...], dict[str, dict[str, object]]]:
930
- """Return ``[plugins]`` as ``(enabled, disable, settings)``: the one reading of that table.
933
+ def read_plugins_table(
934
+ raw: object, source: Path
935
+ ) -> tuple[bool, tuple[str, ...], tuple[str, ...], dict[str, dict[str, object]]]:
936
+ """Return ``[plugins]`` as ``(enabled, disable, enable, settings)``: the one reading of that table.
931
937
 
932
938
  The configuration and ``zettcode plugin`` both need it, and the second must not build the whole settings object
933
939
  (that insists on a model). They used to each write the rules down, and the copies drifted: the command refused a
@@ -938,15 +944,20 @@ def read_plugins_table(raw: object, source: Path) -> tuple[bool, tuple[str, ...]
938
944
  source: The file, for messages.
939
945
 
940
946
  Returns:
941
- Whether installed plugins load at all, the entry-point names left out, and each plugin's own settings table by
942
- name.
947
+ Whether installed plugins load at all, the entry-point names left out, the default-off names asked for, and
948
+ each plugin's own settings table by name.
943
949
 
944
950
  Raises:
945
951
  ValueError: The table, a plugin's table, or a value in one is not what it must be.
946
952
  """
947
953
  own, settings = _read_plugins(raw, source)
948
954
  enabled = _typed(own.get("enabled", True), f"Config key 'plugins.enabled' in {source}", bool)
949
- return enabled, _read_disabled_plugins(own.get("disable"), source), settings
955
+ return (
956
+ enabled,
957
+ _read_plugin_names(own.get("disable"), source, "disable"),
958
+ _read_plugin_names(own.get("enable"), source, "enable"),
959
+ settings,
960
+ )
950
961
 
951
962
 
952
963
  def _read_theme_name(raw: object, source: Path) -> str | None:
@@ -1016,7 +1027,9 @@ def load_config(
1016
1027
  ask_user = _read_table(data.get("ask_user"), "ask_user", source, ASK_USER_KEYS)
1017
1028
  skills = _read_table(data.get("skills"), "skills", source, SKILL_KEYS)
1018
1029
  mcp = _read_table(data.get("mcp"), "mcp", source, MCP_KEYS)
1019
- plugins_enabled, disabled_plugins, plugin_settings = read_plugins_table(data.get("plugins"), source)
1030
+ plugins_enabled, disabled_plugins, enabled_plugins, plugin_settings = read_plugins_table(
1031
+ data.get("plugins"), source
1032
+ )
1020
1033
  transcript = _read_table(data.get("transcript"), "transcript", source, TRANSCRIPT_KEYS)
1021
1034
  update = _read_table(data.get("update"), "update", source, UPDATE_KEYS)
1022
1035
  harness = _read_table(data.get("harness"), "harness", source, HARNESS_KEYS)
@@ -1072,6 +1085,7 @@ def load_config(
1072
1085
  # The configuration file's own list. The legacy switch file's names are read where they are needed
1073
1086
  # (`zettcode.engine.plugins.switches`), so a switch can tell which list it may write.
1074
1087
  disabled_plugins=disabled_plugins,
1088
+ enabled_plugins=enabled_plugins,
1075
1089
  plugin_settings=plugin_settings,
1076
1090
  subagent_enabled=bool(_setting(harness, "subagent", bool, True)),
1077
1091
  subagent_max_iterations=_typed(
@@ -14,7 +14,7 @@ from zett_agent.extensions.events import CompactionEvent
14
14
  from zett_agent.messages import AnyMessage, SystemMessage
15
15
  from zett_agent.model import ToolDefinition
16
16
 
17
- from zettcode.engine.core import build_system_prompt
17
+ from zettcode.engine.core import ZettCodeEngine, build_system_prompt
18
18
  from zettcode.engine.extensions.context import ContextReport
19
19
  from zettcode.engine.extensions.usage import UsageSnapshot
20
20
  from zettcode.engine.storage import Session
@@ -22,6 +22,23 @@ from zettcode.engine.storage import Session
22
22
  from .base import AgentBase
23
23
 
24
24
 
25
+ async def report_of(runtime: ZettCodeEngine, session_id: str) -> ContextReport | None:
26
+ """Return what the next request carries, or ``None`` before it was assembled.
27
+
28
+ The tokenizer is prepared first; the total is the provider's count of this request, clamped out when it reaches
29
+ the window, because a window-sized figure is about something else. A rebuilt request is not counted locally:
30
+ its list carries messages the live request no longer does, so the provider's count is the only honest total.
31
+ ``window = 0`` means no model is configured; nothing is measured and no share is computed from it.
32
+ """
33
+ tokenizer = runtime.tokenizer
34
+ await tokenizer.prepare()
35
+ model = runtime.active_model
36
+ window = 0 if model is None else model.context_window
37
+ counted = runtime.usage.snapshot(session_id).context_tokens
38
+ reported = counted if 0 < counted < window else None
39
+ return runtime.context.report(session_id, window=window, tokenizer=tokenizer, reported=reported)
40
+
41
+
25
42
  class ContextMixin(AgentBase):
26
43
  """Report the request the next turn would send, and compact the conversation behind it."""
27
44
 
@@ -36,42 +53,8 @@ class ContextMixin(AgentBase):
36
53
  return self.runtime.usage.snapshot(self.session_id)
37
54
 
38
55
  async def context_report(self) -> ContextReport | None:
39
- """Return what the next request carries by source, or None before the first call.
40
-
41
- The shares come from the tokenizer, which is prepared first: it prefers
42
- tiktoken and falls back to a character estimate when that vocabulary
43
- cannot be opened, which the report names so the numbers are not mistaken
44
- for exact ones. The total comes from the provider when it counted this
45
- very request — it counts what it bills — and from the tokenizer when the
46
- request was primed from the store, whose stored figure belongs to a call
47
- the dialogue may have outgrown.
48
- """
49
- tokenizer = self.runtime.tokenizer
50
- await tokenizer.prepare()
51
- model = self.runtime.active_model
52
- # No model configured means no window to measure against. Nothing is reported either way: a request is
53
- # refused before one is assembled, so the report is None and no share is ever computed from this 0.
54
- window = 0 if model is None else model.context_window
55
- counted = self.runtime.usage.snapshot(self.session_id).context_tokens
56
- # The provider's own count of this conversation is the total: what it
57
- # billed for the request this process assembled, or — for a session
58
- # rebuilt from the store, which has no call of its own yet — what it
59
- # billed for the last call that session made. Locally counting a rebuilt
60
- # request is not a substitute: on this machine's own session the rebuilt
61
- # list measured 1.65 M tokens against a provider's 326 k, because a
62
- # rebuilt list holds messages the live request no longer carries.
63
- #
64
- # A count that reaches the window is refused instead: a request cannot
65
- # carry more than the window it is measured against, so a figure that does
66
- # is about something else, and a share pinned at a hundred is worse than
67
- # the measurement.
68
- reported = counted if 0 < counted < window else None
69
- return self.runtime.context.report(
70
- self.session_id,
71
- window=window,
72
- tokenizer=tokenizer,
73
- reported=reported,
74
- )
56
+ """Return what the next request carries, or ``None`` before it was assembled."""
57
+ return await report_of(self.runtime, self.session_id)
75
58
 
76
59
  async def ensure_compactable(self) -> None:
77
60
  """Refuse a compaction that would do nothing, before a row is put up for it.
@@ -19,7 +19,6 @@ from zett_agent.resume import ResumePoint
19
19
 
20
20
  from zettcode.engine.dialogue.side import ORIGIN, READER, SIDE, WATCHED, question, watched_by
21
21
  from zettcode.prompt import MentionPart, PromptPart, carries_image, model_text_part, plain_text
22
- from zettcode.relay import Relayed
23
22
 
24
23
  from .base import AgentBase
25
24
 
@@ -57,9 +56,47 @@ class TurnsMixin(AgentBase):
57
56
  side: bool = False,
58
57
  origin: str = READER,
59
58
  watched: bool | None = None,
60
- relayed: Relayed | None = None,
59
+ incoming: UserMessage | None = None,
61
60
  cache: CacheMode | str | None = None,
62
61
  ) -> AsyncIterator[AgentEvent]:
62
+ """Run one turn, counting it as a running request for as long as its events are read.
63
+
64
+ The count is what tells an external event whether it can steer this request or has to wait for the next turn
65
+ (see :mod:`zettcode.engine.delivery`). A side question is not a request of the conversation: nothing steers
66
+ into it, and it does not keep a follow-up waiting.
67
+ """
68
+ if side:
69
+ async with aclosing(
70
+ self._stream(
71
+ parts, hint=hint, side=True, origin=origin, watched=watched, incoming=incoming, cache=cache
72
+ )
73
+ ) as events:
74
+ async for event in events:
75
+ yield event
76
+ return
77
+ self.runtime.run_started()
78
+ try:
79
+ async with aclosing(
80
+ self._stream(
81
+ parts, hint=hint, side=False, origin=origin, watched=watched, incoming=incoming, cache=cache
82
+ )
83
+ ) as events:
84
+ async for event in events:
85
+ yield event
86
+ finally:
87
+ self.runtime.run_finished()
88
+
89
+ async def _stream(
90
+ self,
91
+ parts: Sequence[PromptPart],
92
+ *,
93
+ hint: str | None = None,
94
+ side: bool = False,
95
+ origin: str = READER,
96
+ watched: bool | None = None,
97
+ incoming: UserMessage | None = None,
98
+ cache: CacheMode | str | None = None,
99
+ ) -> AsyncGenerator[AgentEvent, None]:
63
100
  """Run one turn with the selected session, model, and reasoning effort.
64
101
 
65
102
  Args:
@@ -73,8 +110,8 @@ class TurnsMixin(AgentBase):
73
110
  was typed and a restored session shows it.
74
111
  side: Ask without letting the exchange join the conversation; see
75
112
  :mod:`zettcode.engine.dialogue.side`.
76
- relayed: The turn is a message another session sent. It is what the model reads
77
- (with where it came from), and ``parts`` and ``hint`` are not used.
113
+ incoming: The turn is a message another session sent, already framed by the part that owns the
114
+ conversation with it. It is what the model reads, and ``parts`` and ``hint`` are not used.
78
115
  cache: How this request may use the provider's prompt cache: ``"read_write"`` (the default) extends
79
116
  it, ``"read"`` reuses a warm prefix without marking this request's new content for caching, and
80
117
  ``"off"`` sends no cache directive at all. A side question starts at ``"read"`` unless told
@@ -103,8 +140,8 @@ class TurnsMixin(AgentBase):
103
140
  "AsyncGenerator[AgentEvent]",
104
141
  client.stream(
105
142
  self.request(parts, hint=hint, side=side, origin=origin, watched=watched)
106
- if relayed is None
107
- else relayed.message(),
143
+ if incoming is None
144
+ else incoming,
108
145
  config=AgentRunConfig(
109
146
  session_id=session_id,
110
147
  cache_mode=cache_mode_of(cache, side=side),
@@ -297,12 +334,21 @@ class TurnsMixin(AgentBase):
297
334
  if not message.text.strip() and not message.parts:
298
335
  return False
299
336
  payload = {"message": message}
300
- try:
301
- agent = self.runtime.started.agent
302
- except RuntimeError:
303
- return False
304
- accepted = agent.emit_external_event(
305
- ExternalEvent(name=STEERING_MESSAGE_EVENT_NAME, payload=payload),
306
- config=AgentRunConfig(session_id=self.session_id),
307
- )
308
- return bool(accepted)
337
+ return steer_in_flight(self.runtime, self.session_id, payload)
338
+
339
+
340
+ def steer_in_flight(runtime: object, session_id: str, payload: dict[str, object]) -> bool:
341
+ """Emit the steering event for one message, and say whether the running request took it.
342
+
343
+ The engine's own delivery (:class:`zettcode.engine.delivery.Delivery`) and the agent's ``steer`` both come through
344
+ here, so the one rule for what a request accepts has one home.
345
+ """
346
+ try:
347
+ agent = runtime.started.agent # type: ignore[attr-defined]
348
+ except RuntimeError:
349
+ return False
350
+ accepted = agent.emit_external_event(
351
+ ExternalEvent(name=STEERING_MESSAGE_EVENT_NAME, payload=payload),
352
+ config=AgentRunConfig(session_id=session_id),
353
+ )
354
+ return bool(accepted)