zettcode 0.1.5__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.
Files changed (200) hide show
  1. {zettcode-0.1.5 → zettcode-0.1.7}/PKG-INFO +5 -1
  2. {zettcode-0.1.5 → zettcode-0.1.7}/pyproject.toml +7 -0
  3. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/__init__.py +1 -1
  4. zettcode-0.1.7/src/zettcode/app/agent/agent/base.py +54 -0
  5. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/commands.py +6 -0
  6. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/context.py +2 -0
  7. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/prompt.py +26 -0
  8. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/sessions.py +6 -0
  9. zettcode-0.1.7/src/zettcode/app/agent/agent/settings.py +164 -0
  10. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/turns.py +14 -4
  11. zettcode-0.1.7/src/zettcode/app/agent/conversation/changes.py +64 -0
  12. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/entries.py +156 -6
  13. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/presenters.py +383 -42
  14. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/projection.py +16 -2
  15. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/rendering.py +225 -10
  16. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/replay.py +7 -1
  17. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/rows.py +19 -5
  18. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/fold.py +2 -2
  19. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/stream.py +3 -13
  20. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/turn.py +110 -8
  21. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/mentions.py +114 -0
  22. zettcode-0.1.7/src/zettcode/app/agent/dialogue/relay.py +153 -0
  23. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/side.py +1 -1
  24. zettcode-0.1.7/src/zettcode/app/agent/errors.py +125 -0
  25. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/__init__.py +9 -2
  26. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/mcp.py +72 -1
  27. zettcode-0.1.7/src/zettcode/app/agent/extensions/messaging.py +130 -0
  28. zettcode-0.1.7/src/zettcode/app/agent/extensions/messenger.py +183 -0
  29. zettcode-0.1.7/src/zettcode/app/agent/extensions/subagents.py +218 -0
  30. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/runtime.py +94 -8
  31. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/__init__.py +8 -0
  32. zettcode-0.1.7/src/zettcode/app/agent/storage/locks.py +130 -0
  33. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/commands.py +10 -3
  34. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/app.py +149 -15
  35. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/boot.py +5 -2
  36. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/commands.py +49 -8
  37. zettcode-0.1.7/src/zettcode/app/ui/config_form.py +203 -0
  38. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/followups.py +61 -10
  39. zettcode-0.1.7/src/zettcode/app/ui/gc_policy.py +147 -0
  40. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/keys.py +14 -1
  41. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/labels.py +40 -6
  42. zettcode-0.1.7/src/zettcode/app/ui/messaging.py +244 -0
  43. zettcode-0.1.7/src/zettcode/app/ui/providers.py +474 -0
  44. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/rows.py +1 -0
  45. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/sessions.py +89 -0
  46. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/shell.py +38 -1
  47. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/updating.py +63 -11
  48. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/__init__.py +5 -0
  49. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/composer.py +98 -2
  50. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/follow_up.py +2 -2
  51. zettcode-0.1.7/src/zettcode/app/ui/widgets/input_page.py +175 -0
  52. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/transcript.py +3 -2
  53. zettcode-0.1.7/src/zettcode/broker/__init__.py +22 -0
  54. zettcode-0.1.7/src/zettcode/broker/__main__.py +10 -0
  55. zettcode-0.1.7/src/zettcode/broker/client.py +533 -0
  56. zettcode-0.1.7/src/zettcode/broker/files.py +131 -0
  57. zettcode-0.1.7/src/zettcode/broker/finder.py +190 -0
  58. zettcode-0.1.7/src/zettcode/broker/hub.py +275 -0
  59. zettcode-0.1.7/src/zettcode/broker/log.py +11 -0
  60. zettcode-0.1.7/src/zettcode/broker/models.py +77 -0
  61. zettcode-0.1.7/src/zettcode/broker/process.py +117 -0
  62. zettcode-0.1.7/src/zettcode/broker/protocol.py +282 -0
  63. zettcode-0.1.7/src/zettcode/broker/roster.py +176 -0
  64. zettcode-0.1.7/src/zettcode/broker/runner.py +131 -0
  65. zettcode-0.1.7/src/zettcode/broker/server.py +184 -0
  66. zettcode-0.1.7/src/zettcode/broker/text.py +81 -0
  67. zettcode-0.1.7/src/zettcode/broker/web.py +192 -0
  68. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/cli.py +55 -1
  69. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/collector.py +10 -0
  70. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/config.py +47 -6
  71. zettcode-0.1.7/src/zettcode/config_edit.py +268 -0
  72. zettcode-0.1.7/src/zettcode/filelock.py +125 -0
  73. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/builtins.py +4 -4
  74. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/container.py +44 -2
  75. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/loader.py +16 -0
  76. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/plugin.py +4 -0
  77. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/state.py +3 -0
  78. zettcode-0.1.7/src/zettcode/providers.py +212 -0
  79. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/theme.py +49 -17
  80. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/scroll.py +52 -7
  81. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/__init__.py +2 -0
  82. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/text.py +43 -0
  83. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/runner.py +24 -0
  84. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/theme_file.py +5 -2
  85. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/textarea.py +11 -0
  86. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/update.py +113 -4
  87. zettcode-0.1.5/src/zettcode/app/agent/agent/base.py +0 -29
  88. zettcode-0.1.5/src/zettcode/app/agent/agent/settings.py +0 -68
  89. zettcode-0.1.5/src/zettcode/app/agent/errors.py +0 -39
  90. zettcode-0.1.5/src/zettcode/app/agent/extensions/subagents.py +0 -140
  91. {zettcode-0.1.5 → zettcode-0.1.7}/.gitignore +0 -0
  92. {zettcode-0.1.5 → zettcode-0.1.7}/LICENSE +0 -0
  93. {zettcode-0.1.5 → zettcode-0.1.7}/README.md +0 -0
  94. {zettcode-0.1.5 → zettcode-0.1.7}/README.zh-CN.md +0 -0
  95. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/_compat.py +0 -0
  96. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/__init__.py +0 -0
  97. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/__init__.py +0 -0
  98. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/__init__.py +0 -0
  99. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/exports.py +0 -0
  100. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/agent/interaction.py +0 -0
  101. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/__init__.py +0 -0
  102. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/__init__.py +0 -0
  103. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/log.py +0 -0
  104. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/conversation/transcript/oneoff.py +0 -0
  105. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/__init__.py +0 -0
  106. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/ask.py +0 -0
  107. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/dialogue/title.py +0 -0
  108. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/script.js +0 -0
  109. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/style.css +0 -0
  110. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/export/trace/template.html +0 -0
  111. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/__init__.py +0 -0
  112. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/capabilities.py +0 -0
  113. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/compaction.py +0 -0
  114. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/context.py +0 -0
  115. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/mcp_notice.py +0 -0
  116. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/tool_output.py +0 -0
  117. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/extensions/usage.py +0 -0
  118. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/permission/__init__.py +0 -0
  119. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/permission/approval.py +0 -0
  120. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/permission/readonly.py +0 -0
  121. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/metadata.py +0 -0
  122. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/records.py +0 -0
  123. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/agent/storage/store.py +0 -0
  124. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/brand.py +0 -0
  125. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/pipeline.py +0 -0
  126. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/registry.py +0 -0
  127. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/__init__.py +0 -0
  128. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/clipboard.py +0 -0
  129. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/demo.py +0 -0
  130. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/notices.py +0 -0
  131. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/settings.py +0 -0
  132. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/approval_page.py +0 -0
  133. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/ask_page.py +0 -0
  134. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/completer.py +0 -0
  135. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/context_page.py +0 -0
  136. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/model_page.py +0 -0
  137. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/panel.py +0 -0
  138. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/root.py +0 -0
  139. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/send_kind_page.py +0 -0
  140. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/sessions_page.py +0 -0
  141. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/steering.py +0 -0
  142. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/update_page.py +0 -0
  143. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/app/ui/widgets/welcome.py +0 -0
  144. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/paths.py +0 -0
  145. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/__init__.py +0 -0
  146. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/extension.py +0 -0
  147. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/plugins/mixins.py +0 -0
  148. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/__init__.py +0 -0
  149. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/capabilities.py +0 -0
  150. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/clipboard.py +0 -0
  151. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/console.py +0 -0
  152. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/__init__.py +0 -0
  153. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/app.py +0 -0
  154. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/events.py +0 -0
  155. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/focus.py +0 -0
  156. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/geometry.py +0 -0
  157. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/host.py +0 -0
  158. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/keymap.py +0 -0
  159. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/scheduler.py +0 -0
  160. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/screen.py +0 -0
  161. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/core/widget.py +0 -0
  162. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/gallery.py +0 -0
  163. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/glyphs.py +0 -0
  164. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/__init__.py +0 -0
  165. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/bridge.py +0 -0
  166. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/decoder.py +0 -0
  167. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/events.py +0 -0
  168. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/keys.py +0 -0
  169. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/input/reader.py +0 -0
  170. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/__init__.py +0 -0
  171. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/box.py +0 -0
  172. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/overlay.py +0 -0
  173. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/solver.py +0 -0
  174. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/layout/spacing.py +0 -0
  175. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/canvas.py +0 -0
  176. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/code.py +0 -0
  177. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/color.py +0 -0
  178. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/renderer.py +0 -0
  179. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/rich_text.py +0 -0
  180. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/style.py +0 -0
  181. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/render/sweep.py +0 -0
  182. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/terminal.py +0 -0
  183. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/testing/__init__.py +0 -0
  184. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/testing/harness.py +0 -0
  185. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/testing/snapshot.py +0 -0
  186. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/__init__.py +0 -0
  187. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/collapsible.py +0 -0
  188. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/completion.py +0 -0
  189. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/dialog.py +0 -0
  190. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/diff.py +0 -0
  191. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/list.py +0 -0
  192. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/list_page.py +0 -0
  193. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/markdown.py +0 -0
  194. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/progress.py +0 -0
  195. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/rich_text.py +0 -0
  196. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/status.py +0 -0
  197. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/table.py +0 -0
  198. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/tasks.py +0 -0
  199. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/text.py +0 -0
  200. {zettcode-0.1.5 → zettcode-0.1.7}/src/zettcode/tui/widgets/toast.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: zettcode
