zettcode 0.1.6__tar.gz → 0.1.7__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.6 → zettcode-0.1.7}/PKG-INFO +5 -1
- {zettcode-0.1.6 → zettcode-0.1.7}/pyproject.toml +7 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/__init__.py +1 -1
- zettcode-0.1.7/src/zettcode/app/agent/agent/base.py +54 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/context.py +2 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/prompt.py +26 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/sessions.py +6 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/turns.py +14 -4
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/entries.py +50 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/presenters.py +160 -8
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/projection.py +16 -2
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/rendering.py +36 -1
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/replay.py +7 -1
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/rows.py +13 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/turn.py +69 -3
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/mentions.py +114 -0
- zettcode-0.1.7/src/zettcode/app/agent/dialogue/relay.py +153 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/side.py +1 -1
- zettcode-0.1.7/src/zettcode/app/agent/errors.py +125 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/export/__init__.py +9 -2
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/mcp.py +24 -2
- zettcode-0.1.7/src/zettcode/app/agent/extensions/messaging.py +130 -0
- zettcode-0.1.7/src/zettcode/app/agent/extensions/messenger.py +183 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/runtime.py +49 -5
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/storage/__init__.py +8 -0
- zettcode-0.1.7/src/zettcode/app/agent/storage/locks.py +130 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/app.py +81 -10
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/boot.py +5 -2
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/commands.py +4 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/followups.py +8 -5
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/labels.py +40 -6
- zettcode-0.1.7/src/zettcode/app/ui/messaging.py +244 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/providers.py +9 -5
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/rows.py +1 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/sessions.py +89 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/shell.py +26 -1
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/composer.py +98 -2
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/transcript.py +3 -2
- zettcode-0.1.7/src/zettcode/broker/__init__.py +22 -0
- zettcode-0.1.7/src/zettcode/broker/__main__.py +10 -0
- zettcode-0.1.7/src/zettcode/broker/client.py +533 -0
- zettcode-0.1.7/src/zettcode/broker/files.py +131 -0
- zettcode-0.1.7/src/zettcode/broker/finder.py +190 -0
- zettcode-0.1.7/src/zettcode/broker/hub.py +275 -0
- zettcode-0.1.7/src/zettcode/broker/log.py +11 -0
- zettcode-0.1.7/src/zettcode/broker/models.py +77 -0
- zettcode-0.1.7/src/zettcode/broker/process.py +117 -0
- zettcode-0.1.7/src/zettcode/broker/protocol.py +282 -0
- zettcode-0.1.7/src/zettcode/broker/roster.py +176 -0
- zettcode-0.1.7/src/zettcode/broker/runner.py +131 -0
- zettcode-0.1.7/src/zettcode/broker/server.py +184 -0
- zettcode-0.1.7/src/zettcode/broker/text.py +81 -0
- zettcode-0.1.7/src/zettcode/broker/web.py +192 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/config.py +27 -2
- zettcode-0.1.7/src/zettcode/filelock.py +125 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/builtins.py +4 -4
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/state.py +3 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/providers.py +15 -8
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/theme.py +32 -17
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/layout/scroll.py +41 -37
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/__init__.py +2 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/text.py +43 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/theme_file.py +3 -2
- zettcode-0.1.6/src/zettcode/app/agent/agent/base.py +0 -29
- zettcode-0.1.6/src/zettcode/app/agent/errors.py +0 -39
- {zettcode-0.1.6 → zettcode-0.1.7}/.gitignore +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/LICENSE +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/README.md +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/README.zh-CN.md +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/_compat.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/commands.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/exports.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/interaction.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/agent/settings.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/changes.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/fold.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/stream.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/ask.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/title.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/script.js +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/style.css +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/template.html +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/capabilities.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/compaction.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/context.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/subagents.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/tool_output.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/usage.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/permission/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/permission/approval.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/permission/readonly.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/storage/metadata.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/storage/records.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/agent/storage/store.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/brand.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/commands.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/pipeline.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/registry.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/clipboard.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/config_form.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/demo.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/gc_policy.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/keys.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/notices.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/settings.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/updating.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/completer.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/context_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/follow_up.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/input_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/model_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/panel.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/root.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/steering.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/update_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/welcome.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/cli.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/collector.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/config_edit.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/paths.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/container.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/extension.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/loader.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/mixins.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/plugins/plugin.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/capabilities.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/clipboard.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/console.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/app.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/events.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/focus.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/geometry.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/host.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/keymap.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/scheduler.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/screen.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/core/widget.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/gallery.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/glyphs.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/input/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/input/bridge.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/input/decoder.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/input/events.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/input/keys.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/input/reader.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/layout/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/layout/box.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/layout/overlay.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/layout/solver.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/layout/spacing.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/canvas.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/code.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/color.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/renderer.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/rich_text.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/style.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/render/sweep.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/runner.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/terminal.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/testing/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/testing/harness.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/testing/snapshot.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/__init__.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/collapsible.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/completion.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/dialog.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/diff.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/list.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/list_page.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/markdown.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/progress.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/rich_text.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/status.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/table.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/tasks.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/text.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/textarea.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/src/zettcode/tui/widgets/toast.py +0 -0
- {zettcode-0.1.6 → zettcode-0.1.7}/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.7
|
|
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,13 +21,17 @@ 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: httpx<1,>=0.27
|
|
24
25
|
Requires-Dist: pydantic<3,>=2
|
|
25
26
|
Requires-Dist: pyperclip<2,>=1.9
|
|
26
27
|
Requires-Dist: setproctitle<2,>=1.3.7; os_name == 'posix'
|
|
28
|
+
Requires-Dist: starlette<2,>=0.48
|
|
27
29
|
Requires-Dist: tiktoken<1,>=0.12
|
|
28
30
|
Requires-Dist: tomli<3,>=2; python_version < '3.11'
|
|
29
31
|
Requires-Dist: truststore<1,>=0.10
|
|
32
|
+
Requires-Dist: uvicorn<1,>=0.35
|
|
30
33
|
Requires-Dist: wcwidth<1,>=0.2
|
|
34
|
+
Requires-Dist: websockets<17,>=15
|
|
31
35
|
Requires-Dist: zett-agent<0.2,>=0.1.15
|
|
32
36
|
Description-Content-Type: text/markdown
|
|
33
37
|
|
|
@@ -29,6 +29,13 @@ dependencies = [
|
|
|
29
29
|
"pydantic>=2,<3",
|
|
30
30
|
"pyperclip>=1.9,<2",
|
|
31
31
|
"setproctitle>=1.3.7,<2; os_name == 'posix'",
|
|
32
|
+
# The session broker is an ordinary web server: a Starlette application served by uvicorn. A session reaches it
|
|
33
|
+
# with httpx (to send) and websockets (to be reachable). The floors are the oldest versions the broker's tests
|
|
34
|
+
# were run against; all of them arrive through the runtime today and are declared so that is not luck.
|
|
35
|
+
"starlette>=0.48,<2",
|
|
36
|
+
"uvicorn>=0.35,<1",
|
|
37
|
+
"httpx>=0.27,<1",
|
|
38
|
+
"websockets>=15,<17",
|
|
32
39
|
# The counter behind ``/context`` measures a request with it, and imports it
|
|
33
40
|
# itself: the floor matches the runtime's, so neither resolver has to fight.
|
|
34
41
|
"tiktoken>=0.12,<1",
|
|
@@ -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.7"
|
|
11
11
|
|
|
12
12
|
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
13
13
|
_EXPORTS = {
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
from ..storage import SessionLocked
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentBase:
|
|
17
|
+
"""The runtime, the workspace it works in, and the session the next turn belongs to."""
|
|
18
|
+
|
|
19
|
+
#: The runtime this agent speaks to; the one thing the application agent holds.
|
|
20
|
+
runtime: ZettCodeRuntime
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def workspace(self) -> Path:
|
|
24
|
+
"""Return the workspace displayed in the header."""
|
|
25
|
+
return self.runtime.config.workspace
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def session_id(self) -> str:
|
|
29
|
+
"""Return the session used for the next turn."""
|
|
30
|
+
return self.runtime.session_id
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def session_locked(self) -> bool:
|
|
34
|
+
"""Return whether another process has the active session open, so that this one may only read it.
|
|
35
|
+
|
|
36
|
+
Every session is locked by the process that has it open (see :mod:`..storage.locks`). A session this
|
|
37
|
+
process started is always its own; one it resumed may be somebody else's, and then it is shown and
|
|
38
|
+
nothing is written to it: no turn, no title, no compaction.
|
|
39
|
+
"""
|
|
40
|
+
return self.runtime.session_locked
|
|
41
|
+
|
|
42
|
+
def retake_session(self) -> bool:
|
|
43
|
+
"""Try again to lock the active session; return whether this process holds it now."""
|
|
44
|
+
return self.runtime.retake_session()
|
|
45
|
+
|
|
46
|
+
def require_session(self) -> None:
|
|
47
|
+
"""Refuse to go on when the active session is another process's.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
SessionLocked: The lock is not held. A ``ValueError``, which the shell shows as a line in the
|
|
51
|
+
conversation.
|
|
52
|
+
"""
|
|
53
|
+
if self.session_locked:
|
|
54
|
+
raise SessionLocked()
|
|
@@ -80,9 +80,11 @@ class ContextMixin(AgentBase):
|
|
|
80
80
|
worth replacing.
|
|
81
81
|
|
|
82
82
|
Raises:
|
|
83
|
+
SessionLocked: Another process has this session open, and a checkpoint is a write.
|
|
83
84
|
ValueError: When the newest checkpoint already covers the branch —
|
|
84
85
|
a second pass would summarize nothing but the checkpoint.
|
|
85
86
|
"""
|
|
87
|
+
self.require_session()
|
|
86
88
|
if self._compacted_at_the_head():
|
|
87
89
|
raise ValueError("Already compacted; send a message before compacting again")
|
|
88
90
|
await self.runtime.start()
|
|
@@ -23,6 +23,9 @@ class MentionPart:
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
token: str
|
|
26
|
+
#: The words the model reads in the token's place, when it is not to see the token itself. ``None``
|
|
27
|
+
#: leaves the token as typed; the typed text is what the transcript and the store keep either way.
|
|
28
|
+
says: str | None = None
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
#: One piece of a user turn, in the order it was written: a run of text, or the
|
|
@@ -39,6 +42,29 @@ def plain_text_part(part: PromptPart) -> str:
|
|
|
39
42
|
return ""
|
|
40
43
|
|
|
41
44
|
|
|
45
|
+
def model_text_part(part: PromptPart) -> str:
|
|
46
|
+
"""Return the text one part is to the model: a reference that was resolved reads as what it says."""
|
|
47
|
+
if isinstance(part, MentionPart) and part.says is not None:
|
|
48
|
+
return part.says
|
|
49
|
+
return plain_text_part(part)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def mention_parts(parts: Sequence[PromptPart], mentions: MentionRegistry) -> tuple[PromptPart, ...]:
|
|
53
|
+
"""Return a turn with each reference that has words of its own carrying them.
|
|
54
|
+
|
|
55
|
+
``@work`` typed for a session that is running is not a word the model knows, so it is put in the
|
|
56
|
+
model's reading as ``session "work"`` and :func:`mention_hint` explains it. Anything else — a skill, a
|
|
57
|
+
file, a name nobody has — keeps the token as it was typed.
|
|
58
|
+
"""
|
|
59
|
+
if not mentions.providers:
|
|
60
|
+
return tuple(parts)
|
|
61
|
+
resolved: list[PromptPart] = []
|
|
62
|
+
for part in parts:
|
|
63
|
+
says = mentions.rename_token(part.token) if isinstance(part, MentionPart) else None
|
|
64
|
+
resolved.append(MentionPart(part.token, says) if says is not None and isinstance(part, MentionPart) else part)
|
|
65
|
+
return tuple(resolved)
|
|
66
|
+
|
|
67
|
+
|
|
42
68
|
def plain_text(parts: Sequence[PromptPart]) -> str:
|
|
43
69
|
"""Return a turn as the reader typed it, with its ``@`` tokens back in place."""
|
|
44
70
|
return "".join(plain_text_part(part) for part in parts)
|
|
@@ -180,7 +180,11 @@ class SessionsMixin(ContextMixin):
|
|
|
180
180
|
Args:
|
|
181
181
|
title: Name to store; surrounding space is trimmed, and a blank or
|
|
182
182
|
over-long name is rejected by the store.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
SessionLocked: Another process has this session open.
|
|
183
186
|
"""
|
|
187
|
+
self.require_session()
|
|
184
188
|
await self.runtime.persistence.set_title(self.session_id, title)
|
|
185
189
|
return title.strip()
|
|
186
190
|
|
|
@@ -199,6 +203,8 @@ class SessionsMixin(ContextMixin):
|
|
|
199
203
|
"""
|
|
200
204
|
if self.runtime.persistence.session_title(session_id) is not None:
|
|
201
205
|
return None
|
|
206
|
+
if session_id == self.session_id and self.session_locked:
|
|
207
|
+
return None # a nicety, and not one that is worth writing into a session that is not ours
|
|
202
208
|
session = self.runtime.persistence.read(session_id)
|
|
203
209
|
branch = session.branch()
|
|
204
210
|
question = next(
|
|
@@ -12,9 +12,10 @@ from zett_agent.extensions.external import ExternalEvent
|
|
|
12
12
|
from zett_agent.extensions.steering import STEERING_MESSAGE_EVENT_NAME
|
|
13
13
|
from zett_agent.messages import ImageBytesSource, ImageContent, TextContent, UserMessage
|
|
14
14
|
|
|
15
|
+
from ..dialogue.relay import Relayed
|
|
15
16
|
from ..dialogue.side import SIDE, question
|
|
16
17
|
from .base import AgentBase
|
|
17
|
-
from .prompt import MentionPart, PromptPart, carries_image,
|
|
18
|
+
from .prompt import MentionPart, PromptPart, carries_image, model_text_part, plain_text
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class TurnsMixin(AgentBase):
|
|
@@ -30,6 +31,7 @@ class TurnsMixin(AgentBase):
|
|
|
30
31
|
*,
|
|
31
32
|
hint: str | None = None,
|
|
32
33
|
side: bool = False,
|
|
34
|
+
relayed: Relayed | None = None,
|
|
33
35
|
) -> AsyncIterator[AgentEvent]:
|
|
34
36
|
"""Run one turn with the selected session, model, and reasoning effort.
|
|
35
37
|
|
|
@@ -43,14 +45,21 @@ class TurnsMixin(AgentBase):
|
|
|
43
45
|
what was typed and a restored session shows it.
|
|
44
46
|
side: Ask without letting the exchange join the conversation; see
|
|
45
47
|
:mod:`zettcode.app.agent.side`.
|
|
48
|
+
relayed: The turn is a message another session sent. It is what the model reads
|
|
49
|
+
(with where it came from), and ``parts`` and ``hint`` are not used.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
SessionLocked: Another process has the session open. Checked before anything else, and a side
|
|
53
|
+
question is no exception: the store records it even though it never replays it.
|
|
46
54
|
"""
|
|
55
|
+
self.require_session()
|
|
47
56
|
await self.runtime.start()
|
|
48
57
|
client = self.runtime.started
|
|
49
58
|
used = self.runtime.active_model.shown_name
|
|
50
59
|
session_id = self.session_id
|
|
51
60
|
try:
|
|
52
61
|
async for event in client.stream(
|
|
53
|
-
self.request(parts, hint=hint, side=side),
|
|
62
|
+
self.request(parts, hint=hint, side=side) if relayed is None else relayed.message(),
|
|
54
63
|
config=AgentRunConfig(session_id=session_id),
|
|
55
64
|
model=self.runtime.provider,
|
|
56
65
|
reasoning_effort=self.runtime.effort,
|
|
@@ -105,11 +114,12 @@ class TurnsMixin(AgentBase):
|
|
|
105
114
|
text = plain_text(parts)
|
|
106
115
|
if hint is None:
|
|
107
116
|
return text
|
|
108
|
-
|
|
117
|
+
said = "".join(model_text_part(part) for part in parts)
|
|
118
|
+
return UserMessage(content=f"{said}\n\n{hint}", attributes={"prompt": text})
|
|
109
119
|
content: list[TextContent | ImageContent] = []
|
|
110
120
|
for part in parts:
|
|
111
121
|
if isinstance(part, (str, MentionPart)):
|
|
112
|
-
text =
|
|
122
|
+
text = model_text_part(part)
|
|
113
123
|
if text:
|
|
114
124
|
content.append(TextContent(text))
|
|
115
125
|
continue
|
|
@@ -38,6 +38,7 @@ class EntryKind(StrEnum):
|
|
|
38
38
|
THINKING: Reasoning, or a compaction's summary, with its own clock.
|
|
39
39
|
TOOL: One tool call and its bounded result.
|
|
40
40
|
CHANGES: The files a request changed, said once when it ends.
|
|
41
|
+
RELAY: A message that crossed between two sessions, in either direction.
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
44
|
WELCOME = "welcome"
|
|
@@ -50,6 +51,7 @@ class EntryKind(StrEnum):
|
|
|
50
51
|
THINKING = "thinking"
|
|
51
52
|
TOOL = "tool"
|
|
52
53
|
CHANGES = "changes"
|
|
54
|
+
RELAY = "relay"
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
class EntryStatus(StrEnum):
|
|
@@ -181,6 +183,13 @@ class WaitEntry(TimedEntry):
|
|
|
181
183
|
kind: EntryKind = field(default=EntryKind.WAIT, init=False)
|
|
182
184
|
title: str = "Processing"
|
|
183
185
|
started_at: float
|
|
186
|
+
#: The clock reading of the last thing the agent said (a delta, a tool, a step), which is when the model
|
|
187
|
+
#: was last heard from; ``None`` until it has said anything.
|
|
188
|
+
heard_at: float | None = None
|
|
189
|
+
#: Seconds the model has said nothing while nothing else was running, brought up with the timers. Zero
|
|
190
|
+
#: while a tool, a reasoning row or a compaction is the thing being waited for: a ``pytest`` that takes two
|
|
191
|
+
#: minutes is not the model going quiet.
|
|
192
|
+
quiet: float = 0.0
|
|
184
193
|
#: The line the request ended on, written when the row is settled.
|
|
185
194
|
text: str = ""
|
|
186
195
|
|
|
@@ -289,6 +298,24 @@ class DeleteEntry(ToolEntry):
|
|
|
289
298
|
path: str = ""
|
|
290
299
|
|
|
291
300
|
|
|
301
|
+
@dataclass(slots=True, kw_only=True)
|
|
302
|
+
class MessageEntry(ToolEntry):
|
|
303
|
+
"""A message the agent sent to another session: who it was for, and every word of it.
|
|
304
|
+
|
|
305
|
+
The tool's own answer is one line (``Delivered to work …``); what the reader wants to see is what
|
|
306
|
+
was said, which the call's arguments hold and a one-line title cannot, so the entry keeps it and the
|
|
307
|
+
row shows it — a few lines folded, the whole message expanded.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
to: str = ""
|
|
311
|
+
message: str = ""
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def foldable(self) -> bool:
|
|
315
|
+
"""Return whether there is more to see once the call is over: what was said, or how it ended."""
|
|
316
|
+
return not self.running and bool(self.message or self.text)
|
|
317
|
+
|
|
318
|
+
|
|
292
319
|
@dataclass(frozen=True, slots=True)
|
|
293
320
|
class FileSummary:
|
|
294
321
|
"""What one request did to one file, in total.
|
|
@@ -316,3 +343,26 @@ class ChangesEntry(Entry):
|
|
|
316
343
|
|
|
317
344
|
kind: EntryKind = field(default=EntryKind.CHANGES, init=False)
|
|
318
345
|
files: tuple[FileSummary, ...] = ()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@dataclass(slots=True, kw_only=True)
|
|
349
|
+
class RelayEntry(Entry):
|
|
350
|
+
"""A message between two sessions: one this session received, or one its reader sent.
|
|
351
|
+
|
|
352
|
+
Attributes:
|
|
353
|
+
peer: The other session's handle.
|
|
354
|
+
text: The message.
|
|
355
|
+
outgoing: This session sent it; otherwise it arrived.
|
|
356
|
+
by_agent: The other session's agent wrote it (received), or this session's reader chose to
|
|
357
|
+
send it with ``@`` (sent, so ``False``).
|
|
358
|
+
external: It came from a program that is not a ZettCode session, whose name nobody has checked.
|
|
359
|
+
note: What became of a sent message, such as ``queued``; empty for a received one.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
kind: EntryKind = field(default=EntryKind.RELAY, init=False)
|
|
363
|
+
peer: str
|
|
364
|
+
text: str
|
|
365
|
+
outgoing: bool = False
|
|
366
|
+
by_agent: bool = True
|
|
367
|
+
external: bool = False
|
|
368
|
+
note: str = ""
|
|
@@ -37,7 +37,17 @@ from ....tui import (
|
|
|
37
37
|
TextLine,
|
|
38
38
|
Theme,
|
|
39
39
|
)
|
|
40
|
-
from ....tui.render import
|
|
40
|
+
from ....tui.render import (
|
|
41
|
+
SweepSpan,
|
|
42
|
+
blend,
|
|
43
|
+
display_width,
|
|
44
|
+
highlight,
|
|
45
|
+
inset_line,
|
|
46
|
+
layout_rich_lines,
|
|
47
|
+
sweep_spans,
|
|
48
|
+
truncate,
|
|
49
|
+
wrap_words,
|
|
50
|
+
)
|
|
41
51
|
from ....tui.widgets.markdown import MarkdownSource
|
|
42
52
|
from ...brand import LOGO_LINES
|
|
43
53
|
from .entries import (
|
|
@@ -49,8 +59,10 @@ from .entries import (
|
|
|
49
59
|
FileChangeEntry,
|
|
50
60
|
FileSummary,
|
|
51
61
|
MarkdownEntry,
|
|
62
|
+
MessageEntry,
|
|
52
63
|
NoticeLevel,
|
|
53
64
|
ReadEntry,
|
|
65
|
+
RelayEntry,
|
|
54
66
|
TextEntry,
|
|
55
67
|
ThinkingEntry,
|
|
56
68
|
ToolEntry,
|
|
@@ -69,6 +81,7 @@ from .rows import (
|
|
|
69
81
|
duration_text,
|
|
70
82
|
elapsed_text,
|
|
71
83
|
moving,
|
|
84
|
+
quiet_text,
|
|
72
85
|
sweep_column,
|
|
73
86
|
terminal_safe,
|
|
74
87
|
)
|
|
@@ -97,6 +110,8 @@ TOOL_ROLES = {
|
|
|
97
110
|
"todo_write": "plan",
|
|
98
111
|
"read_skill": "read",
|
|
99
112
|
"task": "subagent",
|
|
113
|
+
"send_message": "message",
|
|
114
|
+
"list_sessions": "message",
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
|
|
@@ -184,11 +199,27 @@ class Presenter(Generic[E], ABC):
|
|
|
184
199
|
return entry.block
|
|
185
200
|
|
|
186
201
|
@staticmethod
|
|
187
|
-
def text_lines(text: str, width: int, style: Style, *, indent: int = 0) -> list[TextLine]:
|
|
188
|
-
"""Wrap text with the same layout used by RichText and Markdown.
|
|
189
|
-
|
|
202
|
+
def text_lines(text: str, width: int, style: Style, *, indent: int = 0, words: bool = False) -> list[TextLine]:
|
|
203
|
+
"""Wrap text with the same layout used by RichText and Markdown.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
text: What to wrap; each newline starts a new row.
|
|
207
|
+
width: Columns available.
|
|
208
|
+
style: The ink the text is drawn in.
|
|
209
|
+
indent: Columns left empty at the start of every row.
|
|
210
|
+
words: Break between words and not mid-word. For a sentence a reader is meant to read, such as an
|
|
211
|
+
error naming a command to try; left off, a row breaks at the column, as code and output must.
|
|
212
|
+
"""
|
|
190
213
|
margin = min(indent, max(0, width - 1))
|
|
191
|
-
|
|
214
|
+
room = max(1, width - margin)
|
|
215
|
+
if words:
|
|
216
|
+
rows = [piece for line in text.split("\n") for piece in wrap_words(line, room)]
|
|
217
|
+
# A character wider than the whole row (a wide glyph in a column or two) cannot be placed in it;
|
|
218
|
+
# the layout that wraps by column cuts such a row to the budget, and so does this.
|
|
219
|
+
fitted = layout_rich_lines(tuple(TextLine((Span(row, style),)) for row in rows), room, wrap=False)
|
|
220
|
+
return [inset_line(row, margin) for row in fitted]
|
|
221
|
+
source = tuple(TextLine((Span(line, style),)) for line in text.split("\n"))
|
|
222
|
+
return [inset_line(row, margin) for row in layout_rich_lines(source, room)]
|
|
192
223
|
|
|
193
224
|
|
|
194
225
|
class WelcomePresenter(Presenter[TextEntry]):
|
|
@@ -236,7 +267,7 @@ class NoticePresenter(Presenter[TextEntry]):
|
|
|
236
267
|
def lines(self, entry: TextEntry, width: int, theme: Theme, frame: int) -> list[TextLine]:
|
|
237
268
|
"""Leave a blank separator and let the transcript gutter add the inset."""
|
|
238
269
|
colour = theme.error if entry.level == NoticeLevel.ERROR else theme.muted
|
|
239
|
-
return [TextLine(), *self.text_lines(entry.text, width, Style(foreground=colour))]
|
|
270
|
+
return [TextLine(), *self.text_lines(entry.text, width, Style(foreground=colour), words=True)]
|
|
240
271
|
|
|
241
272
|
|
|
242
273
|
class AnnouncementPresenter(Presenter[TextEntry]):
|
|
@@ -309,6 +340,79 @@ class UserPresenter(Presenter[TextEntry]):
|
|
|
309
340
|
return lines
|
|
310
341
|
|
|
311
342
|
|
|
343
|
+
def relay_detail(theme: Theme) -> str:
|
|
344
|
+
"""Return the colour of the quiet words on a message between sessions (``delivered``, ``queued``).
|
|
345
|
+
|
|
346
|
+
The surface is a little lighter than the reader's own, and the usual quiet text would be too close to
|
|
347
|
+
it, so the words are taken halfway to the body colour: still quieter than the message, and readable.
|
|
348
|
+
"""
|
|
349
|
+
return blend(theme.subtle, theme.text, 0.5)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class RelayPresenter(Presenter[RelayEntry]):
|
|
353
|
+
"""Draw a message between two sessions the way the reader's own message is drawn, on a surface of its own.
|
|
354
|
+
|
|
355
|
+
Same arrow, same text column, same filled rows as what the reader types — it is part of the
|
|
356
|
+
conversation — but on a different background, with a line above the text that says whose it is::
|
|
357
|
+
|
|
358
|
+
← agent of session fix-the-parser
|
|
359
|
+
› The tokenizer change is merged; can you rerun the parser tests?
|
|
360
|
+
|
|
361
|
+
→ to session fix-the-parser · queued
|
|
362
|
+
› yes, on it
|
|
363
|
+
|
|
364
|
+
The header is the one place colour says who spoke: the message colour (mauve) for another session's
|
|
365
|
+
*agent* (another agent, as a delegated child is), the warning yellow for another session's reader,
|
|
366
|
+
and the accent for this reader's own message to it.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
kind = EntryKind.RELAY
|
|
370
|
+
gutter = True
|
|
371
|
+
|
|
372
|
+
def lines(self, entry: RelayEntry, width: int, theme: Theme, frame: int) -> list[TextLine]:
|
|
373
|
+
"""Return the header and the wrapped message on the surface, every row filled to the edge."""
|
|
374
|
+
if entry.outgoing:
|
|
375
|
+
tone = theme.accent
|
|
376
|
+
elif entry.external:
|
|
377
|
+
tone = theme.warning
|
|
378
|
+
elif entry.by_agent:
|
|
379
|
+
tone = theme.tools.message
|
|
380
|
+
else:
|
|
381
|
+
tone = theme.warning
|
|
382
|
+
style = Style(foreground=theme.text, background=theme.surface_peer)
|
|
383
|
+
margin = min(CONTENT_INDENT, max(0, width - 1))
|
|
384
|
+
pad = " " * display_width(PROMPT)
|
|
385
|
+
peer = terminal_safe(entry.peer).replace("\n", " ")
|
|
386
|
+
if entry.outgoing:
|
|
387
|
+
label = f"\u2192 to session {peer}"
|
|
388
|
+
detail = f" {SEPARATOR} {entry.note}" if entry.note else ""
|
|
389
|
+
else:
|
|
390
|
+
label = (
|
|
391
|
+
f"\u2190 external source {peer}"
|
|
392
|
+
if entry.external
|
|
393
|
+
else f"\u2190 {'agent' if entry.by_agent else 'user'} of session {peer}"
|
|
394
|
+
)
|
|
395
|
+
detail = ""
|
|
396
|
+
|
|
397
|
+
def row(*spans: Span) -> TextLine:
|
|
398
|
+
used = sum(display_width(span.text) for span in spans)
|
|
399
|
+
return TextLine((*spans, Span(" " * max(0, width - used), style)))
|
|
400
|
+
|
|
401
|
+
head = truncate(f"{pad} {label}", width)
|
|
402
|
+
tail = truncate(detail, max(0, width - display_width(head)))
|
|
403
|
+
header = row(
|
|
404
|
+
Span(head, Style(foreground=tone, background=theme.surface_peer)),
|
|
405
|
+
Span(tail, Style(foreground=relay_detail(theme), background=theme.surface_peer)),
|
|
406
|
+
)
|
|
407
|
+
blank = row()
|
|
408
|
+
lines = [TextLine(), blank, header]
|
|
409
|
+
for index, text in enumerate(self.text_lines(terminal_safe(entry.text), max(1, width - margin), style)):
|
|
410
|
+
prefix = f"{PROMPT} " if index == 0 else f"{pad} "
|
|
411
|
+
lines.append(row(Span(truncate(f"{prefix}{text.text}", width), style)))
|
|
412
|
+
lines.append(blank)
|
|
413
|
+
return lines
|
|
414
|
+
|
|
415
|
+
|
|
312
416
|
class WaitPresenter(Presenter[WaitEntry]):
|
|
313
417
|
"""Draw the pinned wait row: a sweep while it runs, a line once it has ended.
|
|
314
418
|
|
|
@@ -349,8 +453,16 @@ class WaitPresenter(Presenter[WaitEntry]):
|
|
|
349
453
|
]
|
|
350
454
|
spans = list(_running_label(entry.title, theme, frame))
|
|
351
455
|
if entry.duration is not None:
|
|
352
|
-
|
|
353
|
-
|
|
456
|
+
quiet = quiet_text(entry.quiet)
|
|
457
|
+
said = f"{elapsed_text(entry.duration)} {SEPARATOR} "
|
|
458
|
+
if quiet is not None:
|
|
459
|
+
# In the warning ink, apart from the quiet aside around it: this is the one thing in the row
|
|
460
|
+
# that says something may be wrong, and a reader who has been waiting is looking for it.
|
|
461
|
+
spans.append(Span(f" ({said}", Style(foreground=theme.muted)))
|
|
462
|
+
spans.append(Span(quiet, Style(foreground=theme.warning)))
|
|
463
|
+
spans.append(Span(f" {SEPARATOR} {STOP_HINT})", Style(foreground=theme.muted)))
|
|
464
|
+
else:
|
|
465
|
+
spans.append(Span(f" ({said}{STOP_HINT})", Style(foreground=theme.muted)))
|
|
354
466
|
return [TextLine(), layout_rich_lines((TextLine(tuple(spans)),), width, wrap=False)[0]]
|
|
355
467
|
|
|
356
468
|
|
|
@@ -465,6 +577,44 @@ class ToolPresenter(Presenter[T]):
|
|
|
465
577
|
return lines
|
|
466
578
|
|
|
467
579
|
|
|
580
|
+
class SessionMessagePresenter(ToolPresenter[MessageEntry]):
|
|
581
|
+
"""A message the agent sent to another session: what it said, then how it ended.
|
|
582
|
+
|
|
583
|
+
Folded, the row shows the first lines of the message and the one-line outcome; expanded, every
|
|
584
|
+
line of the message. The message comes first because it is the substance of the call; the outcome
|
|
585
|
+
(``Delivered to work …``, or why it was not) is a line of its own underneath, in the colour of the
|
|
586
|
+
result — muted when it went, the error colour when it did not.
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
entry_type = MessageEntry
|
|
590
|
+
|
|
591
|
+
def body_lines(self, entry: MessageEntry, width: int, theme: Theme, muted: Style) -> list[TextLine]:
|
|
592
|
+
"""Return the message, bounded as any tool output is, and the outcome under it."""
|
|
593
|
+
if entry.running:
|
|
594
|
+
return []
|
|
595
|
+
inner = max(1, width - CONTENT_INDENT * 2)
|
|
596
|
+
limit = TOOL_EXPANDED_ROWS if entry.expanded else TOOL_PREVIEW_ROWS
|
|
597
|
+
detail = Style(foreground=theme.subtle)
|
|
598
|
+
gutter = Style(foreground=theme.muted)
|
|
599
|
+
lines: list[TextLine] = []
|
|
600
|
+
rows, omitted = bounded_rows(entry.message, inner, limit) if entry.message else ([], 0)
|
|
601
|
+
for index, row in enumerate(rows):
|
|
602
|
+
lead = f"{' ' * CONTENT_INDENT}\u2514 " if index == 0 else " " * (CONTENT_INDENT * 2)
|
|
603
|
+
lines.append(TextLine((Span(lead, gutter), Span(row, detail))))
|
|
604
|
+
if omitted:
|
|
605
|
+
lines.append(TextLine((Span(f"{' ' * (CONTENT_INDENT * 2)}{ELLIPSIS} {omitted} more rows", muted),)))
|
|
606
|
+
if entry.text:
|
|
607
|
+
ended = Style(foreground=theme.error if entry.status is EntryStatus.FAILED else theme.muted)
|
|
608
|
+
# An arrow, so the answer is not read as the last line of the message above it.
|
|
609
|
+
outcome, more = bounded_rows(entry.text, max(1, inner - 2), 3 if entry.expanded else 1)
|
|
610
|
+
for index, row in enumerate(outcome):
|
|
611
|
+
lead = f"{' ' * CONTENT_INDENT}\u2192 " if index == 0 else " " * (CONTENT_INDENT * 2)
|
|
612
|
+
lines.append(TextLine((Span(f"{lead}{row}", ended),)))
|
|
613
|
+
if more and not entry.expanded:
|
|
614
|
+
lines[-1] = TextLine((Span(f"{lines[-1].text} {ELLIPSIS}", ended),))
|
|
615
|
+
return lines
|
|
616
|
+
|
|
617
|
+
|
|
468
618
|
class ReadPresenter(ToolPresenter[ReadEntry]):
|
|
469
619
|
"""A file that was read: the body is highlighted as the language the file is in."""
|
|
470
620
|
|
|
@@ -704,9 +854,11 @@ DEFAULT_PRESENTERS = Presenters(
|
|
|
704
854
|
ThinkingPresenter(),
|
|
705
855
|
ToolPresenter(),
|
|
706
856
|
ChangesPresenter(),
|
|
857
|
+
RelayPresenter(),
|
|
707
858
|
ReadPresenter(),
|
|
708
859
|
WritePresenter(),
|
|
709
860
|
EditPresenter(),
|
|
861
|
+
SessionMessagePresenter(),
|
|
710
862
|
)
|
|
711
863
|
)
|
|
712
864
|
|
|
@@ -11,6 +11,7 @@ from zett_agent.extensions.shell_approval import SHELL_APPROVAL_EVENT_NAME
|
|
|
11
11
|
from zett_agent.messages import ToolMessage
|
|
12
12
|
|
|
13
13
|
from ..dialogue.ask import AskUserQuestion, question_from
|
|
14
|
+
from ..dialogue.relay import Relayed
|
|
14
15
|
from ..extensions.mcp_notice import MCP_NOTICE_EVENT_NAME, notice_from
|
|
15
16
|
from ..extensions.usage import USAGE_EVENT_NAME
|
|
16
17
|
from .entries import EntryStatus
|
|
@@ -81,14 +82,27 @@ class TranscriptProjector(AgentEventDispatcher):
|
|
|
81
82
|
self.on_steering_started = on_steering_started
|
|
82
83
|
self.on_steering_interrupted = on_steering_interrupted
|
|
83
84
|
|
|
84
|
-
def begin_turn(self, prompt: str, *, side: bool = False) -> None:
|
|
85
|
+
def begin_turn(self, prompt: str, *, side: bool = False, relayed: Relayed | None = None) -> None:
|
|
85
86
|
"""Echo the prompt into the transcript before the run starts.
|
|
86
87
|
|
|
87
88
|
Args:
|
|
88
89
|
prompt: What the reader asked.
|
|
89
90
|
side: The question is a side one, which the transcript shows as such.
|
|
91
|
+
relayed: The turn answers a message another session sent, which the transcript shows as that.
|
|
90
92
|
"""
|
|
91
|
-
|
|
93
|
+
if relayed is None:
|
|
94
|
+
self.transcript.begin_turn(prompt, side=side)
|
|
95
|
+
else:
|
|
96
|
+
self.transcript.begin_turn(prompt, side=side, relayed=relayed)
|
|
97
|
+
|
|
98
|
+
async def dispatch(self, event: AgentEvent) -> None:
|
|
99
|
+
"""Note that the agent spoke, then hand the event to the callback that draws it.
|
|
100
|
+
|
|
101
|
+
Every event ends a silence, whichever it is: the waiting row counts how long it has been since the model
|
|
102
|
+
last said anything, and has to be told each time it does.
|
|
103
|
+
"""
|
|
104
|
+
self.transcript.heard()
|
|
105
|
+
await super().dispatch(event)
|
|
92
106
|
|
|
93
107
|
async def on_compaction_started_event(self, event: AgentEvent) -> None:
|
|
94
108
|
"""Open the compaction row; the summary streams into it as it arrives."""
|
|
@@ -32,7 +32,8 @@ from pathlib import Path
|
|
|
32
32
|
|
|
33
33
|
from ....tui import ELLIPSIS, SEPARATOR
|
|
34
34
|
from ....tui.render import GENERIC, language_for
|
|
35
|
-
from .entries import DeleteEntry, EditEntry, ReadEntry, ToolEntry, WriteEntry
|
|
35
|
+
from .entries import DeleteEntry, EditEntry, MessageEntry, ReadEntry, ToolEntry, WriteEntry
|
|
36
|
+
from .rows import limit_output
|
|
36
37
|
|
|
37
38
|
#: Output kinds the chain shapes; a link claims one or more of them.
|
|
38
39
|
TOOL = "tool"
|
|
@@ -539,6 +540,39 @@ class TaskTool(ToolHandler):
|
|
|
539
540
|
return output
|
|
540
541
|
|
|
541
542
|
|
|
543
|
+
_UUID = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class MessageTool(ToolHandler):
|
|
547
|
+
"""``send_message``: a message to another session, kept whole so the row can show it."""
|
|
548
|
+
|
|
549
|
+
names = frozenset({"send_message"})
|
|
550
|
+
|
|
551
|
+
def describe(self, name: str, arguments: Mapping[str, object]) -> ToolRow:
|
|
552
|
+
"""Say who it is for; an id is shortened to its end, which is how a session is named without a title."""
|
|
553
|
+
return ToolRow(f"Message to {self._recipient(argument(arguments, 'to', '(unknown)'))}")
|
|
554
|
+
|
|
555
|
+
@staticmethod
|
|
556
|
+
def _recipient(to: str) -> str:
|
|
557
|
+
"""Return the recipient as a reader can use it: a name as it is, an id by its last characters."""
|
|
558
|
+
return f"session {to[-6:]}" if _UUID.fullmatch(to) else to
|
|
559
|
+
|
|
560
|
+
def open_tool(
|
|
561
|
+
self, name: str, arguments: Mapping[str, object], *, id: int, call_id: str, started_at: float
|
|
562
|
+
) -> ToolEntry:
|
|
563
|
+
"""A message row, which holds the text that was sent."""
|
|
564
|
+
message = arguments.get("message")
|
|
565
|
+
return MessageEntry(
|
|
566
|
+
id=id,
|
|
567
|
+
call_id=call_id,
|
|
568
|
+
tool=name,
|
|
569
|
+
title=self.describe(name, arguments).title,
|
|
570
|
+
to=argument(arguments, "to"),
|
|
571
|
+
message=limit_output(message) if isinstance(message, str) else "",
|
|
572
|
+
started_at=started_at,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
542
576
|
class FallbackTool(ToolHandler):
|
|
543
577
|
"""Anything no other link claims: its name and flat arguments, never JSON."""
|
|
544
578
|
|
|
@@ -610,6 +644,7 @@ DEFAULT_RENDERERS = Renderers(
|
|
|
610
644
|
TodoTool(),
|
|
611
645
|
SkillTool(),
|
|
612
646
|
TaskTool(),
|
|
647
|
+
MessageTool(),
|
|
613
648
|
FallbackTool(),
|
|
614
649
|
ReasoningHandler(),
|
|
615
650
|
AnswerHandler(),
|