zettcode 0.1.3__tar.gz → 0.1.5__tar.gz

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