zettcode 0.1.9__tar.gz → 0.1.10__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.9 → zettcode-0.1.10}/PKG-INFO +2 -2
- {zettcode-0.1.9 → zettcode-0.1.10}/pyproject.toml +1 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/__init__.py +1 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/settings.py +36 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/presenters.py +14 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/projection.py +58 -10
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/rendering.py +59 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/replay.py +32 -2
- zettcode-0.1.10/src/zettcode/app/agent/conversation/server_tools.py +157 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/text.py +3 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/tips.py +4 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/stream.py +18 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/ask.py +17 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/side.py +11 -2
- zettcode-0.1.10/src/zettcode/app/agent/extensions/capabilities.py +68 -0
- zettcode-0.1.10/src/zettcode/app/agent/extensions/files.py +101 -0
- zettcode-0.1.10/src/zettcode/app/agent/extensions/harness.py +89 -0
- zettcode-0.1.10/src/zettcode/app/agent/extensions/harness_switch.py +18 -0
- zettcode-0.1.10/src/zettcode/app/agent/extensions/relay.py +93 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/subagents.py +96 -22
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/tool_output.py +8 -7
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/permission/__init__.py +47 -15
- zettcode-0.1.10/src/zettcode/app/agent/permission/sandbox.py +124 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/runtime.py +97 -17
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/metadata.py +7 -2
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/records.py +3 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/store.py +47 -0
- zettcode-0.1.10/src/zettcode/app/agent/tools/__init__.py +7 -0
- zettcode-0.1.10/src/zettcode/app/agent/tools/boundary.py +51 -0
- zettcode-0.1.10/src/zettcode/app/agent/tools/files.py +267 -0
- zettcode-0.1.10/src/zettcode/app/agent/tools/images.py +32 -0
- zettcode-0.1.10/src/zettcode/app/agent/tools/output.py +74 -0
- zettcode-0.1.10/src/zettcode/app/agent/tools/shell.py +171 -0
- zettcode-0.1.10/src/zettcode/app/agent/tools/workspace.py +268 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/app.py +38 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/bars.py +1 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/config_form.py +63 -8
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/keys.py +28 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/notices.py +3 -3
- zettcode-0.1.10/src/zettcode/app/ui/providers.py +614 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/settings.py +5 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/shell.py +1 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/__init__.py +14 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/approval_page.py +9 -9
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/completer.py +3 -4
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/follow_up.py +4 -6
- zettcode-0.1.10/src/zettcode/app/ui/widgets/form_page.py +380 -0
- zettcode-0.1.10/src/zettcode/app/ui/widgets/model_settings_page.py +654 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/pet.py +44 -19
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/statusline_page.py +31 -18
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/transcript.py +47 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/usage_page.py +25 -7
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/cli.py +150 -20
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/config.py +136 -9
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/config_edit.py +27 -1
- zettcode-0.1.10/src/zettcode/oneshot.py +200 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/paths.py +4 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/builtin_agent.py +92 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/builtin_commands.py +46 -5
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/services.py +1 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/state.py +2 -2
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/providers.py +6 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/client.py +25 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/models.py +11 -3
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/types.py +5 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/__init__.py +3 -1
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/capabilities.py +6 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/theme.py +8 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/renderer.py +45 -7
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/theme_file.py +17 -7
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/textarea.py +1 -1
- zettcode-0.1.9/src/zettcode/app/agent/extensions/capabilities.py +0 -50
- zettcode-0.1.9/src/zettcode/app/ui/providers.py +0 -567
- {zettcode-0.1.9 → zettcode-0.1.10}/.gitignore +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/README.md +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/base.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/commands.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/context.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/exports.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/interaction.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/prompt.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/sessions.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/agent/turns.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/changes.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/effort_gauge.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/entries.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/pulse.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/shimmer.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/stops.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/conversation/transcript/turn.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/mentions.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/relay.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/dialogue/title.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/errors.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/script.js +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/style.css +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/export/trace/template.html +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/compaction.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/context.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/mcp.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/messaging.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/messenger.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/extensions/usage.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/permission/approval.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/permission/readonly.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/layout.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/ledger.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/agent/storage/locks.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/arguments.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/completion.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/history.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commandline/runner.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/commands.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/cat.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/chibi.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/dog.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/kangaroo.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/model.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/penguin.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/polarbear.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/scarfy.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/seagull.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/slime.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/snowbear.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/sprout.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/tiger.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pets/whale.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/pipeline.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/registry.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/boot.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/commands.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/demo.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/followups.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/gc_policy.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/labels.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/messaging.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/sessions.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/updating.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/bash_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/composer.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/context_charts.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/context_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/effort_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/input_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/model_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/panel.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/pet_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/root.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/self_compact_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/steering.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/update_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/usage_charts.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/app/ui/widgets/welcome.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/__main__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/client.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/files.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/finder.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/hub.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/log.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/models.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/process.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/protocol.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/roster.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/runner.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/server.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/text.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/broker/web.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/collector.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/doctor.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/filelock.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/builtins.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/container.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/extension.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/loader.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/mixins.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/plugins/plugin.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/py.typed +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/_translate.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/errors.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/events.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/handlers.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/sdk/session.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/statusline.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/clipboard.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/app.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/scheduler.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/core/widget.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/decoder.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/keys.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/scroll.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/ansi.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/style.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/render/text.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/runner.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/terminal.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/ayu-mirage.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/catppuccin-latte.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/catppuccin-mocha.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/dracula.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/github-dimmed.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/github-light.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/gruvbox-dark.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/gruvbox-light.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/kanagawa.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/nord.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/one-dark.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/rose-pine-dawn.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/rose-pine.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/solarized-dark.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/solarized-light.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/themes/tokyo-night.toml +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/console.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/list_page.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/markdown.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/text.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/tui/widgets/toast.py +0 -0
- {zettcode-0.1.9 → zettcode-0.1.10}/src/zettcode/update.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.10
|
|
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
|
|
@@ -32,7 +32,7 @@ Requires-Dist: truststore<1,>=0.10
|
|
|
32
32
|
Requires-Dist: uvicorn<1,>=0.35
|
|
33
33
|
Requires-Dist: wcwidth<1,>=0.2
|
|
34
34
|
Requires-Dist: websockets<17,>=15
|
|
35
|
-
Requires-Dist: zett-agent<0.
|
|
35
|
+
Requires-Dist: zett-agent<0.3,>=0.2.0
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
|
|
38
38
|
# ZettCode
|
|
@@ -53,7 +53,7 @@ dependencies = [
|
|
|
53
53
|
# than it did. An exact pin would make zettcode fight every other tool that
|
|
54
54
|
# shares the dependency — including an upgrade of zett-agent itself, which pip
|
|
55
55
|
# would downgrade to satisfy it.
|
|
56
|
-
"zett-agent>=0.
|
|
56
|
+
"zett-agent>=0.2.0,<0.3",
|
|
57
57
|
]
|
|
58
58
|
|
|
59
59
|
[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.10"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -14,6 +14,7 @@ from zettcode.config_edit import (
|
|
|
14
14
|
remove_model,
|
|
15
15
|
replace_model,
|
|
16
16
|
set_pet,
|
|
17
|
+
set_pet_position,
|
|
17
18
|
set_self_compaction,
|
|
18
19
|
set_statusline,
|
|
19
20
|
set_theme,
|
|
@@ -208,6 +209,29 @@ class SettingsMixin(AgentBase):
|
|
|
208
209
|
self.runtime.config = replace(self.runtime.config, pet=name)
|
|
209
210
|
return self.config_file
|
|
210
211
|
|
|
212
|
+
def remember_pet_position(self, across: float, down: float) -> Path:
|
|
213
|
+
"""Write ``[ui] pet_position`` into the config file, so the next start puts the pet back where it was left.
|
|
214
|
+
|
|
215
|
+
The file is global to the reader (``~/.zettcode/config.toml``), not to a project: a pet is the reader's
|
|
216
|
+
companion, and it does not move house when they open another folder. Only that line changes, and a
|
|
217
|
+
config file that does not exist is not created for it.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The file written.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ConfigEditError: When there is no config file, it cannot be read or edited safely, or it cannot be
|
|
224
|
+
written. Nothing is written then.
|
|
225
|
+
"""
|
|
226
|
+
text = set_pet_position(self._read_config_file(), across, down, where=self.config_file)
|
|
227
|
+
try:
|
|
228
|
+
write_text_atomically(self.config_file, text)
|
|
229
|
+
except OSError as error:
|
|
230
|
+
raise ConfigEditError(f"{self.config_file} could not be written: {error}") from error
|
|
231
|
+
position = (round(min(1.0, max(0.0, across)), 3), round(min(1.0, max(0.0, down)), 3))
|
|
232
|
+
self.runtime.config = replace(self.runtime.config, pet_position=position)
|
|
233
|
+
return self.config_file
|
|
234
|
+
|
|
211
235
|
@property
|
|
212
236
|
def statusline_hidden(self) -> frozenset[str]:
|
|
213
237
|
"""Return the names of the header and status-line parts the reader switched off."""
|
|
@@ -322,6 +346,15 @@ class SettingsMixin(AgentBase):
|
|
|
322
346
|
"""Return the permission levels, strictest first."""
|
|
323
347
|
return tuple(level.value for level in PERMISSION_LEVELS)
|
|
324
348
|
|
|
325
|
-
def use_permission(self, name: str | Permission) -> Permission:
|
|
326
|
-
"""Set how much of the shell may run without asking,
|
|
327
|
-
return self.runtime.use_permission(name)
|
|
349
|
+
def use_permission(self, name: str | Permission, *, remember: bool = True) -> Permission:
|
|
350
|
+
"""Set how much of the shell may run without asking, and by default remember it for this session."""
|
|
351
|
+
return self.runtime.use_permission(name, remember=remember)
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def harness(self) -> bool:
|
|
355
|
+
"""Return whether the just-in-time harness is on for the next request."""
|
|
356
|
+
return self.runtime.harness.on
|
|
357
|
+
|
|
358
|
+
def use_harness(self, on: bool) -> bool:
|
|
359
|
+
"""Turn the just-in-time harness on or off for later requests, for the rest of this run."""
|
|
360
|
+
return self.runtime.use_harness(on)
|
|
@@ -106,6 +106,8 @@ TOOL_ROLES = {
|
|
|
106
106
|
"view_image": "image",
|
|
107
107
|
"glob": "search",
|
|
108
108
|
"grep": "search",
|
|
109
|
+
"web_search": "web-search",
|
|
110
|
+
"ask_user": "ask-user",
|
|
109
111
|
"write_file": "write",
|
|
110
112
|
"edit": "write",
|
|
111
113
|
"replace_in_file": "write", # what a session stored before the rename called it
|
|
@@ -120,8 +122,12 @@ TOOL_ROLES = {
|
|
|
120
122
|
|
|
121
123
|
|
|
122
124
|
def tool_color(tool: str, theme: Theme) -> str:
|
|
123
|
-
"""Return the colour one tool paints its rows with.
|
|
124
|
-
|
|
125
|
+
"""Return the colour one tool paints its rows with.
|
|
126
|
+
|
|
127
|
+
A role spelled with a hyphen (``web-search``) names the theme field it stands for (``web_search``).
|
|
128
|
+
"""
|
|
129
|
+
role = TOOL_ROLES.get(tool, "").replace("-", "_")
|
|
130
|
+
return getattr(theme.tools, role, theme.accent)
|
|
125
131
|
|
|
126
132
|
|
|
127
133
|
#: The entry fields that are not part of how a row looks: its identity, and the
|
|
@@ -575,7 +581,12 @@ class ToolPresenter(Presenter[T]):
|
|
|
575
581
|
}.get(entry.status, STATUS)
|
|
576
582
|
)
|
|
577
583
|
hue = tool_color(entry.tool, theme)
|
|
578
|
-
|
|
584
|
+
# A failure is the error colour, and a row that did not happen at all — a skipped tool, a cancelled
|
|
585
|
+
# question — is a caution rather than a result, so it is amber and not the tool's own hue.
|
|
586
|
+
colour = {
|
|
587
|
+
EntryStatus.FAILED: theme.error,
|
|
588
|
+
EntryStatus.SKIPPED: theme.warning,
|
|
589
|
+
}.get(entry.status, hue)
|
|
579
590
|
marker = Style(foreground=colour)
|
|
580
591
|
label = Style(foreground=colour)
|
|
581
592
|
muted = Style(foreground=theme.muted)
|
|
@@ -10,11 +10,12 @@ from zett_agent.events import AgentEvent
|
|
|
10
10
|
from zett_agent.extensions.shell_approval import SHELL_APPROVAL_EVENT_NAME
|
|
11
11
|
from zett_agent.messages import ToolMessage
|
|
12
12
|
|
|
13
|
-
from ..dialogue.ask import AskUserQuestion, question_from
|
|
13
|
+
from ..dialogue.ask import AskUserQuestion, declined_answer, question_from
|
|
14
14
|
from ..dialogue.relay import Relayed
|
|
15
15
|
from ..extensions.mcp_notice import MCP_NOTICE_EVENT_NAME, notice_from
|
|
16
16
|
from ..extensions.usage import USAGE_EVENT_NAME
|
|
17
17
|
from .entries import EntryStatus
|
|
18
|
+
from .server_tools import ServerToolRun, describe_output
|
|
18
19
|
from .stops import stop_text
|
|
19
20
|
from .transcript import Transcript
|
|
20
21
|
|
|
@@ -82,6 +83,10 @@ class TranscriptProjector(AgentEventDispatcher):
|
|
|
82
83
|
self.on_usage = on_usage
|
|
83
84
|
self.on_steering_started = on_steering_started
|
|
84
85
|
self.on_steering_interrupted = on_steering_interrupted
|
|
86
|
+
#: What a hosted tool was called with while it streams: a start may carry the whole input, or only its
|
|
87
|
+
#: opening and then fragments. Kept per request and dropped when the call reports back.
|
|
88
|
+
self._server_inputs: dict[str, dict[str, object]] = {}
|
|
89
|
+
self._server_fragments: dict[str, str] = {}
|
|
85
90
|
|
|
86
91
|
def begin_turn(self, prompt: str, *, side: bool = False, relayed: Relayed | None = None) -> None:
|
|
87
92
|
"""Echo the prompt into the transcript before the run starts.
|
|
@@ -171,9 +176,19 @@ class TranscriptProjector(AgentEventDispatcher):
|
|
|
171
176
|
self.transcript.start_tool(call.id, call.name, call.arguments)
|
|
172
177
|
|
|
173
178
|
async def on_tool_completed_event(self, event: AgentEvent) -> None:
|
|
174
|
-
"""Attach the returned message to the row of the call it answers.
|
|
179
|
+
"""Attach the returned message to the row of the call it answers.
|
|
180
|
+
|
|
181
|
+
A question the reader cancelled comes back as a result, not a failure: the model reads that it was declined
|
|
182
|
+
and carries on. The row is marked skipped so it does not look like a question that was answered.
|
|
183
|
+
"""
|
|
175
184
|
if isinstance(event.message, ToolMessage):
|
|
176
|
-
|
|
185
|
+
output = tool_output(event.message)
|
|
186
|
+
declined = event.message.name == "ask_user" and declined_answer(output)
|
|
187
|
+
self.transcript.complete_tool(
|
|
188
|
+
event.message.tool_call_id,
|
|
189
|
+
output,
|
|
190
|
+
status=EntryStatus.SKIPPED if declined else EntryStatus.COMPLETED,
|
|
191
|
+
)
|
|
177
192
|
|
|
178
193
|
async def on_tool_failed_event(self, event: AgentEvent) -> None:
|
|
179
194
|
"""Fail every call in the batch and record the error text."""
|
|
@@ -188,23 +203,56 @@ class TranscriptProjector(AgentEventDispatcher):
|
|
|
188
203
|
self.transcript.complete_tool(call.id, output, status=EntryStatus.SKIPPED)
|
|
189
204
|
|
|
190
205
|
async def on_server_tool_started_event(self, event: AgentEvent) -> None:
|
|
191
|
-
"""Open a row for a tool the server runs on the agent's behalf."""
|
|
192
|
-
|
|
193
|
-
|
|
206
|
+
"""Open a row for a tool the server runs on the agent's behalf, with what it was asked."""
|
|
207
|
+
call = event.server_tool_call
|
|
208
|
+
if call is None:
|
|
209
|
+
return
|
|
210
|
+
arguments = dict(call.input or {})
|
|
211
|
+
self._server_inputs[call.id] = arguments
|
|
212
|
+
self._server_fragments.pop(call.id, None)
|
|
213
|
+
self.transcript.start_tool(call.id, call.name, arguments)
|
|
214
|
+
|
|
215
|
+
async def on_server_tool_input_delta_event(self, event: AgentEvent) -> None:
|
|
216
|
+
"""Keep the fragments of a hosted call's input; the query a search ran is in there."""
|
|
217
|
+
delta = event.server_tool_input_delta
|
|
218
|
+
if delta is not None:
|
|
219
|
+
self._server_fragments[delta.call_id] = self._server_fragments.get(delta.call_id, "") + delta.delta
|
|
220
|
+
|
|
221
|
+
def _server_arguments(self, call_id: str) -> dict[str, object]:
|
|
222
|
+
"""Return what a hosted call turned out to be called with: its opening input and its fragments."""
|
|
223
|
+
arguments = dict(self._server_inputs.pop(call_id, {}))
|
|
224
|
+
fragments = self._server_fragments.pop(call_id, "")
|
|
225
|
+
if fragments:
|
|
226
|
+
try:
|
|
227
|
+
parsed = json.loads(fragments)
|
|
228
|
+
except ValueError:
|
|
229
|
+
parsed = None
|
|
230
|
+
if isinstance(parsed, dict):
|
|
231
|
+
arguments.update(parsed)
|
|
232
|
+
return arguments
|
|
194
233
|
|
|
195
234
|
async def on_server_tool_completed_event(self, event: AgentEvent) -> None:
|
|
196
|
-
"""Attach a
|
|
235
|
+
"""Attach a hosted tool's result to its row: the pages it found, never the provider's payload."""
|
|
197
236
|
result = event.server_tool_result
|
|
198
237
|
if result is not None:
|
|
199
|
-
self.
|
|
238
|
+
arguments = self._server_arguments(result.call_id)
|
|
239
|
+
if arguments:
|
|
240
|
+
# The row opened before its input had streamed in; now it knows what was searched for.
|
|
241
|
+
self.transcript.restate_tool(result.call_id, arguments)
|
|
242
|
+
run = ServerToolRun(result.call_id, result.name, arguments, output=result.output, error=result.error_code)
|
|
243
|
+
self.transcript.complete_tool(result.call_id, describe_output(run) or serialize(result.output))
|
|
200
244
|
|
|
201
245
|
async def on_server_tool_failed_event(self, event: AgentEvent) -> None:
|
|
202
|
-
"""Attach a
|
|
246
|
+
"""Attach a hosted tool's failure, preferring the error code over the payload."""
|
|
203
247
|
result = event.server_tool_result
|
|
204
248
|
if result is not None:
|
|
249
|
+
arguments = self._server_arguments(result.call_id)
|
|
250
|
+
if arguments:
|
|
251
|
+
self.transcript.restate_tool(result.call_id, arguments)
|
|
252
|
+
run = ServerToolRun(result.call_id, result.name, arguments, output=result.output, error=result.error_code)
|
|
205
253
|
self.transcript.complete_tool(
|
|
206
254
|
result.call_id,
|
|
207
|
-
|
|
255
|
+
describe_output(run) or serialize(result.output),
|
|
208
256
|
status=EntryStatus.FAILED,
|
|
209
257
|
)
|
|
210
258
|
|
|
@@ -34,7 +34,7 @@ from zettcode.tui import ELLIPSIS, SEPARATOR
|
|
|
34
34
|
from zettcode.tui.render import GENERIC, language_for
|
|
35
35
|
|
|
36
36
|
from .entries import DeleteEntry, EditEntry, MessageEntry, ReadEntry, ToolEntry, WriteEntry
|
|
37
|
-
from .text import limit_output
|
|
37
|
+
from .text import DECLINED_NOTE, limit_output
|
|
38
38
|
|
|
39
39
|
#: Output kinds the chain shapes; a link claims one or more of them.
|
|
40
40
|
TOOL = "tool"
|
|
@@ -386,8 +386,8 @@ class WriteTool(ToolHandler):
|
|
|
386
386
|
names = frozenset({"write_file"})
|
|
387
387
|
|
|
388
388
|
def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
|
|
389
|
-
"""Show the file being written."""
|
|
390
|
-
return ToolRow(f"
|
|
389
|
+
"""Show the file being written: the row is the call, so it says what the call does, not what it did."""
|
|
390
|
+
return ToolRow(f"Write {argument(arguments, 'path', '(unknown file)')}")
|
|
391
391
|
|
|
392
392
|
def open_tool(
|
|
393
393
|
self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
|
|
@@ -497,6 +497,60 @@ class SearchTool(ToolHandler):
|
|
|
497
497
|
return ToolRow(f"Searched {pattern or 'the workspace'}" + (f" in {where}" if where else ""))
|
|
498
498
|
|
|
499
499
|
|
|
500
|
+
class WebSearchTool(ToolHandler):
|
|
501
|
+
"""``web_search``: a search the endpoint ran, so the row says what was searched for."""
|
|
502
|
+
|
|
503
|
+
names = frozenset({"web_search"})
|
|
504
|
+
|
|
505
|
+
def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
|
|
506
|
+
"""Name the query; the call is the endpoint's, so nothing else of it is ours to show."""
|
|
507
|
+
query = argument(arguments, "query")
|
|
508
|
+
return ToolRow(f'Searched the web for "{query}"' if query else "Searched the web")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class AskUserTool(ToolHandler):
|
|
512
|
+
"""``ask_user``: the one row that waited for a person, so it is named as a thing, not as a verb.
|
|
513
|
+
|
|
514
|
+
The body is what the person did, not the protocol: the tool answers with the payload the shell sent, and that
|
|
515
|
+
JSON is neither readable nor useful in a transcript.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
names = frozenset({"ask_user"})
|
|
519
|
+
|
|
520
|
+
def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
|
|
521
|
+
"""Show the question that was put, under the name the shell gives that tool."""
|
|
522
|
+
question = argument(arguments, "question")
|
|
523
|
+
return ToolRow(f"Ask-User: {question}" if question else "Ask-User")
|
|
524
|
+
|
|
525
|
+
def body(self, name: str, output: str) -> str:
|
|
526
|
+
"""Say what the person did: declined, answered, or answered with a picture.
|
|
527
|
+
|
|
528
|
+
Anything that is not the answer payload is left exactly as it came, so a failure or another tool's result
|
|
529
|
+
is never rewritten by this row.
|
|
530
|
+
"""
|
|
531
|
+
payload = _ask_payload(output)
|
|
532
|
+
if payload is None:
|
|
533
|
+
return output
|
|
534
|
+
if payload.get("declined") is True:
|
|
535
|
+
return DECLINED_NOTE
|
|
536
|
+
answer = payload.get("answer")
|
|
537
|
+
if isinstance(answer, str) and answer.strip():
|
|
538
|
+
return f"You answered: {answer.strip()}"
|
|
539
|
+
if isinstance(payload.get("parts"), list) and payload["parts"]:
|
|
540
|
+
return "You answered with a picture."
|
|
541
|
+
return output
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _ask_payload(output: str) -> Mapping[str, object] | None:
|
|
545
|
+
"""Return the payload of an ``ask_user`` result, or ``None`` when the text is not one."""
|
|
546
|
+
try:
|
|
547
|
+
result = json.loads(output)
|
|
548
|
+
except ValueError:
|
|
549
|
+
return None
|
|
550
|
+
payload = result.get("payload") if isinstance(result, Mapping) else None
|
|
551
|
+
return payload if isinstance(payload, Mapping) else None
|
|
552
|
+
|
|
553
|
+
|
|
500
554
|
class TodoTool(ToolHandler):
|
|
501
555
|
"""``todo_write``: a plan, so the number of steps is the story."""
|
|
502
556
|
|
|
@@ -645,6 +699,8 @@ DEFAULT_RENDERERS = Renderers(
|
|
|
645
699
|
DeleteTool(),
|
|
646
700
|
ShellTool(),
|
|
647
701
|
SearchTool(),
|
|
702
|
+
WebSearchTool(),
|
|
703
|
+
AskUserTool(),
|
|
648
704
|
TodoTool(),
|
|
649
705
|
SkillTool(),
|
|
650
706
|
TaskTool(),
|
|
@@ -19,6 +19,7 @@ from zett_agent.messages import AnyMessage, AssistantMessage, SystemMessage, Too
|
|
|
19
19
|
|
|
20
20
|
from zettcode.config import DEFAULT_TRANSCRIPT_MAX_ENTRIES
|
|
21
21
|
|
|
22
|
+
from ..dialogue.ask import declined_answer
|
|
22
23
|
from ..dialogue.relay import relay_of
|
|
23
24
|
from ..dialogue.side import is_side_line
|
|
24
25
|
from ..extensions.usage import UsageSnapshot
|
|
@@ -26,6 +27,7 @@ from ..storage import MessageLine, Session
|
|
|
26
27
|
from .entries import EntryStatus
|
|
27
28
|
from .presenters import DEFAULT_PRESENTERS, Presenters
|
|
28
29
|
from .rendering import DEFAULT_RENDERERS, Renderers
|
|
30
|
+
from .server_tools import ServerToolRun, answer_after_tools, describe_output, runs_in_blocks
|
|
29
31
|
from .stops import stop_text
|
|
30
32
|
from .transcript import Transcript
|
|
31
33
|
|
|
@@ -75,7 +77,13 @@ def entries_added_by(message: AnyMessage) -> int:
|
|
|
75
77
|
return 1
|
|
76
78
|
case AssistantMessage():
|
|
77
79
|
stopped = message.stop is not None and not message.stop.ok
|
|
78
|
-
return
|
|
80
|
+
return (
|
|
81
|
+
bool(message.reasoning)
|
|
82
|
+
+ bool(message.content)
|
|
83
|
+
+ len(message.tool_calls)
|
|
84
|
+
+ len(runs_in_blocks(message.replay_blocks))
|
|
85
|
+
+ stopped
|
|
86
|
+
)
|
|
79
87
|
case _:
|
|
80
88
|
return 0
|
|
81
89
|
|
|
@@ -152,7 +160,26 @@ def replay(
|
|
|
152
160
|
case AssistantMessage():
|
|
153
161
|
if message.reasoning:
|
|
154
162
|
transcript.restore_thinking(message.reasoning, line.timing.reasoning_duration_ns)
|
|
155
|
-
|
|
163
|
+
runs = runs_in_blocks(message.replay_blocks)
|
|
164
|
+
|
|
165
|
+
def hosted(rows: Sequence[ServerToolRun] = runs) -> None:
|
|
166
|
+
"""Add the rows of the tools the endpoint ran, each closed by its own result."""
|
|
167
|
+
for run in rows:
|
|
168
|
+
transcript.start_tool(run.call_id, run.name, run.arguments)
|
|
169
|
+
transcript.complete_tool(
|
|
170
|
+
run.call_id,
|
|
171
|
+
describe_output(run),
|
|
172
|
+
status=EntryStatus.FAILED if run.error else EntryStatus.COMPLETED,
|
|
173
|
+
wait=False,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# A hosted tool has no message of its own: its call and result are both in the answer's blocks, kept
|
|
177
|
+
# because the provider needs them back. Where the answer's text sits in those blocks is where the row
|
|
178
|
+
# goes relative to it, so a reopened session reads in the order the run did.
|
|
179
|
+
if not answer_after_tools(message.replay_blocks) and message.content:
|
|
180
|
+
transcript.append_answer(message.content)
|
|
181
|
+
hosted()
|
|
182
|
+
if answer_after_tools(message.replay_blocks) and message.content:
|
|
156
183
|
transcript.append_answer(message.content)
|
|
157
184
|
for call in message.tool_calls:
|
|
158
185
|
transcript.start_tool(call.id, call.name, call.arguments)
|
|
@@ -160,6 +187,9 @@ def replay(
|
|
|
160
187
|
transcript.error(stop_text(message.stop))
|
|
161
188
|
case ToolMessage():
|
|
162
189
|
status = EntryStatus.COMPLETED if message.success else EntryStatus.FAILED
|
|
190
|
+
if message.name == "ask_user" and declined_answer(message.text):
|
|
191
|
+
# A cancelled question is an answer of its own, not a failure.
|
|
192
|
+
status = EntryStatus.SKIPPED
|
|
163
193
|
transcript.complete_tool(message.tool_call_id, message.text, status=status, wait=False)
|
|
164
194
|
case _:
|
|
165
195
|
continue
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""What a provider-run tool left behind: the rows for a hosted search, live and replayed.
|
|
2
|
+
|
|
3
|
+
A hosted tool is not called by the agent: the endpoint runs it, and the framework reports the call and its result as
|
|
4
|
+
events. Those events are not messages, so the store never sees them; but the provider's own blocks are kept as the
|
|
5
|
+
answer's opaque ``replay_blocks``, which the store writes and reads back. This module is the one place that turns
|
|
6
|
+
either of those into the words of a row, so a resumed session shows what the live one showed.
|
|
7
|
+
|
|
8
|
+
The result of a search is a list of pages, each with a title, a URL and an opaque encrypted payload that must never be
|
|
9
|
+
put in front of the reader or sent back in a prompt as text: only the title and the URL are shown here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Iterable, Mapping
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
#: The most pages one row lists. A row is a note that a search happened, not the search's own page.
|
|
19
|
+
MAX_RESULTS = 5
|
|
20
|
+
|
|
21
|
+
#: Result blocks whose payload is the provider's own and is never shown as text.
|
|
22
|
+
_SEARCH_RESULT_TYPES = frozenset({"web_search_result", "search_result"})
|
|
23
|
+
|
|
24
|
+
#: Block types that record a hosted call rather than text.
|
|
25
|
+
_CALL_TYPES = frozenset({"server_tool_use"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class ServerToolRun:
|
|
30
|
+
"""One hosted tool call as the transcript needs it.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
call_id: The provider's identifier for the call.
|
|
34
|
+
name: The tool's name, such as ``web_search``.
|
|
35
|
+
arguments: What it was asked, when the provider said; ``{"query": ...}`` for a search.
|
|
36
|
+
output: What it returned, as the provider reported it.
|
|
37
|
+
error: The provider's error code when the tool failed.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
call_id: str
|
|
41
|
+
name: str
|
|
42
|
+
arguments: Mapping[str, Any]
|
|
43
|
+
output: Any = None
|
|
44
|
+
error: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _results_in(output: Any) -> list[Mapping[str, Any]]:
|
|
48
|
+
"""Return the page blocks of a search result, wherever the provider put them.
|
|
49
|
+
|
|
50
|
+
The adapter hands a hosted tool's output over as the block's own ``content``: a list of pages for a search, with
|
|
51
|
+
the provider's encrypted payload beside each title and URL.
|
|
52
|
+
"""
|
|
53
|
+
match output:
|
|
54
|
+
case bool() | str() | int() | float() | None:
|
|
55
|
+
return []
|
|
56
|
+
case Mapping():
|
|
57
|
+
content = output.get("content")
|
|
58
|
+
if isinstance(content, Iterable) and not isinstance(content, str):
|
|
59
|
+
return [item for item in content if isinstance(item, Mapping)]
|
|
60
|
+
if output.get("type") in _SEARCH_RESULT_TYPES:
|
|
61
|
+
return [output]
|
|
62
|
+
return []
|
|
63
|
+
case Iterable():
|
|
64
|
+
return [item for item in output if isinstance(item, Mapping)]
|
|
65
|
+
case _:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def describe_search(output: Any) -> str:
|
|
70
|
+
"""Return a search result as the few lines a row shows: how many pages, and which.
|
|
71
|
+
|
|
72
|
+
Only a page's title and URL are shown; the encrypted payload the provider returns beside them is its own business
|
|
73
|
+
and is neither printed nor kept.
|
|
74
|
+
"""
|
|
75
|
+
pages = _results_in(output)
|
|
76
|
+
if not pages:
|
|
77
|
+
return "No pages"
|
|
78
|
+
lines = [f"{len(pages)} page{'s' if len(pages) != 1 else ''}"]
|
|
79
|
+
for page in pages[:MAX_RESULTS]:
|
|
80
|
+
title = page.get("title")
|
|
81
|
+
url = page.get("url")
|
|
82
|
+
named = title.strip() if isinstance(title, str) and title.strip() else ""
|
|
83
|
+
address = url.strip() if isinstance(url, str) and url.strip() else ""
|
|
84
|
+
lines.append(f"- {named} \u2014 {address}" if named and address else f"- {named or address}")
|
|
85
|
+
if len(pages) > MAX_RESULTS:
|
|
86
|
+
lines.append(f"\u2026 and {len(pages) - MAX_RESULTS} more")
|
|
87
|
+
return "\n".join(lines)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def describe_output(run: ServerToolRun) -> str:
|
|
91
|
+
"""Return what one hosted tool said, in the words of its own kind."""
|
|
92
|
+
if run.error:
|
|
93
|
+
return run.error
|
|
94
|
+
if run.name == "web_search" or _results_in(run.output):
|
|
95
|
+
return describe_search(run.output)
|
|
96
|
+
# Another provider's hosted tool: its payload is already text or a plain structure.
|
|
97
|
+
return run.output if isinstance(run.output, str) else ""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def runs_in_blocks(blocks: Iterable[Mapping[str, Any]]) -> list[ServerToolRun]:
|
|
101
|
+
"""Return the hosted tool calls one answer's ``replay_blocks`` record, results paired with their calls.
|
|
102
|
+
|
|
103
|
+
A block that is not a hosted call is skipped: a thinking block, or the answer's text. A call whose result is not
|
|
104
|
+
in the same answer keeps an empty output, which reads as a search that said nothing.
|
|
105
|
+
"""
|
|
106
|
+
calls: dict[str, ServerToolRun] = {}
|
|
107
|
+
order: list[str] = []
|
|
108
|
+
for block in blocks:
|
|
109
|
+
match block.get("type"):
|
|
110
|
+
case "server_tool_use":
|
|
111
|
+
call_id = block.get("id")
|
|
112
|
+
name = block.get("name")
|
|
113
|
+
if not isinstance(call_id, str) or not isinstance(name, str):
|
|
114
|
+
continue
|
|
115
|
+
call_input = block.get("input")
|
|
116
|
+
arguments = dict(call_input) if isinstance(call_input, Mapping) else {}
|
|
117
|
+
calls[call_id] = ServerToolRun(call_id, name, arguments)
|
|
118
|
+
order.append(call_id)
|
|
119
|
+
case "web_search_tool_result" | "server_tool_result" | "web_fetch_tool_result":
|
|
120
|
+
found = block.get("tool_use_id") or block.get("call_id")
|
|
121
|
+
if not isinstance(found, str) or found not in calls:
|
|
122
|
+
continue
|
|
123
|
+
error = block.get("error_code")
|
|
124
|
+
calls[found] = ServerToolRun(
|
|
125
|
+
found,
|
|
126
|
+
calls[found].name,
|
|
127
|
+
calls[found].arguments,
|
|
128
|
+
output=block,
|
|
129
|
+
error=str(error) if isinstance(error, str) and error else None,
|
|
130
|
+
)
|
|
131
|
+
return [calls[call_id] for call_id in order]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def answer_after_tools(blocks: Iterable[Mapping[str, Any]]) -> bool:
|
|
135
|
+
"""Return whether the answer's text came after its hosted tool, as the provider streamed them.
|
|
136
|
+
|
|
137
|
+
Anthropic runs a hosted search before it writes the answer, so the row belongs above it; a provider that writes
|
|
138
|
+
first and searches after (DeepSeek does, when it announces the search) has it below. The blocks record which.
|
|
139
|
+
"""
|
|
140
|
+
first_tool: int | None = None
|
|
141
|
+
for index, block in enumerate(blocks):
|
|
142
|
+
kind = block.get("type")
|
|
143
|
+
if first_tool is None and kind in _CALL_TYPES:
|
|
144
|
+
first_tool = index
|
|
145
|
+
elif kind == "text" and isinstance(block.get("text"), str) and block["text"].strip():
|
|
146
|
+
return first_tool is not None
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = [
|
|
151
|
+
"MAX_RESULTS",
|
|
152
|
+
"ServerToolRun",
|
|
153
|
+
"answer_after_tools",
|
|
154
|
+
"describe_output",
|
|
155
|
+
"describe_search",
|
|
156
|
+
"runs_in_blocks",
|
|
157
|
+
]
|
|
@@ -45,6 +45,9 @@ PROCESSING = "Processing"
|
|
|
45
45
|
#: reserved for it.
|
|
46
46
|
STOP_HINT = "^C/Esc to interrupt"
|
|
47
47
|
|
|
48
|
+
#: What the row of a question the reader cancelled says under its title.
|
|
49
|
+
DECLINED_NOTE = "You declined; the model was told and carries on."
|
|
50
|
+
|
|
48
51
|
# Matches both escape families a tool can smuggle into its output: CSI
|
|
49
52
|
# (``ESC [`` parameters and a final byte) and OSC (``ESC ]`` up to BEL or ST).
|
|
50
53
|
# Stripping them keeps a coloured compiler message from repainting the canvas.
|
|
@@ -15,6 +15,8 @@ from __future__ import annotations
|
|
|
15
15
|
import random
|
|
16
16
|
from collections.abc import Callable, MutableSequence, Sequence
|
|
17
17
|
|
|
18
|
+
from zettcode.tui import OPTION
|
|
19
|
+
|
|
18
20
|
#: What the reader may be told, in the words they will see.
|
|
19
21
|
TIPS: tuple[str, ...] = (
|
|
20
22
|
"Type while it works: your message can steer this request, or wait and follow it.",
|
|
@@ -35,6 +37,7 @@ TIPS: tuple[str, ...] = (
|
|
|
35
37
|
"Ctrl-R searches what you have sent before; Up recalls the last message.",
|
|
36
38
|
"Ctrl-V attaches an image from the clipboard, for a model that takes images.",
|
|
37
39
|
"Page Up scrolls the conversation, and Esc jumps back to the newest line.",
|
|
40
|
+
f"{OPTION.capitalize()}-\u2191/\u2193 (or Ctrl-\u2191/\u2193) jumps to the previous or next message you sent.",
|
|
38
41
|
"Drag in the conversation to copy text. Hold Shift or Alt to use your terminal's own selection.",
|
|
39
42
|
"Put project instructions in AGENTS.md and they are given to the model with every request.",
|
|
40
43
|
"A SKILL.md in ~/.zettcode/skills teaches ZettCode a job it can repeat.",
|
|
@@ -44,7 +47,7 @@ TIPS: tuple[str, ...] = (
|
|
|
44
47
|
"/self-compact lets the model decide for itself when to compact this session.",
|
|
45
48
|
"zettcode resume <id> opens a stored session; zettcode update installs the newest release.",
|
|
46
49
|
"zettcode doctor checks every configured model from a shell, with no session started.",
|
|
47
|
-
"zettcode --
|
|
50
|
+
"zettcode --full skips every shell question and the sandbox for that run; /permission can change it later.",
|
|
48
51
|
"Press Esc to stop a request; /continue picks it up again where it left off.",
|
|
49
52
|
"A plugin is a Python package: install it and ZettCode gains its commands, tools, themes or pets.",
|
|
50
53
|
"These tips can be turned off: tips = false under [transcript] in the configuration.",
|
|
@@ -128,6 +128,24 @@ class StreamMixin(TurnMixin):
|
|
|
128
128
|
# tool's business and this opens every one the same way.
|
|
129
129
|
self._add(self.renderers.open_tool(name, arguments, id=self._next_id, call_id=call_id, started_at=self.clock()))
|
|
130
130
|
|
|
131
|
+
def restate_tool(self, call_id: str, arguments: Mapping[str, object]) -> None:
|
|
132
|
+
"""Phrase a row again with arguments that arrived after it opened.
|
|
133
|
+
|
|
134
|
+
A hosted tool's input may stream in fragments after the call started, so the row opens knowing only the tool's
|
|
135
|
+
name. This is where the query it really ran appears, before the row is closed.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
call_id: Identifier the matching result will carry.
|
|
139
|
+
arguments: What the call turned out to be, which the chain phrases.
|
|
140
|
+
"""
|
|
141
|
+
entry = next(
|
|
142
|
+
(item for item in reversed(self.entries) if isinstance(item, ToolEntry) and item.call_id == call_id), None
|
|
143
|
+
)
|
|
144
|
+
if entry is None:
|
|
145
|
+
return
|
|
146
|
+
entry.title = self.renderers.describe(entry.tool, arguments).title
|
|
147
|
+
self._changed(entry)
|
|
148
|
+
|
|
131
149
|
def complete_tool(
|
|
132
150
|
self, call_id: str, output: str, *, status: EntryStatus = EntryStatus.COMPLETED, wait: bool = True
|
|
133
151
|
) -> None:
|
|
@@ -8,6 +8,7 @@ panel never reads a raw payload and the agent never invents one.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
@@ -19,6 +20,7 @@ __all__ = [
|
|
|
19
20
|
"AskUserQuestion",
|
|
20
21
|
"answer_payload",
|
|
21
22
|
"decline_payload",
|
|
23
|
+
"declined_answer",
|
|
22
24
|
"question_from",
|
|
23
25
|
]
|
|
24
26
|
|
|
@@ -94,3 +96,18 @@ def decline_payload(question: AskUserQuestion) -> dict[str, Any]:
|
|
|
94
96
|
"declined": True,
|
|
95
97
|
"reason": "the user cancelled the question",
|
|
96
98
|
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def declined_answer(text: str) -> bool:
|
|
102
|
+
"""Return whether a tool result is the one a cancelled question leaves behind.
|
|
103
|
+
|
|
104
|
+
The tool answers with the payload the shell sent, serialized as JSON. A cancelled question carries
|
|
105
|
+
``"declined": true`` and no answer; a row that has one is not a row that failed, it is a question nobody
|
|
106
|
+
needed to answer, and the transcript says so.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
result = json.loads(text)
|
|
110
|
+
except ValueError:
|
|
111
|
+
return False
|
|
112
|
+
payload = result.get("payload") if isinstance(result, dict) else None
|
|
113
|
+
return isinstance(payload, dict) and payload.get("declined") is True
|