3
- Version: 0.1.5
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.5"
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()
@@ -8,6 +8,7 @@ agent is where the shell asks for everything the runtime loaded.
8
8
  from __future__ import annotations
9
9
 
10
10
  from ....plugins import UiRow
11
+ from ....providers import BUILTIN_PROVIDERS, Provider, merge_providers
11
12
  from ...commands import Command, CommandContext, CommandResult
12
13
  from ..dialogue.mentions import MentionProvider
13
14
  from .exports import ExportsMixin
@@ -44,6 +45,11 @@ class CommandsMixin(ExportsMixin):
44
45
  """Return the ``@`` resource providers the installed plugins registered."""
45
46
  return self.runtime.plugins.mentions
46
47
 
48
+ @property
49
+ def providers(self) -> tuple[Provider, ...]:
50
+ """Return the hosts ``/config`` can offer: the built-in ones, then the plugins' own."""
51
+ return merge_providers(BUILTIN_PROVIDERS, self.runtime.plugins.providers)
52
+
47
53
  @property
48
54
  def plugin_failures(self) -> tuple[str, ...]:
49
55
  """Return one message per plugin that could not be loaded or activated."""
@@ -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(
@@ -0,0 +1,164 @@
1
+ """What the reader can change about how the agent runs: model, reasoning effort, shell permission."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from pathlib import Path
7
+
8
+ from zett_agent.model import ReasoningEffort
9
+
10
+ from ....config import ModelConfig, _read_models
11
+ from ....config_edit import (
12
+ ConfigEditError,
13
+ add_model,
14
+ file_entries,
15
+ remove_model,
16
+ replace_model,
17
+ write_text_atomically,
18
+ )
19
+ from ....paths import config_path
20
+ from ..permission import PERMISSION_LEVELS, Permission
21
+ from .base import AgentBase
22
+
23
+
24
+ class SettingsMixin(AgentBase):
25
+ """Read and change the model, the reasoning effort, and how much of the shell may run unasked."""
26
+
27
+ @property
28
+ def reduced_motion(self) -> bool:
29
+ """Return whether decorative animation is disabled."""
30
+ return self.runtime.config.reduced_motion
31
+
32
+ @property
33
+ def models(self) -> tuple[ModelConfig, ...]:
34
+ """Return the models available for selection."""
35
+ return self.runtime.config.models
36
+
37
+ @property
38
+ def active_model(self) -> ModelConfig:
39
+ """Return the model selected for the next turn."""
40
+ return self.runtime.active_model
41
+
42
+ def find_model(self, name: str) -> ModelConfig | None:
43
+ """Return the configured model a recorded name stands for, if exactly one does."""
44
+ return self.runtime.find_model(name)
45
+
46
+ def use_model(self, name: str | ModelConfig) -> ModelConfig:
47
+ """Select a configured model for subsequent turns."""
48
+ return self.runtime.use_model(name)
49
+
50
+ # -- the models in the config file --------------------------------------------------
51
+ @property
52
+ def config_file(self) -> Path:
53
+ """Return the file the models are read from and written to."""
54
+ return config_path()
55
+
56
+ def _read_config_file(self) -> str:
57
+ """Return the config file's text, or raise :class:`ConfigEditError` saying why not."""
58
+ path = self.config_file
59
+ try:
60
+ return path.read_text(encoding="utf-8")
61
+ except FileNotFoundError as error:
62
+ raise ConfigEditError(f"{path} does not exist") from error
63
+ except (OSError, UnicodeDecodeError) as error:
64
+ raise ConfigEditError(f"{path} cannot be read: {error}") from error
65
+
66
+ def file_models(self) -> tuple[tuple[ModelConfig, Mapping[str, object]], ...]:
67
+ """Return each model in the file with the keys the file writes for it.
68
+
69
+ The file is the source of truth for editing, not the models the session started with: a
70
+ reader may have changed it by hand since. The keys are what is *written* — a token left to
71
+ an environment variable is absent — which is what a rewrite has to start from.
72
+
73
+ Raises:
74
+ ConfigEditError: When the file cannot be read, is not valid, or lays its models out in
75
+ a way the editor cannot safely change.
76
+ """
77
+ text = self._read_config_file()
78
+ path = self.config_file
79
+ entries = file_entries(text, where=path)
80
+ try:
81
+ models = _read_models(entries, path)
82
+ except ValueError as error:
83
+ raise ConfigEditError(str(error)) from error
84
+ return tuple(zip(models, entries, strict=True))
85
+
86
+ def _commit_models(self, text: str) -> ModelConfig:
87
+ """Write the edited text, then make the running session use what it now says.
88
+
89
+ The file is written first and the session is changed second, so a write that fails leaves
90
+ the session as it was, and what the session holds is always something the file holds too.
91
+ """
92
+ path = self.config_file
93
+ try:
94
+ write_text_atomically(path, text)
95
+ except OSError as error:
96
+ raise ConfigEditError(f"{path} could not be written: {error}") from error
97
+ models = _read_models(file_entries(text, where=path), path)
98
+ return self.runtime.replace_models(models)
99
+
100
+ def add_configured_model(self, entry: Mapping[str, object]) -> ModelConfig:
101
+ """Add one ``[[models]]`` entry to the file and to this session; return the new model.
102
+
103
+ The model in use does not change: adding is not choosing. The new one appears in ``/model``.
104
+
105
+ Args:
106
+ entry: The keys of the new model as the file writes them.
107
+
108
+ Raises:
109
+ ConfigEditError: When the entry is not a model that can be called, or the file cannot
110
+ be edited. Nothing is written then.
111
+ """
112
+ text = add_model(self._read_config_file(), entry, where=self.config_file)
113
+ self._commit_models(text)
114
+ return self.runtime.config.models[-1]
115
+
116
+ def update_configured_model(self, index: int, entry: Mapping[str, object]) -> ModelConfig:
117
+ """Rewrite model number ``index`` (from zero) of the file as ``entry``; return the result."""
118
+ text = replace_model(self._read_config_file(), index, entry, where=self.config_file)
119
+ self._commit_models(text)
120
+ return self.runtime.config.models[index]
121
+
122
+ def remove_configured_model(self, index: int) -> ModelConfig:
123
+ """Remove model number ``index`` (from zero) from the file and this session.
124
+
125
+ Returns:
126
+ The model in use afterwards: the one before unless it was the one removed.
127
+
128
+ Raises:
129
+ ConfigEditError: When it is the only model, or the file cannot be edited.
130
+ """
131
+ text = remove_model(self._read_config_file(), index, where=self.config_file)
132
+ return self._commit_models(text)
133
+
134
+ @property
135
+ def effort(self) -> str:
136
+ """Return the reasoning effort the next request will ask for."""
137
+ return self.runtime.effort.value
138
+
139
+ @property
140
+ def efforts(self) -> tuple[str, ...]:
141
+ """Return the reasoning levels the runtime accepts, cheapest first."""
142
+ return tuple(level.value for level in ReasoningEffort)
143
+
144
+ def use_effort(self, name: str) -> str:
145
+ """Select a reasoning level for later requests and return its value."""
146
+ return self.runtime.use_effort(name).value
147
+
148
+ def approve_all_shell_commands(self) -> None:
149
+ """Approve every shell command for the rest of this run, without asking."""
150
+ self.runtime.approve_all_shell_commands()
151
+
152
+ @property
153
+ def permission(self) -> Permission:
154
+ """Return how much of the shell may run without asking."""
155
+ return self.runtime.permission
156
+
157
+ @property
158
+ def permissions(self) -> tuple[str, ...]:
159
+ """Return the permission levels, strictest first."""
160
+ return tuple(level.value for level in PERMISSION_LEVELS)
161
+
162
+ def use_permission(self, name: str | Permission) -> Permission:
163
+ """Set how much of the shell may run without asking, for the rest of this run."""
164
+ return self.runtime.use_permission(name)
@@ -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, plain_text, plain_text_part
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
- return UserMessage(content=f"{text}\n\n{hint}", attributes={"prompt": text})
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 = plain_text_part(part)
122
+ text = model_text_part(part)
113
123
  if text:
114
124
  content.append(TextContent(text))
115
125
  continue
@@ -0,0 +1,64 @@
1
+ """What a request did to the files, added up: the data behind the summary at the end of a turn."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from pathlib import PurePath
7
+
8
+ from .entries import DeleteEntry, Entry, EntryStatus, FileChangeEntry, FileSummary
9
+
10
+
11
+ def shown_path(path: str, workspace: PurePath | None) -> str:
12
+ """Return a file as the reader knows it: relative to the workspace when it is inside it.
13
+
14
+ The model names the same file as ``app.py`` in one call and as ``/work/app.py`` in the next, and
15
+ the summary must not count them twice. Only the text is looked at, never the disk.
16
+
17
+ Args:
18
+ path: The path as the call named it.
19
+ workspace: The directory the session works in, or ``None`` to leave every path as it is.
20
+ """
21
+ if workspace is None or not path:
22
+ return path
23
+ candidate = PurePath(path)
24
+ # Rooted, not only absolute: on Windows ``/work/a.py`` has a root and no drive, which is not
25
+ # "absolute" to pathlib, and it still has to be compared with the workspace it is inside.
26
+ if candidate.is_absolute() or candidate.root:
27
+ try:
28
+ return candidate.relative_to(workspace).as_posix()
29
+ except ValueError:
30
+ return path
31
+ return candidate.as_posix()
32
+
33
+
34
+ def changed_files(entries: Iterable[Entry], workspace: PurePath | None = None) -> tuple[FileSummary, ...]:
35
+ """Return the files a run of entries changed, each once, in the order they were first touched.
36
+
37
+ Only a call that *succeeded* counts: a failed edit changed nothing, and a skipped one never ran.
38
+ A file changed several times is one row with the totals. A file deleted and then written again is
39
+ a file that exists, counted from the write, and one written and then deleted is deleted.
40
+
41
+ Args:
42
+ entries: The entries of one request, in order; anything that is not a file change is ignored.
43
+ workspace: The directory paths are shown relative to.
44
+ """
45
+ order: list[str] = []
46
+ totals: dict[str, FileSummary] = {}
47
+ for entry in entries:
48
+ if not isinstance(entry, (FileChangeEntry, DeleteEntry)) or entry.status is not EntryStatus.COMPLETED:
49
+ continue
50
+ if not entry.path:
51
+ continue
52
+ path = shown_path(entry.path, workspace)
53
+ if path not in totals:
54
+ order.append(path)
55
+ totals[path] = FileSummary(path)
56
+ current = totals[path]
57
+ if isinstance(entry, DeleteEntry):
58
+ totals[path] = FileSummary(path, deleted=True)
59
+ continue
60
+ base = FileSummary(path) if current.deleted else current
61
+ totals[path] = FileSummary(
62
+ path, base.added + entry.added, base.removed + getattr(entry, "removed", 0), deleted=False
63
+ )
64
+ return tuple(totals[path] for path in order)