zettcode 0.1.3__tar.gz → 0.1.4__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.4}/.gitignore +1 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/PKG-INFO +6 -3
- {zettcode-0.1.3 → zettcode-0.1.4}/README.md +2 -1
- {zettcode-0.1.3 → zettcode-0.1.4}/README.zh-CN.md +1 -1
- {zettcode-0.1.3 → zettcode-0.1.4}/pyproject.toml +17 -7
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/__init__.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/__init__.py +3 -2
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/agent.py +143 -18
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/approval.py +20 -3
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/capabilities.py +2 -2
- zettcode-0.1.4/src/zettcode/app/agent/context.py +532 -0
- zettcode-0.1.4/src/zettcode/app/agent/entries.py +218 -0
- zettcode-0.1.4/src/zettcode/app/agent/mentions.py +303 -0
- zettcode-0.1.4/src/zettcode/app/agent/permission.py +146 -0
- zettcode-0.1.4/src/zettcode/app/agent/presenters.py +615 -0
- zettcode-0.1.4/src/zettcode/app/agent/readonly.py +435 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/rendering.py +60 -4
- zettcode-0.1.4/src/zettcode/app/agent/replay.py +156 -0
- zettcode-0.1.4/src/zettcode/app/agent/rows.py +388 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/runtime.py +207 -74
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/storage/__init__.py +2 -1
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/storage/records.py +120 -15
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/storage/store.py +135 -2
- zettcode-0.1.4/src/zettcode/app/agent/transcript/__init__.py +73 -0
- zettcode-0.1.4/src/zettcode/app/agent/transcript/fold.py +56 -0
- zettcode-0.1.4/src/zettcode/app/agent/transcript/log.py +182 -0
- zettcode-0.1.4/src/zettcode/app/agent/transcript/oneoff.py +54 -0
- zettcode-0.1.4/src/zettcode/app/agent/transcript/stream.py +165 -0
- zettcode-0.1.4/src/zettcode/app/agent/transcript/turn.py +164 -0
- zettcode-0.1.4/src/zettcode/app/pipeline.py +191 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/app.py +208 -15
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/boot.py +20 -10
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/commands.py +57 -2
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/demo.py +18 -18
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/keys.py +38 -2
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/labels.py +44 -5
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/notices.py +30 -8
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/rows.py +59 -3
- zettcode-0.1.4/src/zettcode/app/ui/sessions.py +266 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/settings.py +23 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/shell.py +46 -2
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/approval_page.py +38 -8
- zettcode-0.1.4/src/zettcode/app/ui/widgets/context_page.py +114 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/model_page.py +17 -9
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/root.py +8 -1
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/transcript.py +26 -15
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/welcome.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/cli.py +152 -30
- zettcode-0.1.4/src/zettcode/collector.py +44 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/config.py +131 -37
- zettcode-0.1.4/src/zettcode/paths.py +60 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/builtins.py +18 -3
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/state.py +5 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/app.py +15 -2
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/scheduler.py +1 -1
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/input/decoder.py +13 -1
- zettcode-0.1.4/src/zettcode/tui/input/keys.py +259 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/__init__.py +2 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/style.py +23 -4
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/text.py +50 -3
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/runner.py +19 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/terminal.py +8 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/list_page.py +17 -4
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/markdown.py +61 -3
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/text.py +7 -5
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/textarea.py +9 -9
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/update.py +18 -3
- zettcode-0.1.3/src/zettcode/app/agent/blocks.py +0 -367
- 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/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/transcript.py +0 -531
- zettcode-0.1.3/src/zettcode/app/ui/sessions.py +0 -89
- 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.4}/LICENSE +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/ask.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/compaction.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/export.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/mcp.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/projection.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/side.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/storage/metadata.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/subagents.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/title.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/trace/script.js +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/trace/style.css +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/trace/template.html +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/agent/usage.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/commands.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/registry.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/updating.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/completer.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/composer.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/panel.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/steering.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/app/ui/widgets/update_page.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/container.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/extension.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/loader.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/mixins.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/plugins/plugin.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/theme.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/core/widget.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/layout/scroll.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/theme_file.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.1.3 → zettcode-0.1.4}/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.4
|
|
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.14
|
|
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
|
|
@@ -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
|
|
@@ -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.14,<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.4"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -7,7 +7,7 @@ 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": ".
|
|
10
|
+
"Entry": ".entries",
|
|
11
11
|
"Transcript": ".transcript",
|
|
12
12
|
"TranscriptProjector": ".projection",
|
|
13
13
|
"ZettCodeAgent": ".agent",
|
|
@@ -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 .entries import Entry
|
|
20
21
|
from .projection import TranscriptProjector
|
|
21
22
|
from .runtime import ZettCodeRuntime
|
|
22
|
-
from .transcript import
|
|
23
|
+
from .transcript import Transcript
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def __getattr__(name: str) -> object:
|
|
@@ -33,7 +33,8 @@ from .ask import ASK_USER_RESPONSE_EVENT_NAME, AskUserQuestion, answer_payload,
|
|
|
33
33
|
from .context import ContextReport
|
|
34
34
|
from .export import build_trace, render_html, write_export
|
|
35
35
|
from .mentions import MentionProvider, MentionRegistry
|
|
36
|
-
from .
|
|
36
|
+
from .permission import PERMISSION_LEVELS, Permission
|
|
37
|
+
from .replay import Replayed, replay
|
|
37
38
|
from .runtime import ZettCodeRuntime, build_system_prompt
|
|
38
39
|
from .side import SIDE, question
|
|
39
40
|
from .storage import Session, SessionInfo
|
|
@@ -58,6 +59,24 @@ class MentionPart:
|
|
|
58
59
|
PromptPart: TypeAlias = str | MentionPart | tuple[bytes, str]
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class StoredSession:
|
|
64
|
+
"""A session read from the store and rebuilt into rows, not yet made active.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
session_id: The session that was read.
|
|
68
|
+
session: Its parsed file.
|
|
69
|
+
replayed: The rows, totals, and branch it replays into.
|
|
70
|
+
title: Its stored display title, if it has one: read here because folding
|
|
71
|
+
the metadata log is the other thing resuming used to do on the loop.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
session_id: str
|
|
75
|
+
session: Session
|
|
76
|
+
replayed: Replayed
|
|
77
|
+
title: str | None
|
|
78
|
+
|
|
79
|
+
|
|
61
80
|
def plain_text_part(part: PromptPart) -> str:
|
|
62
81
|
"""Return the text one part contributes; an image contributes nothing."""
|
|
63
82
|
if isinstance(part, str):
|
|
@@ -152,6 +171,15 @@ class ZettCodeAgent:
|
|
|
152
171
|
"""
|
|
153
172
|
return self.runtime.persistence.session_title(self.session_id)
|
|
154
173
|
|
|
174
|
+
def session_path(self) -> Path:
|
|
175
|
+
"""Return the file this session is stored in, under the store root.
|
|
176
|
+
|
|
177
|
+
The whole conversation, on disk: what ``/context`` shows beside the token
|
|
178
|
+
breakdown, and what a reader opens when they want the raw records rather
|
|
179
|
+
than a rendered transcript.
|
|
180
|
+
"""
|
|
181
|
+
return self.runtime.persistence.session_path(self.session_id)
|
|
182
|
+
|
|
155
183
|
@property
|
|
156
184
|
def session_name(self) -> str:
|
|
157
185
|
"""Return the name to show for the active session.
|
|
@@ -175,16 +203,36 @@ class ZettCodeAgent:
|
|
|
175
203
|
async def context_report(self) -> ContextReport | None:
|
|
176
204
|
"""Return what the next request carries by source, or None before the first call.
|
|
177
205
|
|
|
178
|
-
The tokenizer is prepared first: it prefers
|
|
179
|
-
character estimate when that vocabulary
|
|
180
|
-
report names so the numbers are not mistaken
|
|
206
|
+
The shares come from the tokenizer, which is prepared first: it prefers
|
|
207
|
+
tiktoken and falls back to a character estimate when that vocabulary
|
|
208
|
+
cannot be opened, which the report names so the numbers are not mistaken
|
|
209
|
+
for exact ones. The total comes from the provider when it counted this
|
|
210
|
+
very request — it counts what it bills — and from the tokenizer when the
|
|
211
|
+
request was primed from the store, whose stored figure belongs to a call
|
|
212
|
+
the dialogue may have outgrown.
|
|
181
213
|
"""
|
|
182
214
|
tokenizer = self.runtime.tokenizer
|
|
183
215
|
await tokenizer.prepare()
|
|
216
|
+
window = self.runtime.active_model.context_window
|
|
217
|
+
counted = self.runtime.usage.snapshot(self.session_id).context_tokens
|
|
218
|
+
# The provider's own count of this conversation is the total: what it
|
|
219
|
+
# billed for the request this process assembled, or — for a session
|
|
220
|
+
# rebuilt from the store, which has no call of its own yet — what it
|
|
221
|
+
# billed for the last call that session made. Locally counting a rebuilt
|
|
222
|
+
# request is not a substitute: on this machine's own session the rebuilt
|
|
223
|
+
# list measured 1.65 M tokens against a provider's 326 k, because a
|
|
224
|
+
# rebuilt list holds messages the live request no longer carries.
|
|
225
|
+
#
|
|
226
|
+
# A count that reaches the window is refused instead: a request cannot
|
|
227
|
+
# carry more than the window it is measured against, so a figure that does
|
|
228
|
+
# is about something else, and a share pinned at a hundred is worse than
|
|
229
|
+
# the measurement.
|
|
230
|
+
reported = counted if 0 < counted < window else None
|
|
184
231
|
return self.runtime.context.report(
|
|
185
232
|
self.session_id,
|
|
186
|
-
window=
|
|
233
|
+
window=window,
|
|
187
234
|
tokenizer=tokenizer,
|
|
235
|
+
reported=reported,
|
|
188
236
|
)
|
|
189
237
|
|
|
190
238
|
@property
|
|
@@ -344,27 +392,81 @@ class ZettCodeAgent:
|
|
|
344
392
|
"""Switch future turns to a session."""
|
|
345
393
|
self.runtime.use_session(session_id)
|
|
346
394
|
|
|
347
|
-
def
|
|
348
|
-
"""
|
|
349
|
-
|
|
395
|
+
def fork_session(self) -> str:
|
|
396
|
+
"""Copy this conversation's context into a new session, and return its id.
|
|
397
|
+
|
|
398
|
+
What is carried over is what a request would send — the newest checkpoint's
|
|
399
|
+
summary and the messages after it — not the whole stored history: the fork
|
|
400
|
+
starts from the conversation the model is actually working with. The shell
|
|
401
|
+
installs the result the way a resume does, so the reader lands in the fork
|
|
402
|
+
with the same conversation in front of them.
|
|
403
|
+
|
|
404
|
+
Synchronous file work on purpose, so the shell can run it on a thread.
|
|
405
|
+
"""
|
|
406
|
+
return self.runtime.persistence.fork(self.session_id)
|
|
407
|
+
|
|
408
|
+
def load_session(self, session_id: str, transcript: Transcript) -> StoredSession:
|
|
409
|
+
"""Read a stored session and rebuild what it will show, changing nothing.
|
|
410
|
+
|
|
411
|
+
This is the expensive half of resuming — parsing the file and replaying the
|
|
412
|
+
newest part of it into rows — and it touches no state the shell is reading:
|
|
413
|
+
the rows are built into a transcript of their own, and the result is a value.
|
|
414
|
+
That is what lets the shell run it on a worker thread while the interface
|
|
415
|
+
keeps answering keys, and hand the result to :meth:`install_session` on the
|
|
416
|
+
loop. The ``transcript`` is only read, for the renderers, presenters, and cap
|
|
417
|
+
the rebuilt one has to match.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
session_id: The session to read.
|
|
421
|
+
transcript: The transcript the result will be installed into.
|
|
422
|
+
|
|
423
|
+
Raises:
|
|
424
|
+
ValueError: The store holds no such session, or its file is corrupt.
|
|
425
|
+
"""
|
|
426
|
+
persistence = self.runtime.persistence
|
|
427
|
+
session = persistence.read(session_id)
|
|
350
428
|
if session.header is None:
|
|
351
429
|
raise ValueError(f"Unknown session: {session_id}")
|
|
352
430
|
replayed = replay(
|
|
353
431
|
session,
|
|
354
432
|
renderers=transcript.renderers,
|
|
355
|
-
|
|
433
|
+
presenters=transcript.presenters,
|
|
356
434
|
frame=transcript.frame,
|
|
435
|
+
max_entries=transcript.max_entries,
|
|
436
|
+
)
|
|
437
|
+
return StoredSession(
|
|
438
|
+
session_id=session_id,
|
|
439
|
+
session=session,
|
|
440
|
+
replayed=replayed,
|
|
441
|
+
title=persistence.session_title(session_id),
|
|
357
442
|
)
|
|
443
|
+
|
|
444
|
+
def install_session(self, stored: StoredSession, transcript: Transcript) -> None:
|
|
445
|
+
"""Make a loaded session the active one: its rows, its totals, its id.
|
|
446
|
+
|
|
447
|
+
The cheap half, and the one that changes state, so it runs where the rest of
|
|
448
|
+
the shell does. Everything it does is assignment.
|
|
449
|
+
"""
|
|
450
|
+
session_id, session, replayed = stored.session_id, stored.session, stored.replayed
|
|
358
451
|
transcript.replace(replayed.transcript.entries)
|
|
359
452
|
self.runtime.usage.seed(session_id, replayed.usage)
|
|
360
|
-
# The instructions come from a request this process already assembled, or
|
|
361
|
-
# from the prompt the runtime would build; the environment and tool notes
|
|
362
|
-
# only exist inside a request, which the report admits.
|
|
363
|
-
instructions = self.runtime.context.instructions or (
|
|
364
|
-
SystemMessage(content=build_system_prompt(self.runtime.config)),
|
|
365
|
-
)
|
|
366
|
-
self.runtime.context.remember(session_id, [*instructions, *replayed.history])
|
|
367
453
|
self.use_session(session_id)
|
|
454
|
+
# Prime the report with what the next turn would send, not with what the
|
|
455
|
+
# transcript shows: the display keeps every message a compaction folded
|
|
456
|
+
# away, and measuring those claims a request several times the size of the
|
|
457
|
+
# real one — 2.9 M tokens against a context of 290 k, which is a share
|
|
458
|
+
# pinned at a hundred. `_context_request` is the one definition of that
|
|
459
|
+
# request; the environment and tool notes only exist inside a real one,
|
|
460
|
+
# which the report admits.
|
|
461
|
+
#
|
|
462
|
+
# It is handed over as a function, not built here: the request holds every
|
|
463
|
+
# message since the last checkpoint, decoding them is most of what a long
|
|
464
|
+
# resume would cost, and nothing reads the list until `/context` does.
|
|
465
|
+
self.runtime.context.remember(session_id, lambda: self._stored_request(session))
|
|
466
|
+
|
|
467
|
+
def restore_session(self, session_id: str, transcript: Transcript) -> None:
|
|
468
|
+
"""Load the active stored branch into the UI before switching sessions."""
|
|
469
|
+
self.install_session(self.load_session(session_id, transcript), transcript)
|
|
368
470
|
|
|
369
471
|
async def list_sessions(self, *, limit: int = 20) -> list[SessionInfo]:
|
|
370
472
|
"""List persisted session metadata by recent activity."""
|
|
@@ -426,6 +528,20 @@ class ZettCodeAgent:
|
|
|
426
528
|
"""Approve every shell command for the rest of this run, without asking."""
|
|
427
529
|
self.runtime.approve_all_shell_commands()
|
|
428
530
|
|
|
531
|
+
@property
|
|
532
|
+
def permission(self) -> Permission:
|
|
533
|
+
"""Return how much of the shell may run without asking."""
|
|
534
|
+
return self.runtime.permission
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def permissions(self) -> tuple[str, ...]:
|
|
538
|
+
"""Return the permission levels, strictest first."""
|
|
539
|
+
return tuple(level.value for level in PERMISSION_LEVELS)
|
|
540
|
+
|
|
541
|
+
def use_permission(self, name: str | Permission) -> Permission:
|
|
542
|
+
"""Set how much of the shell may run without asking, for the rest of this run."""
|
|
543
|
+
return self.runtime.use_permission(name)
|
|
544
|
+
|
|
429
545
|
async def compact(self) -> CompactionEvent | None:
|
|
430
546
|
"""Summarize the conversation now, without adding a turn to it.
|
|
431
547
|
|
|
@@ -477,7 +593,7 @@ class ZettCodeAgent:
|
|
|
477
593
|
async def export_session(self, path: str | Path | None = None) -> Path:
|
|
478
594
|
"""Write the active session, and the context it now carries, to HTML.
|
|
479
595
|
|
|
480
|
-
The conversation is rendered with the same
|
|
596
|
+
The conversation is rendered with the same presenters the shell paints,
|
|
481
597
|
and the request the next turn would send is included beside it, so a
|
|
482
598
|
reader sees both the history and what the model is down to.
|
|
483
599
|
|
|
@@ -522,13 +638,22 @@ class ZettCodeAgent:
|
|
|
522
638
|
if assembled is not None:
|
|
523
639
|
messages, tools = assembled
|
|
524
640
|
return tuple(messages), tuple(tools)
|
|
641
|
+
return self._stored_request(session), self.runtime.context.tools
|
|
642
|
+
|
|
643
|
+
def _stored_request(self, session: Session) -> tuple[AnyMessage, ...]:
|
|
644
|
+
"""Return the messages a request for ``session`` would carry, from what is stored.
|
|
645
|
+
|
|
646
|
+
This depends on the session it is given and on nothing that changes with the
|
|
647
|
+
active one, so it can be run later — a restore hands it over unrun, and the
|
|
648
|
+
reader may have moved to another session by the time it is wanted.
|
|
649
|
+
"""
|
|
525
650
|
instructions = self.runtime.context.instructions or (
|
|
526
651
|
SystemMessage(content=build_system_prompt(self.runtime.config)),
|
|
527
652
|
)
|
|
528
653
|
checkpoint, tail = session.active()
|
|
529
654
|
stored: list[AnyMessage] = [checkpoint.message[0]] if checkpoint is not None else []
|
|
530
655
|
stored.extend(line.message[0] for line in tail if line.message[0].include_in_messages)
|
|
531
|
-
return (*
|
|
656
|
+
return (*instructions, *stored)
|
|
532
657
|
|
|
533
658
|
def _export_meta(self, session: Session) -> tuple[tuple[str, str], ...]:
|
|
534
659
|
"""Return the label/value pairs the exported page's sidebar carries."""
|
|
@@ -14,6 +14,8 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from zett_agent.extensions.shell_approval import ShellApprovalMode
|
|
16
16
|
|
|
17
|
+
from .permission import ShellPolicy
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
class ShellApprovalMemory:
|
|
19
21
|
"""Approval memory for one process: a policy per session, exact commands.
|
|
@@ -21,12 +23,25 @@ class ShellApprovalMemory:
|
|
|
21
23
|
This satisfies ``zett-agent``'s ``ShellApprovalStorage``, which the shell
|
|
22
24
|
extension uses to look up a session's mode and to check and record one
|
|
23
25
|
approved command.
|
|
26
|
+
|
|
27
|
+
The shell extension asks :meth:`is_allowed` before it puts a command to the
|
|
28
|
+
reader, which makes it the one place a permission level can say "do not ask":
|
|
29
|
+
``safe`` answers yes for a command that cannot change anything and ``auto``
|
|
30
|
+
answers yes for every command, while ``ask`` falls through to the commands the
|
|
31
|
+
reader approved by hand. ``deny`` is not decided here, because refusing is not
|
|
32
|
+
a thing this question can express.
|
|
24
33
|
"""
|
|
25
34
|
|
|
26
|
-
def __init__(self) -> None:
|
|
27
|
-
"""Start with no session policy and nothing approved.
|
|
35
|
+
def __init__(self, policy: ShellPolicy | None = None) -> None:
|
|
36
|
+
"""Start with no session policy and nothing approved.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
policy: The permission level to consult; without one every command
|
|
40
|
+
is put to the reader, which is what ``ask`` does.
|
|
41
|
+
"""
|
|
28
42
|
self._modes: dict[str, ShellApprovalMode] = {}
|
|
29
43
|
self._allowed: set[str] = set()
|
|
44
|
+
self._policy = policy
|
|
30
45
|
|
|
31
46
|
async def get_session_mode(self, session_id: str) -> ShellApprovalMode:
|
|
32
47
|
"""Return a session's policy, which is review until one is set."""
|
|
@@ -41,7 +56,9 @@ class ShellApprovalMemory:
|
|
|
41
56
|
self._modes.pop(session_id, None)
|
|
42
57
|
|
|
43
58
|
async def is_allowed(self, session_id: str, command: str) -> bool:
|
|
44
|
-
"""Return whether
|
|
59
|
+
"""Return whether a command may run without asking: by level, or approved by hand."""
|
|
60
|
+
if self._policy is not None and self._policy.allows_unasked(command):
|
|
61
|
+
return True
|
|
45
62
|
return command in self._allowed
|
|
46
63
|
|
|
47
64
|
async def allow_command(self, command: str) -> None:
|
|
@@ -6,7 +6,7 @@ from collections.abc import Mapping
|
|
|
6
6
|
|
|
7
7
|
from zett_agent.agent import AgentRunContext
|
|
8
8
|
from zett_agent.extensions.base import AgentExtension
|
|
9
|
-
from zett_agent.
|
|
9
|
+
from zett_agent.model import AgentModel
|
|
10
10
|
from zett_agent.tools.images import view_image
|
|
11
11
|
|
|
12
12
|
from ...config import ModelConfig
|
|
@@ -29,7 +29,7 @@ class ModelCapabilities(AgentExtension):
|
|
|
29
29
|
#: After the coding bundle, which is what registers the tool being filtered.
|
|
30
30
|
priority = 110
|
|
31
31
|
|
|
32
|
-
def __init__(self, models: Mapping[ModelConfig,
|
|
32
|
+
def __init__(self, models: Mapping[ModelConfig, AgentModel]) -> None:
|
|
33
33
|
"""Remember the runtime's providers, keyed by the model each one serves.
|
|
34
34
|
|
|
35
35
|
The mapping is the runtime's own and grows as models are selected, so
|