zettcode 0.1.3__tar.gz → 0.1.5__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.
- {zettcode-0.1.3 → zettcode-0.1.5}/.gitignore +1 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/PKG-INFO +7 -4
- {zettcode-0.1.3 → zettcode-0.1.5}/README.md +3 -2
- {zettcode-0.1.3 → zettcode-0.1.5}/README.zh-CN.md +2 -2
- {zettcode-0.1.3 → zettcode-0.1.5}/pyproject.toml +17 -7
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/__init__.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/agent/__init__.py +6 -5
- zettcode-0.1.5/src/zettcode/app/agent/agent/__init__.py +74 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/base.py +29 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/commands.py +80 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/context.py +139 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/exports.py +66 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/interaction.py +53 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/prompt.py +68 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/sessions.py +219 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/settings.py +68 -0
- zettcode-0.1.5/src/zettcode/app/agent/agent/turns.py +160 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/__init__.py +7 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/entries.py +218 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/presenters.py +616 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/conversation}/projection.py +10 -3
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/conversation}/rendering.py +62 -6
- zettcode-0.1.5/src/zettcode/app/agent/conversation/replay.py +156 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/rows.py +423 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/transcript/__init__.py +74 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/transcript/fold.py +56 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/transcript/log.py +182 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/transcript/oneoff.py +54 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/transcript/stream.py +165 -0
- zettcode-0.1.5/src/zettcode/app/agent/conversation/transcript/turn.py +204 -0
- zettcode-0.1.5/src/zettcode/app/agent/dialogue/__init__.py +6 -0
- zettcode-0.1.5/src/zettcode/app/agent/dialogue/mentions.py +303 -0
- zettcode-0.1.5/src/zettcode/app/agent/errors.py +39 -0
- zettcode-0.1.3/src/zettcode/app/agent/export.py → zettcode-0.1.5/src/zettcode/app/agent/export/__init__.py +4 -4
- zettcode-0.1.5/src/zettcode/app/agent/extensions/__init__.py +8 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/extensions}/capabilities.py +3 -3
- zettcode-0.1.5/src/zettcode/app/agent/extensions/compaction.py +146 -0
- zettcode-0.1.5/src/zettcode/app/agent/extensions/context.py +532 -0
- zettcode-0.1.5/src/zettcode/app/agent/extensions/mcp.py +156 -0
- zettcode-0.1.5/src/zettcode/app/agent/extensions/mcp_notice.py +31 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/extensions}/subagents.py +1 -1
- zettcode-0.1.5/src/zettcode/app/agent/extensions/tool_output.py +152 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/extensions}/usage.py +1 -1
- zettcode-0.1.5/src/zettcode/app/agent/permission/__init__.py +146 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/permission}/approval.py +20 -3
- zettcode-0.1.5/src/zettcode/app/agent/permission/readonly.py +435 -0
- zettcode-0.1.5/src/zettcode/app/agent/runtime.py +562 -0
- zettcode-0.1.5/src/zettcode/app/agent/storage/__init__.py +31 -0
- zettcode-0.1.5/src/zettcode/app/agent/storage/metadata.py +270 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/agent/storage/records.py +120 -15
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/agent/storage/store.py +275 -33
- zettcode-0.1.5/src/zettcode/app/pipeline.py +191 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/app.py +358 -73
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/boot.py +20 -10
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/clipboard.py +28 -2
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/commands.py +58 -3
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/demo.py +39 -20
- zettcode-0.1.5/src/zettcode/app/ui/followups.py +368 -0
- zettcode-0.1.5/src/zettcode/app/ui/keys.py +329 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/labels.py +44 -5
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/notices.py +32 -10
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/rows.py +60 -4
- zettcode-0.1.5/src/zettcode/app/ui/sessions.py +291 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/settings.py +23 -0
- zettcode-0.1.5/src/zettcode/app/ui/shell.py +229 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/__init__.py +10 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/approval_page.py +47 -14
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/ask_page.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/completer.py +8 -3
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/composer.py +88 -11
- zettcode-0.1.5/src/zettcode/app/ui/widgets/context_page.py +114 -0
- zettcode-0.1.5/src/zettcode/app/ui/widgets/follow_up.py +170 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/model_page.py +17 -9
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/panel.py +16 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/root.py +16 -3
- zettcode-0.1.5/src/zettcode/app/ui/widgets/send_kind_page.py +192 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/sessions_page.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/steering.py +9 -3
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/transcript.py +26 -15
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/welcome.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/cli.py +152 -30
- zettcode-0.1.5/src/zettcode/collector.py +44 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/config.py +131 -37
- zettcode-0.1.5/src/zettcode/paths.py +60 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/builtins.py +20 -5
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/container.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/loader.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/plugin.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/state.py +6 -1
- zettcode-0.1.5/src/zettcode/tui/clipboard.py +91 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/app.py +26 -5
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/scheduler.py +10 -1
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/widget.py +11 -2
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/input/decoder.py +38 -2
- zettcode-0.1.5/src/zettcode/tui/input/keys.py +289 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/input/reader.py +2 -2
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/__init__.py +2 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/rich_text.py +16 -2
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/style.py +23 -4
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/text.py +50 -3
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/runner.py +71 -16
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/terminal.py +8 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/list_page.py +17 -4
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/markdown.py +61 -3
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/text.py +7 -5
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/textarea.py +18 -9
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/update.py +18 -3
- zettcode-0.1.3/src/zettcode/app/agent/agent.py +0 -626
- zettcode-0.1.3/src/zettcode/app/agent/blocks.py +0 -367
- zettcode-0.1.3/src/zettcode/app/agent/compaction.py +0 -47
- zettcode-0.1.3/src/zettcode/app/agent/context.py +0 -293
- zettcode-0.1.3/src/zettcode/app/agent/entries.py +0 -191
- zettcode-0.1.3/src/zettcode/app/agent/mcp.py +0 -29
- zettcode-0.1.3/src/zettcode/app/agent/mentions.py +0 -126
- zettcode-0.1.3/src/zettcode/app/agent/replay.py +0 -84
- zettcode-0.1.3/src/zettcode/app/agent/rows.py +0 -215
- zettcode-0.1.3/src/zettcode/app/agent/runtime.py +0 -411
- zettcode-0.1.3/src/zettcode/app/agent/storage/__init__.py +0 -28
- zettcode-0.1.3/src/zettcode/app/agent/storage/metadata.py +0 -135
- zettcode-0.1.3/src/zettcode/app/agent/transcript.py +0 -531
- zettcode-0.1.3/src/zettcode/app/ui/keys.py +0 -188
- zettcode-0.1.3/src/zettcode/app/ui/sessions.py +0 -89
- zettcode-0.1.3/src/zettcode/app/ui/shell.py +0 -125
- zettcode-0.1.3/src/zettcode/app/ui/widgets/context_page.py +0 -70
- zettcode-0.1.3/src/zettcode/paths.py +0 -29
- zettcode-0.1.3/src/zettcode/tui/input/keys.py +0 -60
- {zettcode-0.1.3 → zettcode-0.1.5}/LICENSE +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/dialogue}/ask.py +0 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/dialogue}/side.py +0 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/dialogue}/title.py +0 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/export}/trace/script.js +0 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/export}/trace/style.css +0 -0
- {zettcode-0.1.3/src/zettcode/app/agent → zettcode-0.1.5/src/zettcode/app/agent/export}/trace/template.html +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/commands.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/registry.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/updating.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/app/ui/widgets/update_page.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/extension.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/plugins/mixins.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/core/theme.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/layout/scroll.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/theme_file.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.5}/src/zettcode/tui/widgets/toast.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: zettcode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A focused terminal coding agent powered by zett-agent
|
|
5
5
|
Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
|
|
6
6
|
Project-URL: Issues, https://github.com/Chang-LeHung/zettcode/issues
|
|
@@ -21,12 +21,14 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
21
21
|
Classifier: Typing :: Typed
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
Requires-Dist: exceptiongroup<2,>=1.2; python_version < '3.11'
|
|
24
|
+
Requires-Dist: pydantic<3,>=2
|
|
24
25
|
Requires-Dist: pyperclip<2,>=1.9
|
|
25
26
|
Requires-Dist: setproctitle<2,>=1.3.7; os_name == 'posix'
|
|
27
|
+
Requires-Dist: tiktoken<1,>=0.12
|
|
26
28
|
Requires-Dist: tomli<3,>=2; python_version < '3.11'
|
|
27
29
|
Requires-Dist: truststore<1,>=0.10
|
|
28
30
|
Requires-Dist: wcwidth<1,>=0.2
|
|
29
|
-
Requires-Dist: zett-agent<0.2,>=0.1.
|
|
31
|
+
Requires-Dist: zett-agent<0.2,>=0.1.15
|
|
30
32
|
Description-Content-Type: text/markdown
|
|
31
33
|
|
|
32
34
|
<p align="center">
|
|
@@ -49,7 +51,8 @@ Description-Content-Type: text/markdown
|
|
|
49
51
|
|
|
50
52
|
## Get running
|
|
51
53
|
|
|
52
|
-
You need **Python 3.10+**, a UTF-8 terminal, and an **OpenAI-compatible API
|
|
54
|
+
You need **Python 3.10+**, a UTF-8 terminal, and an **OpenAI-compatible API**
|
|
55
|
+
(or Anthropic's **Messages API**).
|
|
53
56
|
Works on macOS, Linux, and Windows (use Windows Terminal).
|
|
54
57
|
|
|
55
58
|
### 1. Install
|
|
@@ -98,7 +101,7 @@ and prints the command to resume your session.
|
|
|
98
101
|
| --- | --- |
|
|
99
102
|
| **See every step** | Reasoning, answers, and tool calls stream as readable rows — not raw JSON. |
|
|
100
103
|
| **Keep the decision** | Review shell commands before they run. When the agent needs your input, choose an option or type an answer. |
|
|
101
|
-
| **Keep moving** |
|
|
104
|
+
| **Keep moving** | Type during a reply and choose: steer it now, or queue a follow-up you can edit, delete and reorder with `Option`+`↑`/`↓`. Use `/btw` for a side question that stays out of later context. |
|
|
102
105
|
| **Come back later** | `/resume` restores a conversation; `/export` saves a readable HTML record. |
|
|
103
106
|
| **Understand the context** | Monitor token usage and caching, inspect `/context`, and compact when needed. |
|
|
104
107
|
| **Bring your tools** | Use project `AGENTS.md` instructions, skills, MCP servers, and Python plugins. |
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
## Get running
|
|
20
20
|
|
|
21
|
-
You need **Python 3.10+**, a UTF-8 terminal, and an **OpenAI-compatible API
|
|
21
|
+
You need **Python 3.10+**, a UTF-8 terminal, and an **OpenAI-compatible API**
|
|
22
|
+
(or Anthropic's **Messages API**).
|
|
22
23
|
Works on macOS, Linux, and Windows (use Windows Terminal).
|
|
23
24
|
|
|
24
25
|
### 1. Install
|
|
@@ -67,7 +68,7 @@ and prints the command to resume your session.
|
|
|
67
68
|
| --- | --- |
|
|
68
69
|
| **See every step** | Reasoning, answers, and tool calls stream as readable rows — not raw JSON. |
|
|
69
70
|
| **Keep the decision** | Review shell commands before they run. When the agent needs your input, choose an option or type an answer. |
|
|
70
|
-
| **Keep moving** |
|
|
71
|
+
| **Keep moving** | Type during a reply and choose: steer it now, or queue a follow-up you can edit, delete and reorder with `Option`+`↑`/`↓`. Use `/btw` for a side question that stays out of later context. |
|
|
71
72
|
| **Come back later** | `/resume` restores a conversation; `/export` saves a readable HTML record. |
|
|
72
73
|
| **Understand the context** | Monitor token usage and caching, inspect `/context`, and compact when needed. |
|
|
73
74
|
| **Bring your tools** | Use project `AGENTS.md` instructions, skills, MCP servers, and Python plugins. |
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
## 先跑起来
|
|
20
20
|
|
|
21
|
-
需要 **Python 3.10+**、支持 UTF-8 的终端和 **OpenAI
|
|
21
|
+
需要 **Python 3.10+**、支持 UTF-8 的终端和 **OpenAI 兼容接口**(或 Anthropic 的 **Messages API**)。
|
|
22
22
|
支持 macOS、Linux 和 Windows(请使用 Windows Terminal)。
|
|
23
23
|
|
|
24
24
|
### 1. 安装
|
|
@@ -64,7 +64,7 @@ zettcode
|
|
|
64
64
|
| --- | --- |
|
|
65
65
|
| **看清每一步** | 思考、回答和工具调用实时呈现为可读的行,不是原始 JSON。 |
|
|
66
66
|
| **决定权在你手上** | 执行 shell 命令前先审阅;智能体需要你的决定时,可以选选项,也可以打字回答。 |
|
|
67
|
-
| **工作不中断** |
|
|
67
|
+
| **工作不中断** | 回复过程中直接输入并选择:立刻引导,或排成可用 `Option`+`↑`/`↓` 调整顺序、可编辑可删除的后续消息;用 `/btw` 问一句,不让它进入后续上下文。 |
|
|
68
68
|
| **随时接着做** | `/resume` 恢复对话,`/export` 导出便于阅读的 HTML。 |
|
|
69
69
|
| **看懂上下文** | 查看 token 与缓存统计,用 `/context` 检查占用,在需要时压缩。 |
|
|
70
70
|
| **接入你的工具** | 使用项目 `AGENTS.md` 指令、skills、MCP 服务器和 Python 插件。 |
|
|
@@ -24,19 +24,29 @@ classifiers = [
|
|
|
24
24
|
dependencies = [
|
|
25
25
|
# The stdlib gained both of these in 3.11; older runtimes take the backports.
|
|
26
26
|
"exceptiongroup>=1.2,<2; python_version < '3.11'",
|
|
27
|
+
# Imported directly by the session store's record and metadata codecs, so it
|
|
28
|
+
# is declared here rather than borrowed from the runtime that brings it today.
|
|
29
|
+
"pydantic>=2,<3",
|
|
27
30
|
"pyperclip>=1.9,<2",
|
|
28
31
|
"setproctitle>=1.3.7,<2; os_name == 'posix'",
|
|
32
|
+
# The counter behind ``/context`` measures a request with it, and imports it
|
|
33
|
+
# itself: the floor matches the runtime's, so neither resolver has to fight.
|
|
34
|
+
"tiktoken>=0.12,<1",
|
|
29
35
|
"tomli>=2,<3; python_version < '3.11'",
|
|
30
36
|
# The system trust store, so the release check works behind a proxy.
|
|
31
37
|
"truststore>=0.10,<1",
|
|
32
38
|
"wcwidth>=0.2,<1",
|
|
33
|
-
# A range, not a pin: 0.1.
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
39
|
+
# A range, not a pin: 0.1.13 is the oldest runtime whose tool argument schemas
|
|
40
|
+
# are self-contained — Pydantic's ``$defs``/``$ref`` are expanded when the
|
|
41
|
+
# schema is built — and a gateway that refuses to resolve a pointer otherwise
|
|
42
|
+
# answers 400 to every request on its Messages route. It also keeps the two
|
|
43
|
+
# the floor below it was raised for: the shell tool called ``bash``, which this
|
|
44
|
+
# client's phrasing, colours, and approval path are keyed on, and SQLAlchemy,
|
|
45
|
+
# tiktoken, and httpx behind their first use, so warming a runtime costs less
|
|
46
|
+
# than it did. An exact pin would make zettcode fight every other tool that
|
|
47
|
+
# shares the dependency — including an upgrade of zett-agent itself, which pip
|
|
48
|
+
# would downgrade to satisfy it.
|
|
49
|
+
"zett-agent>=0.1.15,<0.2",
|
|
40
50
|
]
|
|
41
51
|
|
|
42
52
|
[project.scripts]
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
#: The one place the version is written. ``pyproject.toml`` reads it from here
|
|
9
9
|
#: and the release workflow checks the tag against it, so a bump is one edit.
|
|
10
|
-
__version__ = "0.1.
|
|
10
|
+
__version__ = "0.1.5"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
9
9
|
_EXPORTS = {
|
|
10
|
-
"Entry": ".
|
|
11
|
-
"Transcript": ".transcript",
|
|
12
|
-
"TranscriptProjector": ".projection",
|
|
10
|
+
"Entry": ".conversation.entries",
|
|
11
|
+
"Transcript": ".conversation.transcript",
|
|
12
|
+
"TranscriptProjector": ".conversation.projection",
|
|
13
13
|
"ZettCodeAgent": ".agent",
|
|
14
14
|
"ZettCodeRuntime": ".runtime",
|
|
15
15
|
}
|
|
@@ -17,9 +17,10 @@ _EXPORTS = {
|
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
|
|
19
19
|
from .agent import ZettCodeAgent
|
|
20
|
-
from .
|
|
20
|
+
from .conversation.entries import Entry
|
|
21
|
+
from .conversation.projection import TranscriptProjector
|
|
22
|
+
from .conversation.transcript import Transcript
|
|
21
23
|
from .runtime import ZettCodeRuntime
|
|
22
|
-
from .transcript import Entry, Transcript
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def __getattr__(name: str) -> object:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""The application-facing coding agent: one class, assembled from the parts that make it up.
|
|
2
|
+
|
|
3
|
+
The class used to be one 750-line body doing eight unrelated jobs. It is divided the way the
|
|
4
|
+
transcript and the shell are, by what each part is responsible for:
|
|
5
|
+
|
|
6
|
+
============== ===========================================================================
|
|
7
|
+
``base`` :class:`AgentBase` — the runtime, the workspace, the active session id
|
|
8
|
+
``settings`` :class:`SettingsMixin` — model, reasoning effort, shell permission
|
|
9
|
+
``context`` :class:`ContextMixin` — the request the next turn carries; compaction
|
|
10
|
+
``sessions`` :class:`SessionsMixin` — switch, resume, fork, list, and title sessions
|
|
11
|
+
``turns`` :class:`TurnsMixin` — build a request, stream its events, steer it
|
|
12
|
+
``interaction`` :class:`InteractionMixin` — answer an approval request or a question
|
|
13
|
+
``exports`` :class:`ExportsMixin` — write a session out as a page
|
|
14
|
+
``commands`` :class:`CommandsMixin` — the agent's commands and what plugins contributed
|
|
15
|
+
``prompt`` the parts of a user turn: text, ``@`` references, images (no class)
|
|
16
|
+
============== ===========================================================================
|
|
17
|
+
|
|
18
|
+
A mixin inherits the ones whose methods it really calls, and :class:`AgentBase` below all of
|
|
19
|
+
them, so each is type-checked against what it relies on; nothing is declared twice. This class
|
|
20
|
+
is the composition and the lifecycle — building the runtime, and closing it.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from ....config import ZettCodeConfig
|
|
26
|
+
from ..runtime import ZettCodeRuntime
|
|
27
|
+
from .commands import CommandsMixin
|
|
28
|
+
from .context import ContextMixin
|
|
29
|
+
from .exports import ExportsMixin
|
|
30
|
+
from .interaction import InteractionMixin
|
|
31
|
+
from .sessions import SessionsMixin
|
|
32
|
+
from .settings import SettingsMixin
|
|
33
|
+
from .turns import TurnsMixin
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ZettCodeAgent(
|
|
37
|
+
CommandsMixin,
|
|
38
|
+
InteractionMixin,
|
|
39
|
+
TurnsMixin,
|
|
40
|
+
ExportsMixin,
|
|
41
|
+
SessionsMixin,
|
|
42
|
+
SettingsMixin,
|
|
43
|
+
ContextMixin,
|
|
44
|
+
):
|
|
45
|
+
"""Expose agent operations without leaking runtime resources to the UI.
|
|
46
|
+
|
|
47
|
+
The runtime assembles the vendor client and extensions; this class is the
|
|
48
|
+
application's entry point for turns, session and model selection, progress,
|
|
49
|
+
and responses to approval requests.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, runtime: ZettCodeRuntime) -> None:
|
|
53
|
+
self.runtime = runtime
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
async def create(cls, config: ZettCodeConfig) -> ZettCodeAgent:
|
|
57
|
+
"""Build the runtime and wrap it in the application agent."""
|
|
58
|
+
return cls(await ZettCodeRuntime.create(config))
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def preview(cls, config: ZettCodeConfig) -> ZettCodeAgent:
|
|
62
|
+
"""Wrap a runtime that still has to start, so a shell can paint first.
|
|
63
|
+
|
|
64
|
+
The runtime here holds the session and this application's own
|
|
65
|
+
extensions; the provider SDK and the client are built by the first
|
|
66
|
+
:meth:`runtime.start`, which a turn or a command awaits. Everything the
|
|
67
|
+
shell reads per frame — the model, the effort, the session, the usage —
|
|
68
|
+
is answered from that preview.
|
|
69
|
+
"""
|
|
70
|
+
return cls(ZettCodeRuntime.preview(config))
|
|
71
|
+
|
|
72
|
+
async def aclose(self) -> None:
|
|
73
|
+
"""Release the underlying runtime resources."""
|
|
74
|
+
await self.runtime.aclose()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""What every part of the agent shares: the runtime it speaks to, and who and where it is.
|
|
2
|
+
|
|
3
|
+
The mixins in this package are split by what they are responsible for, and each one
|
|
4
|
+
inherits this class only to be type-checked against what the others rely on. The two
|
|
5
|
+
properties here are read by nearly all of them, which is why they live below all of them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..runtime import ZettCodeRuntime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentBase:
|
|
16
|
+
"""The runtime, the workspace it works in, and the session the next turn belongs to."""
|
|
17
|
+
|
|
18
|
+
#: The runtime this agent speaks to; the one thing the application agent holds.
|
|
19
|
+
runtime: ZettCodeRuntime
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def workspace(self) -> Path:
|
|
23
|
+
"""Return the workspace displayed in the header."""
|
|
24
|
+
return self.runtime.config.workspace
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def session_id(self) -> str:
|
|
28
|
+
"""Return the session used for the next turn."""
|
|
29
|
+
return self.runtime.session_id
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""What the agent offers the shell to run and to show: its own commands, and what plugins added.
|
|
2
|
+
|
|
3
|
+
The commands report through transcript Markdown; commands that present a widget belong to
|
|
4
|
+
the shell, which owns the widgets. The plugin contributions are listed here because the
|
|
5
|
+
agent is where the shell asks for everything the runtime loaded.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ....plugins import UiRow
|
|
11
|
+
from ...commands import Command, CommandContext, CommandResult
|
|
12
|
+
from ..dialogue.mentions import MentionProvider
|
|
13
|
+
from .exports import ExportsMixin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandsMixin(ExportsMixin):
|
|
17
|
+
"""The ``/new``, ``/compact``, and ``/export`` commands, and the plugins' commands, mentions, and rows."""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def commands(self) -> tuple[Command, ...]:
|
|
21
|
+
"""Return the conversation commands the agent owns.
|
|
22
|
+
|
|
23
|
+
They report through transcript Markdown; commands that present a widget
|
|
24
|
+
(such as the model picker) belong to the shell, which owns the widgets.
|
|
25
|
+
"""
|
|
26
|
+
return (
|
|
27
|
+
Command("/new", "start a fresh session", "agent", self._command_new),
|
|
28
|
+
Command("/compact", "summarize the context now", "agent", self._command_compact),
|
|
29
|
+
Command("/export", "write this session to an HTML file: /export [path]", "agent", self._command_export),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def plugin_commands(self) -> tuple[Command, ...]:
|
|
34
|
+
"""Return the commands the installed plugins registered.
|
|
35
|
+
|
|
36
|
+
The shell keeps them after its own commands, so a plugin cannot take
|
|
37
|
+
over a built-in name; plugin-versus-plugin duplicates are rejected when
|
|
38
|
+
the plugins load.
|
|
39
|
+
"""
|
|
40
|
+
return self.runtime.plugins.commands
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def plugin_mentions(self) -> tuple[MentionProvider, ...]:
|
|
44
|
+
"""Return the ``@`` resource providers the installed plugins registered."""
|
|
45
|
+
return self.runtime.plugins.mentions
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def plugin_failures(self) -> tuple[str, ...]:
|
|
49
|
+
"""Return one message per plugin that could not be loaded or activated."""
|
|
50
|
+
return self.runtime.plugins.failures
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def plugin_rows(self) -> tuple[UiRow, ...]:
|
|
54
|
+
"""Return the header and status rows plugins asked the shell to draw."""
|
|
55
|
+
return self.runtime.plugins.rows
|
|
56
|
+
|
|
57
|
+
async def _command_new(self, context: CommandContext) -> CommandResult:
|
|
58
|
+
"""Start a fresh session."""
|
|
59
|
+
session_id = self.new_session()
|
|
60
|
+
return CommandResult(notification=f"started session {session_id[:8]}")
|
|
61
|
+
|
|
62
|
+
async def _command_export(self, context: CommandContext) -> CommandResult:
|
|
63
|
+
"""Write the session and its context to one HTML file.
|
|
64
|
+
|
|
65
|
+
The path argument is optional; without it the file lands in the
|
|
66
|
+
workspace, named after the session, and the result says where.
|
|
67
|
+
"""
|
|
68
|
+
path = await self.export_session(context.argument.strip() or None)
|
|
69
|
+
return CommandResult(message=f"Exported to `{path}`")
|
|
70
|
+
|
|
71
|
+
async def _command_compact(self, context: CommandContext) -> CommandResult:
|
|
72
|
+
"""Summarize the conversation now, whatever its size.
|
|
73
|
+
|
|
74
|
+
The row and the outcome notice arrive while this await runs: the pass
|
|
75
|
+
dispatches through the same event dispatcher a turn does, so the
|
|
76
|
+
transcript animates ``Compacting`` and ends with ``Context compaction
|
|
77
|
+
applied`` (or ``skipped`` when the summary would not be smaller).
|
|
78
|
+
"""
|
|
79
|
+
await self.compact()
|
|
80
|
+
return CommandResult()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""What the next request carries: its size, its breakdown, and shrinking it.
|
|
2
|
+
|
|
3
|
+
Counting a request and summarizing one are the same subject seen from two ends — one says
|
|
4
|
+
how full the window is, the other makes room in it — and both are answered from what the
|
|
5
|
+
runtime assembled or, before it has assembled one, from what the store holds.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from zett_agent.agent import AgentRunConfig
|
|
11
|
+
from zett_agent.extensions.events import CompactionEvent
|
|
12
|
+
from zett_agent.messages import AnyMessage, SystemMessage
|
|
13
|
+
from zett_agent.model import ToolDefinition
|
|
14
|
+
|
|
15
|
+
from ..extensions.context import ContextReport
|
|
16
|
+
from ..extensions.usage import UsageSnapshot
|
|
17
|
+
from ..runtime import build_system_prompt
|
|
18
|
+
from ..storage import Session
|
|
19
|
+
from .base import AgentBase
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContextMixin(AgentBase):
|
|
23
|
+
"""Report the request the next turn would send, and compact the conversation behind it."""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def usage(self) -> UsageSnapshot:
|
|
27
|
+
"""Return the active session's cumulative token counters.
|
|
28
|
+
|
|
29
|
+
The numbers come from the usage extension's in-memory totals, so the
|
|
30
|
+
status line can read them every frame; a session replayed from the store
|
|
31
|
+
is seeded before this is asked.
|
|
32
|
+
"""
|
|
33
|
+
return self.runtime.usage.snapshot(self.session_id)
|
|
34
|
+
|
|
35
|
+
async def context_report(self) -> ContextReport | None:
|
|
36
|
+
"""Return what the next request carries by source, or None before the first call.
|
|
37
|
+
|
|
38
|
+
The shares come from the tokenizer, which is prepared first: it prefers
|
|
39
|
+
tiktoken and falls back to a character estimate when that vocabulary
|
|
40
|
+
cannot be opened, which the report names so the numbers are not mistaken
|
|
41
|
+
for exact ones. The total comes from the provider when it counted this
|
|
42
|
+
very request — it counts what it bills — and from the tokenizer when the
|
|
43
|
+
request was primed from the store, whose stored figure belongs to a call
|
|
44
|
+
the dialogue may have outgrown.
|
|
45
|
+
"""
|
|
46
|
+
tokenizer = self.runtime.tokenizer
|
|
47
|
+
await tokenizer.prepare()
|
|
48
|
+
window = self.runtime.active_model.context_window
|
|
49
|
+
counted = self.runtime.usage.snapshot(self.session_id).context_tokens
|
|
50
|
+
# The provider's own count of this conversation is the total: what it
|
|
51
|
+
# billed for the request this process assembled, or — for a session
|
|
52
|
+
# rebuilt from the store, which has no call of its own yet — what it
|
|
53
|
+
# billed for the last call that session made. Locally counting a rebuilt
|
|
54
|
+
# request is not a substitute: on this machine's own session the rebuilt
|
|
55
|
+
# list measured 1.65 M tokens against a provider's 326 k, because a
|
|
56
|
+
# rebuilt list holds messages the live request no longer carries.
|
|
57
|
+
#
|
|
58
|
+
# A count that reaches the window is refused instead: a request cannot
|
|
59
|
+
# carry more than the window it is measured against, so a figure that does
|
|
60
|
+
# is about something else, and a share pinned at a hundred is worse than
|
|
61
|
+
# the measurement.
|
|
62
|
+
reported = counted if 0 < counted < window else None
|
|
63
|
+
return self.runtime.context.report(
|
|
64
|
+
self.session_id,
|
|
65
|
+
window=window,
|
|
66
|
+
tokenizer=tokenizer,
|
|
67
|
+
reported=reported,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def compact(self) -> CompactionEvent | None:
|
|
71
|
+
"""Summarize the conversation now, without adding a turn to it.
|
|
72
|
+
|
|
73
|
+
zett-agent runs one pass over the restored session and dispatches every
|
|
74
|
+
event it emits, so the transcript animates the same ``Compacting`` row an
|
|
75
|
+
automatic compaction shows. Nothing is appended to the conversation and
|
|
76
|
+
no primary model call is made: the only work is the summary itself.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The checkpoint that was stored, or ``None`` when there was nothing
|
|
80
|
+
worth replacing.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: When the newest checkpoint already covers the branch —
|
|
84
|
+
a second pass would summarize nothing but the checkpoint.
|
|
85
|
+
"""
|
|
86
|
+
if self._compacted_at_the_head():
|
|
87
|
+
raise ValueError("Already compacted; send a message before compacting again")
|
|
88
|
+
await self.runtime.start()
|
|
89
|
+
return await self.runtime.started.compact(
|
|
90
|
+
config=AgentRunConfig(session_id=self.session_id),
|
|
91
|
+
model=self.runtime.provider,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _compacted_at_the_head(self) -> bool:
|
|
95
|
+
"""Return whether nothing has been appended since the newest checkpoint.
|
|
96
|
+
|
|
97
|
+
Messages and checkpoints both carry ``created_at``, so the last message
|
|
98
|
+
being older than the last checkpoint is what says the branch has not
|
|
99
|
+
moved since it was summarized — no matter which session is active or
|
|
100
|
+
whether the transcript in this process ever showed that compaction.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
session = self.runtime.persistence.read(self.session_id)
|
|
104
|
+
except ValueError:
|
|
105
|
+
# A runtime without a store to read: no checkpoint can cover it.
|
|
106
|
+
return False
|
|
107
|
+
latest = session.latest_compaction
|
|
108
|
+
if latest is None or session.head_id is None:
|
|
109
|
+
return False
|
|
110
|
+
head = session.message(session.head_id)
|
|
111
|
+
return head is not None and head.created_at <= latest.created_at
|
|
112
|
+
|
|
113
|
+
def _context_request(self, session: Session) -> tuple[tuple[AnyMessage, ...], tuple[ToolDefinition, ...]]:
|
|
114
|
+
"""Return the messages and tools the next turn would send.
|
|
115
|
+
|
|
116
|
+
The request this process assembled is the honest answer; before one has
|
|
117
|
+
run — a session restored into a fresh process — the instructions and the
|
|
118
|
+
active branch stand in for it, which is all that can be known there.
|
|
119
|
+
"""
|
|
120
|
+
assembled = self.runtime.context.assembled(self.session_id)
|
|
121
|
+
if assembled is not None:
|
|
122
|
+
messages, tools = assembled
|
|
123
|
+
return tuple(messages), tuple(tools)
|
|
124
|
+
return self._stored_request(session), self.runtime.context.tools
|
|
125
|
+
|
|
126
|
+
def _stored_request(self, session: Session) -> tuple[AnyMessage, ...]:
|
|
127
|
+
"""Return the messages a request for ``session`` would carry, from what is stored.
|
|
128
|
+
|
|
129
|
+
This depends on the session it is given and on nothing that changes with the
|
|
130
|
+
active one, so it can be run later — a restore hands it over unrun, and the
|
|
131
|
+
reader may have moved to another session by the time it is wanted.
|
|
132
|
+
"""
|
|
133
|
+
instructions = self.runtime.context.instructions or (
|
|
134
|
+
SystemMessage(content=build_system_prompt(self.runtime.config)),
|
|
135
|
+
)
|
|
136
|
+
checkpoint, tail = session.active()
|
|
137
|
+
stored: list[AnyMessage] = [checkpoint.message[0]] if checkpoint is not None else []
|
|
138
|
+
stored.extend(line.message[0] for line in tail if line.message[0].include_in_messages)
|
|
139
|
+
return (*instructions, *stored)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Writing a session out as a page a reader can open."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..export import build_trace, render_html, write_export
|
|
10
|
+
from ..storage import Session
|
|
11
|
+
from .context import ContextMixin
|
|
12
|
+
from .sessions import SessionsMixin
|
|
13
|
+
from .settings import SettingsMixin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExportsMixin(SessionsMixin, SettingsMixin, ContextMixin):
|
|
17
|
+
"""Export the active session together with the request the next turn would send."""
|
|
18
|
+
|
|
19
|
+
async def export_session(self, path: str | Path | None = None) -> Path:
|
|
20
|
+
"""Write the active session, and the context it now carries, to HTML.
|
|
21
|
+
|
|
22
|
+
The conversation is rendered with the same presenters the shell paints,
|
|
23
|
+
and the request the next turn would send is included beside it, so a
|
|
24
|
+
reader sees both the history and what the model is down to.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
path: File to write; ``None`` puts ``zettcode-<id8>.html`` in the
|
|
28
|
+
workspace. A relative path is taken from the working directory.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The absolute path that was written.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: When the session has no stored header, so there is
|
|
35
|
+
nothing to export yet.
|
|
36
|
+
"""
|
|
37
|
+
session = self.runtime.persistence.read(self.session_id)
|
|
38
|
+
if session.header is None:
|
|
39
|
+
raise ValueError("Nothing to export: this session has not been stored yet")
|
|
40
|
+
report = await self.context_report()
|
|
41
|
+
messages, tools = self._context_request(session)
|
|
42
|
+
trace = build_trace(
|
|
43
|
+
session,
|
|
44
|
+
title=self.session_name,
|
|
45
|
+
subtitle=f"Session {self.session_id} · {self.workspace}",
|
|
46
|
+
meta=self._export_meta(session),
|
|
47
|
+
tags=(self.runtime.active_model.shown_name, self.effort),
|
|
48
|
+
context_messages=messages,
|
|
49
|
+
tools=tuple(tool.name for tool in tools),
|
|
50
|
+
report=report,
|
|
51
|
+
)
|
|
52
|
+
trace = replace(trace, exported_at=datetime.now().astimezone().strftime("%Y-%m-%d %H:%M"))
|
|
53
|
+
target = path if path is not None else self.workspace / f"zettcode-{self.session_id[:8]}.html"
|
|
54
|
+
return write_export(target, render_html(trace))
|
|
55
|
+
|
|
56
|
+
def _export_meta(self, session: Session) -> tuple[tuple[str, str], ...]:
|
|
57
|
+
"""Return the label/value pairs the exported page's sidebar carries."""
|
|
58
|
+
model = self.runtime.active_model
|
|
59
|
+
return (
|
|
60
|
+
("Workspace", str(self.workspace)),
|
|
61
|
+
("Model", f"{model.shown_name} ({model.model})"),
|
|
62
|
+
("Effort", self.effort),
|
|
63
|
+
("Created", session.created_at.astimezone().strftime("%Y-%m-%d %H:%M")),
|
|
64
|
+
("Updated", session.updated_at.astimezone().strftime("%Y-%m-%d %H:%M")),
|
|
65
|
+
("Messages", f"{session.message_count}"),
|
|
66
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Answering what the model asked for: a shell approval, or a question for the reader.
|
|
2
|
+
|
|
3
|
+
The agent is suspended inside a tool call while it waits, so the answer is an external
|
|
4
|
+
event routed to the session and the call the request came from.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from zett_agent.agent import AgentRunConfig
|
|
10
|
+
from zett_agent.extensions.external import ExternalEvent
|
|
11
|
+
from zett_agent.extensions.shell_approval import SHELL_APPROVAL_RESPONSE_EVENT_NAME
|
|
12
|
+
|
|
13
|
+
from ..dialogue.ask import ASK_USER_RESPONSE_EVENT_NAME, AskUserQuestion, answer_payload, decline_payload
|
|
14
|
+
from .base import AgentBase
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InteractionMixin(AgentBase):
|
|
18
|
+
"""Send the reader's answer to an approval request or a question back to the running agent."""
|
|
19
|
+
|
|
20
|
+
def respond_approval(self, session_id: str, call_id: str, decision: str, remember: bool) -> None:
|
|
21
|
+
"""Answer a pending shell approval for the originating session."""
|
|
22
|
+
self.runtime.started.agent.emit_external_event(
|
|
23
|
+
ExternalEvent(
|
|
24
|
+
name=SHELL_APPROVAL_RESPONSE_EVENT_NAME,
|
|
25
|
+
payload={"tool_call_id": call_id, "decision": decision, "remember": remember},
|
|
26
|
+
),
|
|
27
|
+
config=AgentRunConfig(session_id=session_id),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def answer_ask(self, question: AskUserQuestion, answer: str) -> None:
|
|
31
|
+
"""Answer the model's question; the suspended tool call resumes with it.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
question: The question being answered, which carries the session and
|
|
35
|
+
the tool call the runtime routes the reply on.
|
|
36
|
+
answer: What the reader typed, exactly as it should reach the model.
|
|
37
|
+
"""
|
|
38
|
+
self._respond_to_ask(question, answer_payload(question, answer))
|
|
39
|
+
|
|
40
|
+
def decline_ask(self, question: AskUserQuestion) -> None:
|
|
41
|
+
"""Tell the model the reader cancelled, rather than leaving it waiting.
|
|
42
|
+
|
|
43
|
+
``ask_user`` has no rejection channel in the runtime, so the tool result
|
|
44
|
+
says the question was declined and why; the model carries on from that.
|
|
45
|
+
"""
|
|
46
|
+
self._respond_to_ask(question, decline_payload(question))
|
|
47
|
+
|
|
48
|
+
def _respond_to_ask(self, question: AskUserQuestion, payload: dict[str, object]) -> None:
|
|
49
|
+
"""Emit one ``ask_user_response`` for the session the question came from."""
|
|
50
|
+
self.runtime.started.agent.emit_external_event(
|
|
51
|
+
ExternalEvent(name=ASK_USER_RESPONSE_EVENT_NAME, payload=dict(payload)),
|
|
52
|
+
config=AgentRunConfig(session_id=question.session_id),
|
|
53
|
+
)
|