dandy 2.2.0__tar.gz → 3.0.0a2__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 (285) hide show
  1. {dandy-2.2.0 → dandy-3.0.0a2}/PKG-INFO +41 -16
  2. dandy-3.0.0a2/README.md +52 -0
  3. dandy-3.0.0a2/dandy/__init__.py +86 -0
  4. dandy-3.0.0a2/dandy/application/agent/agent.py +294 -0
  5. dandy-3.0.0a2/dandy/application/agent/cli/agents_md.py +27 -0
  6. dandy-3.0.0a2/dandy/application/agent/cli/cli_agent.py +78 -0
  7. dandy-3.0.0a2/dandy/application/agent/cli/intelligence/bots/coding_bot.py +35 -0
  8. dandy-3.0.0a2/dandy/application/agent/cli/intelligence/bots/init_bot.py +23 -0
  9. dandy-3.0.0a2/dandy/application/agent/cli/intelligence/bots/session_naming_bot.py +40 -0
  10. dandy-3.0.0a2/dandy/application/agent/cli/intelligence/prompts.py +183 -0
  11. dandy-3.0.0a2/dandy/application/agent/exceptions.py +9 -0
  12. dandy-3.0.0a2/dandy/application/agent/intel.py +36 -0
  13. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/__init__.py +42 -0
  14. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/command/__init__.py +6 -0
  15. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/command/command_tool.py +34 -0
  16. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/__init__.py +33 -0
  17. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/create_directory_tool.py +28 -0
  18. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/delete_file_tool.py +28 -0
  19. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/edit_file_tool.py +74 -0
  20. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/list_directory_tool.py +43 -0
  21. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/read_file_tool.py +68 -0
  22. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/search_tool.py +103 -0
  23. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/file/write_file_tool.py +30 -0
  24. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/git/__init__.py +7 -0
  25. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/git/git_diff_tool.py +41 -0
  26. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/git/git_status_tool.py +23 -0
  27. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/paths.py +28 -0
  28. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/question/__init__.py +9 -0
  29. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/question/question_tool.py +35 -0
  30. dandy-3.0.0a2/dandy/application/agent/intelligence/tools/tool_errors.py +25 -0
  31. dandy-3.0.0a2/dandy/application/agent/prompts.py +11 -0
  32. dandy-3.0.0a2/dandy/application/agent/session.py +30 -0
  33. dandy-3.0.0a2/dandy/application/agent/session_store.py +145 -0
  34. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/application}/bot/bot.py +16 -36
  35. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/application}/bot/exceptions.py +2 -2
  36. dandy-2.2.0/dandy/llm/decoder/decoder.py → dandy-3.0.0a2/dandy/application/decision/decision.py +61 -79
  37. dandy-3.0.0a2/dandy/application/decision/mixin.py +12 -0
  38. dandy-3.0.0a2/dandy/application/decision/service.py +48 -0
  39. dandy-3.0.0a2/dandy/application/diligence/diligence.py +40 -0
  40. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/application}/diligence/handler.py +5 -3
  41. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/application}/diligence/mixin.py +3 -3
  42. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/application}/diligence/second_pass/diligence.py +3 -4
  43. dandy-3.0.0a2/dandy/application/diligence/service.py +31 -0
  44. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/application}/diligence/stop_word_removal/diligence.py +10 -10
  45. dandy-3.0.0a2/dandy/application/diligence/vowel_removal/constants.py +1 -0
  46. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/application}/diligence/vowel_removal/diligence.py +10 -12
  47. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/application}/llm/mixin.py +8 -10
  48. dandy-3.0.0a2/dandy/application/llm/service.py +142 -0
  49. dandy-3.0.0a2/dandy/application/skill/mixin.py +12 -0
  50. dandy-3.0.0a2/dandy/application/skill/service.py +148 -0
  51. dandy-3.0.0a2/dandy/application/skill/tools.py +48 -0
  52. dandy-3.0.0a2/dandy/application/tool/mixin.py +12 -0
  53. dandy-3.0.0a2/dandy/application/tool/service.py +261 -0
  54. dandy-3.0.0a2/dandy/domain/decision/exceptions.py +17 -0
  55. dandy-3.0.0a2/dandy/domain/decision/intel.py +19 -0
  56. {dandy-2.2.0/dandy/llm/decoder/intelligence → dandy-3.0.0a2/dandy/domain/decision}/prompts.py +9 -17
  57. {dandy-2.2.0/dandy/intel/field → dandy-3.0.0a2/dandy/domain/intel}/annotation.py +5 -5
  58. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/domain}/intel/exceptions.py +2 -2
  59. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/domain}/intel/factory.py +17 -30
  60. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/domain}/intel/intel.py +24 -72
  61. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/domain}/intel/mixin.py +3 -3
  62. dandy-3.0.0a2/dandy/domain/intel/service.py +26 -0
  63. dandy-3.0.0a2/dandy/domain/intel/typing.py +5 -0
  64. dandy-3.0.0a2/dandy/domain/message/compaction.py +161 -0
  65. dandy-3.0.0a2/dandy/domain/message/message.py +243 -0
  66. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/domain}/prompt/prompt.py +38 -83
  67. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/domain}/prompt/snippet.py +43 -26
  68. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/domain/request}/exceptions.py +2 -2
  69. dandy-3.0.0a2/dandy/domain/request/options.py +51 -0
  70. {dandy-2.2.0/dandy/llm/intelligence → dandy-3.0.0a2/dandy/domain/request}/prompts.py +10 -7
  71. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/domain}/request/request.py +21 -24
  72. dandy-3.0.0a2/dandy/domain/skill/__init__.py +3 -0
  73. dandy-3.0.0a2/dandy/domain/skill/exceptions.py +5 -0
  74. dandy-3.0.0a2/dandy/domain/skill/prompts.py +35 -0
  75. dandy-3.0.0a2/dandy/domain/skill/skill.py +90 -0
  76. dandy-3.0.0a2/dandy/domain/tokens/estimate.py +119 -0
  77. dandy-3.0.0a2/dandy/domain/tool/__init__.py +5 -0
  78. dandy-3.0.0a2/dandy/domain/tool/beat.py +25 -0
  79. dandy-3.0.0a2/dandy/domain/tool/exceptions.py +5 -0
  80. dandy-3.0.0a2/dandy/domain/tool/intel.py +14 -0
  81. dandy-3.0.0a2/dandy/domain/tool/repeated_call.py +145 -0
  82. dandy-3.0.0a2/dandy/domain/tool/tool.py +145 -0
  83. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/cache/cache.py +6 -6
  84. dandy-3.0.0a2/dandy/infrastructure/cache/decorators.py +40 -0
  85. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/cache/exceptions.py +2 -2
  86. dandy-3.0.0a2/dandy/infrastructure/cache/key.py +58 -0
  87. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/cache/memory/cache.py +2 -2
  88. dandy-3.0.0a2/dandy/infrastructure/cache/memory/decorators.py +23 -0
  89. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/cache/sqlite/cache.py +21 -20
  90. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/cache/sqlite/connection.py +6 -5
  91. dandy-3.0.0a2/dandy/infrastructure/cache/sqlite/decorators.py +23 -0
  92. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/file/mixin.py +3 -3
  93. dandy-3.0.0a2/dandy/infrastructure/file/service.py +48 -0
  94. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/infrastructure}/future/exceptions.py +2 -2
  95. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/infrastructure}/future/future.py +4 -7
  96. dandy-3.0.0a2/dandy/infrastructure/future/process.py +9 -0
  97. dandy-3.0.0a2/dandy/infrastructure/http/connector.py +46 -0
  98. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/http/exceptions.py +2 -2
  99. {dandy-2.2.0/dandy/http/intelligence → dandy-3.0.0a2/dandy/infrastructure/http}/intel.py +9 -4
  100. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/http/mixin.py +2 -2
  101. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/http/service.py +7 -13
  102. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/http/url.py +5 -3
  103. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/llm/config.py +36 -44
  104. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/llm/connector.py +182 -45
  105. dandy-3.0.0a2/dandy/infrastructure/llm/fenced_json.py +26 -0
  106. dandy-2.2.0/dandy/bot/recorder.py → dandy-3.0.0a2/dandy/infrastructure/recorder/bot_events.py +15 -33
  107. dandy-2.2.0/dandy/llm/decoder/recorder.py → dandy-3.0.0a2/dandy/infrastructure/recorder/decision_events.py +15 -27
  108. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/decorators.py +25 -13
  109. dandy-2.2.0/dandy/llm/diligence/recorder.py → dandy-3.0.0a2/dandy/infrastructure/recorder/diligence_events.py +3 -5
  110. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/events.py +3 -5
  111. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/exceptions.py +2 -2
  112. dandy-2.2.0/dandy/llm/recorder.py → dandy-3.0.0a2/dandy/infrastructure/recorder/llm_events.py +35 -60
  113. dandy-3.0.0a2/dandy/infrastructure/recorder/recorder.py +154 -0
  114. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/recording.py +5 -5
  115. dandy-3.0.0a2/dandy/infrastructure/recorder/renderer/html.py +45 -0
  116. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/renderer/json.py +4 -9
  117. dandy-3.0.0a2/dandy/infrastructure/recorder/renderer/markdown.py +31 -0
  118. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/renderer/renderer.py +2 -5
  119. dandy-3.0.0a2/dandy/infrastructure/recorder/tool_events.py +34 -0
  120. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/utils.py +3 -7
  121. dandy-3.0.0a2/dandy/interfaces/cli/cli.py +321 -0
  122. dandy-3.0.0a2/dandy/interfaces/cli/error_log.py +54 -0
  123. dandy-3.0.0a2/dandy/interfaces/cli/main.py +130 -0
  124. dandy-2.2.0/dandy/cli/processing_phrases.py → dandy-3.0.0a2/dandy/interfaces/cli/thinking_phrases.py +41 -4
  125. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/interfaces}/cli/tui/ascii.py +1 -1
  126. dandy-3.0.0a2/dandy/interfaces/cli/tui/banner.py +56 -0
  127. dandy-3.0.0a2/dandy/interfaces/cli/tui/markdown.py +416 -0
  128. dandy-3.0.0a2/dandy/interfaces/cli/tui/printer.py +273 -0
  129. dandy-3.0.0a2/dandy/interfaces/cli/tui/story_progress.py +264 -0
  130. dandy-3.0.0a2/dandy/interfaces/cli/tui/text.py +76 -0
  131. dandy-3.0.0a2/dandy/interfaces/cli/tui/tui.py +131 -0
  132. dandy-3.0.0a2/dandy/interfaces/cli/utils.py +76 -0
  133. dandy-3.0.0a2/dandy/shared/cancellation.py +28 -0
  134. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/connector/connector.py +1 -1
  135. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/connector/exceptions.py +2 -2
  136. dandy-3.0.0a2/dandy/shared/constants.py +34 -0
  137. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/debug.py +7 -7
  138. dandy-3.0.0a2/dandy/shared/exceptions.py +33 -0
  139. dandy-2.2.0/dandy/file/utils.py → dandy-3.0.0a2/dandy/shared/files.py +12 -18
  140. dandy-3.0.0a2/dandy/shared/interaction.py +83 -0
  141. dandy-3.0.0a2/dandy/shared/media.py +48 -0
  142. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/service/exceptions.py +2 -2
  143. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/service/mixin.py +1 -3
  144. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/service/service.py +1 -1
  145. dandy-3.0.0a2/dandy/shared/settings/__init__.py +3 -0
  146. dandy-3.0.0a2/dandy/shared/settings/default_settings.py +44 -0
  147. dandy-3.0.0a2/dandy/shared/settings/load.py +105 -0
  148. dandy-2.2.0/dandy/conf.py → dandy-3.0.0a2/dandy/shared/settings/settings.py +38 -16
  149. dandy-3.0.0a2/dandy/shared/subprocess.py +70 -0
  150. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/typing/exceptions.py +2 -2
  151. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/typing/registry.py +2 -4
  152. dandy-2.2.0/dandy/core/typing/tools.py → dandy-3.0.0a2/dandy/shared/typing/signature.py +9 -16
  153. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/typing/typed_kwargs.py +6 -13
  154. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/typing/typing.py +1 -1
  155. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/utils.py +36 -13
  156. {dandy-2.2.0 → dandy-3.0.0a2}/dandy.egg-info/PKG-INFO +41 -16
  157. dandy-3.0.0a2/dandy.egg-info/SOURCES.txt +207 -0
  158. dandy-3.0.0a2/dandy.egg-info/entry_points.txt +2 -0
  159. dandy-3.0.0a2/dandy.egg-info/requires.txt +22 -0
  160. dandy-3.0.0a2/pyproject.toml +192 -0
  161. dandy-3.0.0a2/tests/test_dandy.py +70 -0
  162. dandy-2.2.0/README.md +0 -23
  163. dandy-2.2.0/dandy/__init__.py +0 -36
  164. dandy-2.2.0/dandy/cache/decorators.py +0 -54
  165. dandy-2.2.0/dandy/cache/memory/decorators.py +0 -32
  166. dandy-2.2.0/dandy/cache/sqlite/decorators.py +0 -32
  167. dandy-2.2.0/dandy/cache/tools.py +0 -73
  168. dandy-2.2.0/dandy/cli/actions/action.py +0 -34
  169. dandy-2.2.0/dandy/cli/actions/bot/action.py +0 -144
  170. dandy-2.2.0/dandy/cli/actions/code/action.py +0 -25
  171. dandy-2.2.0/dandy/cli/actions/code/intelligence/bots/coding_bot.py +0 -5
  172. dandy-2.2.0/dandy/cli/actions/constants.py +0 -11
  173. dandy-2.2.0/dandy/cli/actions/explain/action.py +0 -16
  174. dandy-2.2.0/dandy/cli/actions/explain/intelligence/bots/code_explainer_bot.py +0 -34
  175. dandy-2.2.0/dandy/cli/actions/explain/intelligence/decoders/files_decoder.py +0 -23
  176. dandy-2.2.0/dandy/cli/actions/explain/intelligence/workflow.py +0 -47
  177. dandy-2.2.0/dandy/cli/actions/help/action.py +0 -25
  178. dandy-2.2.0/dandy/cli/actions/help/intelligence/bots/default_user_input_bot.py +0 -15
  179. dandy-2.2.0/dandy/cli/actions/help/intelligence/intel/default_user_input_intel.py +0 -5
  180. dandy-2.2.0/dandy/cli/actions/help/intelligence/prompt.py +0 -24
  181. dandy-2.2.0/dandy/cli/actions/manager.py +0 -43
  182. dandy-2.2.0/dandy/cli/actions/quit/action.py +0 -15
  183. dandy-2.2.0/dandy/cli/cli.py +0 -48
  184. dandy-2.2.0/dandy/cli/intelligence/bots/source_code_bot.py +0 -37
  185. dandy-2.2.0/dandy/cli/intelligence/intel/source_code_intel.py +0 -17
  186. dandy-2.2.0/dandy/cli/main.py +0 -72
  187. dandy-2.2.0/dandy/cli/session.py +0 -52
  188. dandy-2.2.0/dandy/cli/tui/printer.py +0 -86
  189. dandy-2.2.0/dandy/cli/tui/tools.py +0 -32
  190. dandy-2.2.0/dandy/cli/tui/tui.py +0 -209
  191. dandy-2.2.0/dandy/cli/utils.py +0 -36
  192. dandy-2.2.0/dandy/constants.py +0 -29
  193. dandy-2.2.0/dandy/core/exceptions.py +0 -10
  194. dandy-2.2.0/dandy/core/future/tools.py +0 -13
  195. dandy-2.2.0/dandy/default_settings.py +0 -38
  196. dandy-2.2.0/dandy/file/audio/constants.py +0 -8
  197. dandy-2.2.0/dandy/file/audio/utils.py +0 -18
  198. dandy-2.2.0/dandy/file/exceptions.py +0 -9
  199. dandy-2.2.0/dandy/file/image/constants.py +0 -7
  200. dandy-2.2.0/dandy/file/image/utils.py +0 -18
  201. dandy-2.2.0/dandy/file/service.py +0 -50
  202. dandy-2.2.0/dandy/http/connector.py +0 -40
  203. dandy-2.2.0/dandy/intel/service.py +0 -34
  204. dandy-2.2.0/dandy/intel/typing.py +0 -5
  205. dandy-2.2.0/dandy/llm/decoder/exceptions.py +0 -17
  206. dandy-2.2.0/dandy/llm/decoder/intel.py +0 -20
  207. dandy-2.2.0/dandy/llm/decoder/mixin.py +0 -12
  208. dandy-2.2.0/dandy/llm/decoder/service.py +0 -56
  209. dandy-2.2.0/dandy/llm/diligence/__init__.py +0 -0
  210. dandy-2.2.0/dandy/llm/diligence/diligence.py +0 -24
  211. dandy-2.2.0/dandy/llm/diligence/second_pass/__init__.py +0 -0
  212. dandy-2.2.0/dandy/llm/diligence/service.py +0 -38
  213. dandy-2.2.0/dandy/llm/diligence/stop_word_removal/__init__.py +0 -0
  214. dandy-2.2.0/dandy/llm/diligence/vowel_removal/__init__.py +0 -0
  215. dandy-2.2.0/dandy/llm/diligence/vowel_removal/constants.py +0 -7
  216. dandy-2.2.0/dandy/llm/intelligence/__init__.py +0 -0
  217. dandy-2.2.0/dandy/llm/options.py +0 -35
  218. dandy-2.2.0/dandy/llm/prompt/__init__.py +0 -0
  219. dandy-2.2.0/dandy/llm/prompt/tools.py +0 -27
  220. dandy-2.2.0/dandy/llm/request/__init__.py +0 -0
  221. dandy-2.2.0/dandy/llm/request/message.py +0 -198
  222. dandy-2.2.0/dandy/llm/service.py +0 -91
  223. dandy-2.2.0/dandy/llm/tokens/__init__.py +0 -0
  224. dandy-2.2.0/dandy/llm/tokens/utils.py +0 -5
  225. dandy-2.2.0/dandy/recorder/__init__.py +0 -0
  226. dandy-2.2.0/dandy/recorder/recorder.py +0 -207
  227. dandy-2.2.0/dandy/recorder/renderer/__init__.py +0 -0
  228. dandy-2.2.0/dandy/recorder/renderer/html.py +0 -53
  229. dandy-2.2.0/dandy/recorder/renderer/markdown.py +0 -38
  230. dandy-2.2.0/dandy/tool/__init__.py +0 -0
  231. dandy-2.2.0/dandy/tool/git/__init__.py +0 -0
  232. dandy-2.2.0/dandy/tool/git/tool.py +0 -53
  233. dandy-2.2.0/dandy/tool/tool.py +0 -15
  234. dandy-2.2.0/dandy.egg-info/SOURCES.txt +0 -186
  235. dandy-2.2.0/dandy.egg-info/entry_points.txt +0 -2
  236. dandy-2.2.0/dandy.egg-info/requires.txt +0 -26
  237. dandy-2.2.0/pyproject.toml +0 -96
  238. {dandy-2.2.0 → dandy-3.0.0a2}/LICENSE.md +0 -0
  239. {dandy-2.2.0/dandy/bot → dandy-3.0.0a2/dandy/application}/__init__.py +0 -0
  240. {dandy-2.2.0/dandy/cache → dandy-3.0.0a2/dandy/application/agent}/__init__.py +0 -0
  241. {dandy-2.2.0/dandy/cache/memory → dandy-3.0.0a2/dandy/application/agent/cli}/__init__.py +0 -0
  242. {dandy-2.2.0/dandy/cache/sqlite → dandy-3.0.0a2/dandy/application/agent/cli/intelligence}/__init__.py +0 -0
  243. {dandy-2.2.0/dandy/cli → dandy-3.0.0a2/dandy/application/agent/cli/intelligence/bots}/__init__.py +0 -0
  244. {dandy-2.2.0/dandy/cli/actions → dandy-3.0.0a2/dandy/application/agent/intelligence}/__init__.py +0 -0
  245. {dandy-2.2.0/dandy/cli/actions → dandy-3.0.0a2/dandy/application}/bot/__init__.py +0 -0
  246. {dandy-2.2.0/dandy/cli/actions/code → dandy-3.0.0a2/dandy/application/decision}/__init__.py +0 -0
  247. {dandy-2.2.0/dandy/cli/actions/code/intelligence → dandy-3.0.0a2/dandy/application/diligence}/__init__.py +0 -0
  248. {dandy-2.2.0/dandy/cli/actions/code/intelligence/bots → dandy-3.0.0a2/dandy/application/diligence/second_pass}/__init__.py +0 -0
  249. {dandy-2.2.0/dandy/cli/actions/explain → dandy-3.0.0a2/dandy/application/diligence/stop_word_removal}/__init__.py +0 -0
  250. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/application}/diligence/stop_word_removal/constants.py +0 -0
  251. {dandy-2.2.0/dandy/cli/actions/explain/intelligence → dandy-3.0.0a2/dandy/application/diligence/vowel_removal}/__init__.py +0 -0
  252. {dandy-2.2.0/dandy/cli/actions/explain/intelligence/bots → dandy-3.0.0a2/dandy/application/llm}/__init__.py +0 -0
  253. {dandy-2.2.0/dandy/cli/actions/explain/intelligence/decoders → dandy-3.0.0a2/dandy/application/skill}/__init__.py +0 -0
  254. {dandy-2.2.0/dandy/cli/actions/explain/intelligence/intel → dandy-3.0.0a2/dandy/application/tool}/__init__.py +0 -0
  255. {dandy-2.2.0/dandy/cli/actions/help → dandy-3.0.0a2/dandy/domain}/__init__.py +0 -0
  256. {dandy-2.2.0/dandy/cli/actions/help/intelligence → dandy-3.0.0a2/dandy/domain/decision}/__init__.py +0 -0
  257. {dandy-2.2.0/dandy/cli/actions/help/intelligence/bots → dandy-3.0.0a2/dandy/domain/intel}/__init__.py +0 -0
  258. {dandy-2.2.0/dandy/cli/actions/help/intelligence/intel → dandy-3.0.0a2/dandy/domain/message}/__init__.py +0 -0
  259. {dandy-2.2.0/dandy/cli/actions/quit → dandy-3.0.0a2/dandy/domain/prompt}/__init__.py +0 -0
  260. {dandy-2.2.0/dandy/cli/intelligence → dandy-3.0.0a2/dandy/domain/request}/__init__.py +0 -0
  261. {dandy-2.2.0/dandy/cli/intelligence/bots → dandy-3.0.0a2/dandy/domain/tokens}/__init__.py +0 -0
  262. {dandy-2.2.0/dandy/cli/intelligence/intel → dandy-3.0.0a2/dandy/infrastructure}/__init__.py +0 -0
  263. {dandy-2.2.0/dandy/cli/tui → dandy-3.0.0a2/dandy/infrastructure/cache}/__init__.py +0 -0
  264. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/infrastructure/cache/memory}/__init__.py +0 -0
  265. {dandy-2.2.0/dandy/core/connector → dandy-3.0.0a2/dandy/infrastructure/cache/sqlite}/__init__.py +0 -0
  266. {dandy-2.2.0/dandy/core/future → dandy-3.0.0a2/dandy/infrastructure/file}/__init__.py +0 -0
  267. {dandy-2.2.0/dandy/core/service → dandy-3.0.0a2/dandy/infrastructure/future}/__init__.py +0 -0
  268. {dandy-2.2.0/dandy/core/typing → dandy-3.0.0a2/dandy/infrastructure/http}/__init__.py +0 -0
  269. {dandy-2.2.0/dandy/file → dandy-3.0.0a2/dandy/infrastructure/llm}/__init__.py +0 -0
  270. {dandy-2.2.0/dandy/file/audio → dandy-3.0.0a2/dandy/infrastructure/recorder}/__init__.py +0 -0
  271. {dandy-2.2.0/dandy/file/image → dandy-3.0.0a2/dandy/infrastructure/recorder/renderer}/__init__.py +0 -0
  272. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/renderer/html_templates/base_event_template.html +0 -0
  273. {dandy-2.2.0/dandy → dandy-3.0.0a2/dandy/infrastructure}/recorder/renderer/html_templates/base_recording_output_template.html +0 -0
  274. {dandy-2.2.0/dandy/http → dandy-3.0.0a2/dandy/interfaces}/__init__.py +0 -0
  275. {dandy-2.2.0/dandy/http/intelligence → dandy-3.0.0a2/dandy/interfaces/cli}/__init__.py +0 -0
  276. {dandy-2.2.0/dandy/intel → dandy-3.0.0a2/dandy/interfaces/cli/tui}/__init__.py +0 -0
  277. {dandy-2.2.0/dandy/intel/field → dandy-3.0.0a2/dandy/shared}/__init__.py +0 -0
  278. {dandy-2.2.0/dandy/llm → dandy-3.0.0a2/dandy/shared/connector}/__init__.py +0 -0
  279. {dandy-2.2.0/dandy/llm/decoder → dandy-3.0.0a2/dandy/shared/service}/__init__.py +0 -0
  280. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/singleton.py +0 -0
  281. {dandy-2.2.0/dandy/llm/decoder/intelligence → dandy-3.0.0a2/dandy/shared/typing}/__init__.py +0 -0
  282. {dandy-2.2.0/dandy/core → dandy-3.0.0a2/dandy/shared}/typing/constants.py +0 -0
  283. {dandy-2.2.0 → dandy-3.0.0a2}/dandy.egg-info/dependency_links.txt +0 -0
  284. {dandy-2.2.0 → dandy-3.0.0a2}/dandy.egg-info/top_level.txt +0 -0
  285. {dandy-2.2.0 → dandy-3.0.0a2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dandy
3
- Version: 2.2.0
3
+ Version: 3.0.0a2
4
4
  Summary: Python Artificial Intelligence Framework
5
5
  Author-email: Nathan Johnson <nathanj@stratusadv.com>
6
6
  License-Expression: MIT
@@ -20,30 +20,26 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE.md
23
- Requires-Dist: blessed==1.29.0
24
- Requires-Dist: requests==2.32.5
25
- Requires-Dist: pydantic==2.12.5
26
- Requires-Dist: python-dotenv==1.2.1
23
+ Requires-Dist: blessed==1.52.0
24
+ Requires-Dist: requests==2.34.2
25
+ Requires-Dist: pydantic==2.14.2
26
+ Requires-Dist: python-dotenv==1.2.4
27
+ Provides-Extra: documentation
28
+ Requires-Dist: mkdocs; extra == "documentation"
29
+ Requires-Dist: mkdocs-material; extra == "documentation"
30
+ Requires-Dist: mkdocstrings[python]; extra == "documentation"
27
31
  Provides-Extra: development
28
32
  Requires-Dist: build; extra == "development"
29
33
  Requires-Dist: Faker; extra == "development"
34
+ Requires-Dist: hypothesis; extra == "development"
35
+ Requires-Dist: import-linter; extra == "development"
30
36
  Requires-Dist: pytest; extra == "development"
37
+ Requires-Dist: pytest-cov; extra == "development"
31
38
  Requires-Dist: ruff; extra == "development"
32
39
  Requires-Dist: setuptools; extra == "development"
33
40
  Requires-Dist: twine; extra == "development"
34
41
  Requires-Dist: ty; extra == "development"
35
42
  Requires-Dist: wheel; extra == "development"
36
- Provides-Extra: documentation
37
- Requires-Dist: mkdocs; extra == "documentation"
38
- Requires-Dist: markdown-exec[ansi]; extra == "documentation"
39
- Requires-Dist: mkdocstrings-python; extra == "documentation"
40
- Requires-Dist: mkdocs-gen-files; extra == "documentation"
41
- Requires-Dist: mkdocs-include-markdown-plugin; extra == "documentation"
42
- Requires-Dist: mkdocs-literate-nav; extra == "documentation"
43
- Requires-Dist: mkdocs-material; extra == "documentation"
44
- Requires-Dist: mkdocs-section-index; extra == "documentation"
45
- Requires-Dist: mkdocs-table-reader-plugin; extra == "documentation"
46
- Requires-Dist: openpyxl; extra == "documentation"
47
43
  Dynamic: license-file
48
44
 
49
45
  <div align="center">
@@ -69,3 +65,32 @@ Start by installing with `pip install dandy` and then checking out our [quick st
69
65
 
70
66
  Check out the [Dandy Website](https://dandysoftware.com) for documentation including tutorials and more.
71
67
 
68
+ ### Command Line
69
+
70
+ The `dandy` command runs the coding assistant against the current project. Run it from your project root (where `dandy_settings.py` lives). If no `dandy_settings.py` exists, dandy will instead load settings from a `dandy.json` file found in `.dandy/dandy.json` in the current directory, or `~/.config/dandy/dandy.json` as a last resort before creating a settings file for you.
71
+
72
+ ```bash
73
+ dandy # start an interactive agentic loop
74
+ dandy "fix the failing tests" # run one request, then exit
75
+ dandy -h # show usage
76
+ ```
77
+
78
+ Inside the interactive session every message goes to the coding agent. `/clear` resets the conversation, and pressing `escape` twice (or `/quit`) exits the loop. Use the one-shot form when you only want a single answer without entering the REPL.
79
+
80
+ The CLI's model is set with `CLI_CONFIG` in `dandy_settings.py`, e.g. `CLI_CONFIG = {'CODING': 'DEFAULT'}` points the coding agent at the `DEFAULT` entry in `LLM_CONFIGS`.
81
+
82
+ The same settings can be expressed as JSON when you don't want a Python module. Keys are case-insensitive at the top level (`llm_configs` === `LLM_CONFIGS`), so a minimal `.dandy/dandy.json` looks like:
83
+
84
+ ```json
85
+ {
86
+ "llm_configs": {
87
+ "default": {
88
+ "host": "api.example.com",
89
+ "api_key": "your-key",
90
+ "model": "your-model"
91
+ }
92
+ },
93
+ "cli_config": {"coding": "default"}
94
+ }
95
+ ```
96
+
@@ -0,0 +1,52 @@
1
+ <div align="center">
2
+ <a href="https://dandysoftware.com">
3
+ <img alt="Dandy Logo" src="https://dandysoftware.com/static/img/dandy_icon_128.png"/>
4
+ </a>
5
+ </div>
6
+
7
+ # Dandy
8
+
9
+ ![Build](https://img.shields.io/github/actions/workflow/status/stratusadv/dandy/run_tests.yml)
10
+ ![Python Versions](https://img.shields.io/pypi/pyversions/dandy)
11
+ ![PyPI Version](https://img.shields.io/pypi/v/dandy)
12
+ ![Downloads](https://img.shields.io/pypi/dm/dandy)
13
+
14
+ A powerful open-source Python framework that simplifies the development of artificial intelligence software.
15
+
16
+ ### Get Started
17
+
18
+ Start by installing with `pip install dandy` and then checking out our [quick start guide](https://dandysoftware.com/getting_started/quick_start/) for more steps.
19
+
20
+ ### Documentation
21
+
22
+ Check out the [Dandy Website](https://dandysoftware.com) for documentation including tutorials and more.
23
+
24
+ ### Command Line
25
+
26
+ The `dandy` command runs the coding assistant against the current project. Run it from your project root (where `dandy_settings.py` lives). If no `dandy_settings.py` exists, dandy will instead load settings from a `dandy.json` file found in `.dandy/dandy.json` in the current directory, or `~/.config/dandy/dandy.json` as a last resort before creating a settings file for you.
27
+
28
+ ```bash
29
+ dandy # start an interactive agentic loop
30
+ dandy "fix the failing tests" # run one request, then exit
31
+ dandy -h # show usage
32
+ ```
33
+
34
+ Inside the interactive session every message goes to the coding agent. `/clear` resets the conversation, and pressing `escape` twice (or `/quit`) exits the loop. Use the one-shot form when you only want a single answer without entering the REPL.
35
+
36
+ The CLI's model is set with `CLI_CONFIG` in `dandy_settings.py`, e.g. `CLI_CONFIG = {'CODING': 'DEFAULT'}` points the coding agent at the `DEFAULT` entry in `LLM_CONFIGS`.
37
+
38
+ The same settings can be expressed as JSON when you don't want a Python module. Keys are case-insensitive at the top level (`llm_configs` === `LLM_CONFIGS`), so a minimal `.dandy/dandy.json` looks like:
39
+
40
+ ```json
41
+ {
42
+ "llm_configs": {
43
+ "default": {
44
+ "host": "api.example.com",
45
+ "api_key": "your-key",
46
+ "model": "your-model"
47
+ }
48
+ },
49
+ "cli_config": {"coding": "default"}
50
+ }
51
+ ```
52
+
@@ -0,0 +1,86 @@
1
+ from dandy.application.agent.agent import Agent
2
+ from dandy.application.bot.bot import Bot
3
+ from dandy.application.decision.decision import Decision
4
+ from dandy.infrastructure.cache.memory.cache import MemoryCache
5
+ from dandy.infrastructure.cache.memory.decorators import cache_to_memory
6
+ from dandy.infrastructure.cache.sqlite.cache import SqliteCache
7
+ from dandy.infrastructure.cache.sqlite.decorators import cache_to_sqlite
8
+ from dandy.infrastructure.cache.key import generate_cache_key
9
+ from dandy.shared.exceptions import DandyCriticalError, DandyError, DandyRecoverableError
10
+ from dandy.infrastructure.future.process import process_to_future
11
+ from dandy.domain.intel.intel import BaseIntel, BaseListIntel, DefaultIntel
12
+ from dandy.domain.message.message import MessageHistory
13
+ from dandy.domain.prompt.prompt import Prompt
14
+ from dandy.domain.request.options import LlmOptions
15
+ from dandy.infrastructure.recorder.decorators import (
16
+ recorder_to_html_file,
17
+ recorder_to_json_file,
18
+ recorder_to_markdown_file,
19
+ )
20
+ from dandy.infrastructure.recorder.recorder import Recorder
21
+ from dandy.domain.tool.tool import BaseTool
22
+ from dandy.domain.skill.skill import BaseSkill
23
+
24
+ __all__ = [
25
+ 'Agent',
26
+ 'BaseIntel',
27
+ 'BaseListIntel',
28
+ 'BaseSkill',
29
+ 'BaseTool',
30
+ 'Bot',
31
+ 'Dandy',
32
+ 'DandyCriticalError',
33
+ 'DandyError',
34
+ 'DandyRecoverableError',
35
+ 'Decision',
36
+ 'DefaultIntel',
37
+ 'LlmOptions',
38
+ 'MemoryCache',
39
+ 'MessageHistory',
40
+ 'Prompt',
41
+ 'Recorder',
42
+ 'SqliteCache',
43
+ 'cache_to_memory',
44
+ 'cache_to_sqlite',
45
+ 'generate_cache_key',
46
+ 'process_to_future',
47
+ 'recorder_to_html_file',
48
+ 'recorder_to_json_file',
49
+ 'recorder_to_markdown_file',
50
+ ]
51
+
52
+
53
+ class Dandy:
54
+ """The public Dandy API as a single namespace.
55
+
56
+ Every public object is reachable as an attribute of this class, and it is
57
+ the identical object the flat import gives (``Dandy.Bot`` is
58
+ ``dandy.Bot`` — nothing is wrapped or re-created). ``from dandy import
59
+ Dandy`` is a one-import entry point with full discoverability:
60
+ ``Dandy.<tab>`` lists the whole API.
61
+ """
62
+
63
+ Agent = Agent
64
+ BaseIntel = BaseIntel
65
+ BaseListIntel = BaseListIntel
66
+ BaseSkill = BaseSkill
67
+ BaseTool = BaseTool
68
+ Bot = Bot
69
+ DandyCriticalError = DandyCriticalError
70
+ DandyError = DandyError
71
+ DandyRecoverableError = DandyRecoverableError
72
+ Decision = Decision
73
+ DefaultIntel = DefaultIntel
74
+ LlmOptions = LlmOptions
75
+ MemoryCache = MemoryCache
76
+ MessageHistory = MessageHistory
77
+ Prompt = Prompt
78
+ Recorder = Recorder
79
+ SqliteCache = SqliteCache
80
+ cache_to_memory = cache_to_memory
81
+ cache_to_sqlite = cache_to_sqlite
82
+ generate_cache_key = generate_cache_key
83
+ process_to_future = process_to_future
84
+ recorder_to_html_file = recorder_to_html_file
85
+ recorder_to_json_file = recorder_to_json_file
86
+ recorder_to_markdown_file = recorder_to_markdown_file
@@ -0,0 +1,294 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from pydantic import ValidationError
6
+
7
+ from dandy.application.agent.exceptions import AgentCriticalError, AgentRecoverableError
8
+ from dandy.application.agent.intel import AgentSession, AgentTurnIntel
9
+ from dandy.application.agent.prompts import continuation_prompt
10
+ from dandy.shared.cancellation import stop_signal
11
+ from dandy.shared.exceptions import RUN_STOPPED_MESSAGE, RunStoppedError
12
+ from dandy.domain.message.compaction import (
13
+ COMPACTION_TARGET_RATIO,
14
+ COMPACTION_TRIGGER_RATIO,
15
+ DEFAULT_CONTEXT_WINDOW,
16
+ compact_message_history,
17
+ drop_incomplete_tool_round,
18
+ )
19
+ from dandy.domain.message.message import MessageHistory
20
+ from dandy.domain.prompt.prompt import Prompt
21
+ from dandy.domain.skill.skill import to_skill_instances
22
+ from dandy.domain.tokens.estimate import get_estimated_token_count_for_string
23
+ from dandy.domain.tool.beat import StoryBeat
24
+
25
+ if TYPE_CHECKING:
26
+ from dandy.application.bot.bot import Bot
27
+ from dandy.domain.intel.typing import IntelType
28
+ from dandy.domain.skill.skill import SkillType
29
+ from dandy.domain.tool.tool import ToolType
30
+
31
+ # A turn is capped at this many tool rounds by default: a model that keeps
32
+ # requesting tools without ever giving a final answer must not burn the API
33
+ # budget indefinitely. Two hundred rounds covers a thorough
34
+ # reproduce-then-fix-then-verify workflow while still bounding a model stuck
35
+ # in a degenerate tool-call loop. Pass max_tool_rounds=None to a specific
36
+ # Agent to disable the cap.
37
+ AGENT_MAX_TOOL_ROUNDS_DEFAULT = 200
38
+
39
+
40
+ class Agent:
41
+ """A stateful agent built around a Bot, designed to be looped.
42
+
43
+ A turn is one exchange: a message goes in, the agent works (possibly across
44
+ many tool rounds), and a final answer comes out; the conversation
45
+ accumulates in the agent's history. run drives turns until the agent's
46
+ result signals the request is done or the turn budget is exhausted.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ bot: Bot,
52
+ tools: list[ToolType] | None = None,
53
+ skills: list[SkillType] | None = None,
54
+ max_turns: int | None = None,
55
+ max_tool_rounds: int | None = AGENT_MAX_TOOL_ROUNDS_DEFAULT,
56
+ intel_class: type[IntelType] | None = None,
57
+ ) -> None:
58
+ self.bot = bot
59
+ self.tools = tools
60
+ self.skills = to_skill_instances(skills)
61
+ self.max_turns = max_turns
62
+ self.max_tool_rounds = max_tool_rounds
63
+ self.intel_class = intel_class or AgentTurnIntel
64
+ self.history = MessageHistory()
65
+ self.max_context_tokens = self._resolve_context_size()
66
+ self.last_request: str | None = None
67
+
68
+ if self.skills:
69
+ self.bot.llm.skills.register(self.skills)
70
+ self.bot.llm.append_system_prompt(self.bot.llm.skills.system_prompt_section())
71
+
72
+ def _resolve_context_size(self) -> int:
73
+ """The bot's LLM config ``CONTEXT_SIZE``, or a safe default."""
74
+ context_size = self.bot.llm.config.context_size
75
+ return context_size if context_size > 0 else DEFAULT_CONTEXT_WINDOW
76
+
77
+ def turn(
78
+ self,
79
+ message: Prompt | str | None = None,
80
+ progress_callback: Callable[[StoryBeat], None] | None = None,
81
+ verbose_callback: Callable[[str], None] | None = None,
82
+ tool_call_callback: Callable[[str, dict[str, object]], None] | None = None,
83
+ ) -> IntelType:
84
+ """Run one exchange: prepare, compact, work through any tool rounds, return the answer.
85
+
86
+ The conversation accumulates in ``self.history`` across turns. When
87
+ ``tool_call_callback`` is given, it is called once per executed tool
88
+ call with the tool name and its validated keyword arguments.
89
+ """
90
+ if stop_signal.is_set():
91
+ raise RunStoppedError(RUN_STOPPED_MESSAGE)
92
+
93
+ self.last_request = self._message_as_str(message)
94
+ message = self._prepare_message(message, progress_callback, verbose_callback)
95
+ self._compact_history_for_next_message(message, progress_callback)
96
+ self._ensure_history_fits(message)
97
+
98
+ return self.bot.llm.tools.prompt_to_intel(
99
+ prompt=message,
100
+ tools=self.tools,
101
+ intel_class=self.intel_class,
102
+ message_history=self.history,
103
+ replace_message_history=True,
104
+ progress_callback=progress_callback,
105
+ verbose_callback=verbose_callback,
106
+ tool_call_callback=tool_call_callback,
107
+ max_tool_iterations=self.max_tool_rounds,
108
+ )
109
+
110
+ def _prepare_message(
111
+ self,
112
+ message: Prompt | str | None,
113
+ _progress_callback: Callable[[StoryBeat], None] | None = None,
114
+ _verbose_callback: Callable[[str], None] | None = None,
115
+ ) -> Prompt | str | None:
116
+ """Subclass hook to shape the incoming message before it goes to the bot."""
117
+ return message
118
+
119
+ def _ensure_history_fits(self, message: Prompt | str | None) -> None:
120
+ """Fail clearly when compaction could not bring the conversation under the window.
121
+
122
+ Compaction drops everything it can drop and then gives up without
123
+ raising; when what remains still exceeds the whole context window no
124
+ valid request can be built, so callers see this instead of an opaque
125
+ provider 400.
126
+ """
127
+ total_token_count = self.history.estimated_token_count + self._incoming_token_count(message)
128
+
129
+ if total_token_count <= self.max_context_tokens:
130
+ return
131
+
132
+ error_message = (
133
+ f'The conversation ({self.history.estimated_token_count} tokens) plus your '
134
+ f'message still exceeds the {self.max_context_tokens} token context window '
135
+ 'after dropping everything droppable. Clear the conversation and retry, '
136
+ 'with a shorter message if possible.'
137
+ )
138
+ raise AgentRecoverableError(error_message)
139
+
140
+ def run(
141
+ self,
142
+ request: Prompt | str,
143
+ is_done: Callable[[IntelType], bool] | None = None,
144
+ progress_callback: Callable[[StoryBeat], None] | None = None,
145
+ verbose_callback: Callable[[str], None] | None = None,
146
+ tool_call_callback: Callable[[str, dict[str, object]], None] | None = None,
147
+ ) -> IntelType:
148
+ """Drive turns until the request is complete or the turn budget is exhausted.
149
+
150
+ The first turn receives the request; later turns receive a
151
+ continuation nudge. Completion comes from ``is_done(result)`` when
152
+ given, otherwise from a ``done`` field on the turn result.
153
+ """
154
+ if is_done is None and not self._has_done_field():
155
+ message = (
156
+ f'Agent.run cannot tell when the request is complete: '
157
+ f'{self.intel_class.__name__} has no `done` field and no `is_done` '
158
+ f'callback was given.'
159
+ )
160
+ raise AgentCriticalError(message)
161
+
162
+ result = self.turn(
163
+ request,
164
+ progress_callback=progress_callback,
165
+ verbose_callback=verbose_callback,
166
+ tool_call_callback=tool_call_callback,
167
+ )
168
+ turn_count = 1
169
+
170
+ while not self._is_done(result, is_done):
171
+ if stop_signal.is_set():
172
+ raise RunStoppedError(RUN_STOPPED_MESSAGE)
173
+
174
+ if self.max_turns is not None and turn_count >= self.max_turns:
175
+ message = (
176
+ f'Agent gave up after {self.max_turns} turns without the request '
177
+ f'being complete.'
178
+ )
179
+ raise AgentRecoverableError(message)
180
+
181
+ result = self.turn(
182
+ continuation_prompt(),
183
+ progress_callback=progress_callback,
184
+ verbose_callback=verbose_callback,
185
+ tool_call_callback=tool_call_callback,
186
+ )
187
+ turn_count += 1
188
+
189
+ return result
190
+
191
+ def clear(self) -> None:
192
+ """Reset the conversation history and the bot."""
193
+ self.history = MessageHistory()
194
+ self.last_request = None
195
+ self.bot.reset()
196
+
197
+ def save_session(self) -> AgentSession:
198
+ """Snapshot the conversation and LLM context for later loading.
199
+
200
+ A crash mid tool round can leave the history ending on a tool-call
201
+ declaration whose results never arrived; that round is invalid on
202
+ the wire, so it is dropped from the live history before the snapshot
203
+ and the next turn continues from a well-formed conversation.
204
+ """
205
+ drop_incomplete_tool_round(self.history)
206
+
207
+ return AgentSession(
208
+ messages=list(self.history.messages),
209
+ last_request=self.last_request,
210
+ model=self.bot.llm.config.model,
211
+ config=self.bot.llm.config.name,
212
+ token_count=self.history.estimated_token_count,
213
+ )
214
+
215
+ def load_session(self, session: AgentSession) -> None:
216
+ """Replace the conversation with a saved snapshot and reset the bot."""
217
+ self.history = MessageHistory(messages=list(session.messages))
218
+ self.last_request = session.last_request
219
+ self.bot.reset()
220
+
221
+ def save_session_json(self) -> str:
222
+ """The conversation snapshot as a JSON string for external storage."""
223
+ return self.save_session().model_dump_json()
224
+
225
+ def load_session_json(self, session_json: str) -> None:
226
+ """Replace the conversation with a snapshot from save_session_json."""
227
+ try:
228
+ session = AgentSession.model_validate_json(session_json)
229
+ except ValidationError:
230
+ message = 'The session JSON is not a valid agent session snapshot.'
231
+ raise AgentRecoverableError(message) from None
232
+
233
+ self.load_session(session)
234
+
235
+ def _has_done_field(self) -> bool:
236
+ return 'done' in getattr(self.intel_class, 'model_fields', {})
237
+
238
+ def _is_done(self, result: IntelType, is_done: Callable[[IntelType], bool] | None) -> bool:
239
+ if is_done is not None:
240
+ return bool(is_done(result))
241
+
242
+ return bool(getattr(result, 'done', False))
243
+
244
+ def _message_as_str(self, message: Prompt | str | None) -> str | None:
245
+ if message is None:
246
+ return None
247
+
248
+ if isinstance(message, Prompt):
249
+ return message.to_str()
250
+
251
+ return message
252
+
253
+ def _incoming_token_count(self, message: Prompt | str | None) -> int:
254
+ if message is None:
255
+ return 0
256
+
257
+ if isinstance(message, Prompt):
258
+ return message.estimated_token_count
259
+
260
+ return get_estimated_token_count_for_string(message)
261
+
262
+ def _compaction_target_token_count(self) -> int:
263
+ """A fraction of the max context, reserving headroom for output."""
264
+ return max(1, int(self.max_context_tokens * COMPACTION_TARGET_RATIO))
265
+
266
+ def _compaction_trigger_token_count(self) -> int:
267
+ """The higher fraction at which compaction kicks in, above the target."""
268
+ return max(
269
+ self._compaction_target_token_count() + 1,
270
+ int(self.max_context_tokens * COMPACTION_TRIGGER_RATIO),
271
+ )
272
+
273
+ def _compact_history_for_next_message(
274
+ self,
275
+ message: Prompt | str | None,
276
+ progress_callback: Callable[[StoryBeat], None] | None = None,
277
+ ) -> None:
278
+ incoming_token_count = self._incoming_token_count(message)
279
+ target_token_count = max(1, self._compaction_target_token_count() - incoming_token_count)
280
+ trigger_token_count = self._compaction_trigger_token_count() - incoming_token_count
281
+ current_token_count = self.history.estimated_token_count
282
+
283
+ if current_token_count <= trigger_token_count:
284
+ return
285
+
286
+ if progress_callback is not None:
287
+ progress_callback(
288
+ StoryBeat(
289
+ description='Compacting my thoughts into a more manageable size',
290
+ kind='compaction',
291
+ )
292
+ )
293
+
294
+ compact_message_history(history=self.history, max_context_tokens=target_token_count)
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+
3
+ from dandy.shared.files import file_exists, read_from_file
4
+ from dandy.domain.prompt.prompt import Prompt
5
+
6
+
7
+ def read_agents_md(agents_md_path: Path) -> str | None:
8
+ """The project's AGENTS.md content, or None when the file is missing or blank."""
9
+ if not file_exists(agents_md_path):
10
+ return None
11
+
12
+ agents_md = read_from_file(agents_md_path).strip()
13
+
14
+ return agents_md or None
15
+
16
+
17
+ def agents_md_section(agents_md: str) -> Prompt:
18
+ """The AGENTS.md content as a Project Instructions section for agent guidelines."""
19
+ return (
20
+ Prompt()
21
+ .sub_heading('Project Instructions')
22
+ .text(
23
+ 'The project provides the following instructions for AI coding agents. '
24
+ 'Follow them whenever they apply to your work.'
25
+ )
26
+ .text(agents_md)
27
+ )
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from dandy.application.agent.agent import AGENT_MAX_TOOL_ROUNDS_DEFAULT, Agent
6
+ from dandy.application.agent.cli.agents_md import read_agents_md
7
+ from dandy.application.agent.cli.intelligence.bots.coding_bot import CodingBot
8
+ from dandy.application.agent.cli.intelligence.bots.init_bot import InitBot
9
+ from dandy.application.agent.cli.intelligence.prompts import init_prompt
10
+ from dandy.application.agent.session import session
11
+ from dandy.application.agent.intelligence.tools import AGENT_TOOLS
12
+ from dandy.application.agent.intelligence.tools.question import QuestionHandler, QuestionTool
13
+ from dandy.domain.intel.intel import DefaultIntel
14
+ from dandy.domain.message.message import MessageContent
15
+
16
+ if TYPE_CHECKING:
17
+ from dandy.domain.tool.beat import StoryBeat
18
+
19
+
20
+ class CliAgent(Agent):
21
+ """Multi-turn coding agent that keeps the conversation across turns."""
22
+
23
+ def __init__(self, ask_user: QuestionHandler | None = None) -> None:
24
+ super().__init__(
25
+ bot=CodingBot(),
26
+ tools=[*AGENT_TOOLS, QuestionTool(ask_user=ask_user)],
27
+ intel_class=DefaultIntel,
28
+ )
29
+
30
+ def reload_agents_md(self) -> str:
31
+ """Re-read the project AGENTS.md and sync it into the system prompt.
32
+
33
+ Rebuilds the bot's guidelines, the connector's system message, and the
34
+ running conversation's system message (when one exists), so a
35
+ regenerated AGENTS.md applies to the next turn without a /new.
36
+ Returns the refreshed system prompt text.
37
+ """
38
+ self.bot.guidelines = self.bot.build_guidelines()
39
+ system_prompt = self.bot.llm.rebuild_system_prompt()
40
+
41
+ if self.history.has_system_message:
42
+ self.history.messages[0].content = [MessageContent(type='text', text=system_prompt)]
43
+
44
+ return system_prompt
45
+
46
+ def run_init_pass(
47
+ self,
48
+ progress_callback: Callable[[StoryBeat], None] | None = None,
49
+ verbose_callback: Callable[[str], None] | None = None,
50
+ ) -> str | None:
51
+ """Run the AGENTS.md generation pass in a fresh InitBot conversation.
52
+
53
+ The bot explores the project and writes (or refreshes) AGENTS.md at
54
+ the project root, reading the existing file from the prompt when one
55
+ exists. On success the coding agent's system prompt is reloaded from
56
+ the file. Returns the bot's summary text, or None when the file was
57
+ not written.
58
+ """
59
+ init_bot = InitBot()
60
+
61
+ result_intel: DefaultIntel = init_bot.llm.tools.prompt_to_intel(
62
+ prompt=init_prompt(read_agents_md(session.agents_md_path)),
63
+ tools=AGENT_TOOLS,
64
+ intel_class=DefaultIntel,
65
+ progress_callback=progress_callback,
66
+ verbose_callback=verbose_callback,
67
+ max_tool_iterations=AGENT_MAX_TOOL_ROUNDS_DEFAULT,
68
+ )
69
+
70
+ if not session.agents_md_path.is_file():
71
+ return None
72
+
73
+ self.reload_agents_md()
74
+
75
+ return result_intel.text
76
+
77
+ def clear(self) -> None:
78
+ super().clear()
@@ -0,0 +1,35 @@
1
+ from dandy.application.agent.cli.agents_md import agents_md_section, read_agents_md
2
+ from dandy.application.agent.cli.intelligence.prompts import coding_guidelines_prompt
3
+ from dandy.application.agent.session import session
4
+ from dandy.application.agent.intelligence.tools import AGENT_TOOLS
5
+ from dandy.application.bot.bot import Bot
6
+ from dandy.shared.settings.load import get_cli_llm_config
7
+ from dandy.domain.intel.intel import DefaultIntel
8
+ from dandy.domain.prompt.prompt import Prompt
9
+
10
+
11
+ class CodingBot(Bot):
12
+ role = 'Senior Software Engineer'
13
+
14
+ def __post_init__(self) -> None:
15
+ super().__post_init__()
16
+ self.llm_config = get_cli_llm_config('CODING')
17
+ self.guidelines = self.build_guidelines()
18
+
19
+ def build_guidelines(self) -> Prompt:
20
+ guidelines = coding_guidelines_prompt()
21
+ agents_md = read_agents_md(session.agents_md_path)
22
+
23
+ if agents_md is not None:
24
+ guidelines = guidelines.prompt(agents_md_section(agents_md))
25
+
26
+ return guidelines
27
+
28
+ task = (
29
+ 'Implement the user coding request by editing the project. '
30
+ 'Use the available tools to explore and modify the code, '
31
+ 'then give a short summary of what you changed and why.'
32
+ )
33
+
34
+ def process(self, user_input: Prompt | str) -> DefaultIntel:
35
+ return self.llm.tools.prompt_to_intel(prompt=user_input, tools=AGENT_TOOLS)
@@ -0,0 +1,23 @@
1
+ from dandy.application.agent.cli.intelligence.prompts import init_guidelines_prompt
2
+ from dandy.application.agent.intelligence.tools import AGENT_TOOLS
3
+ from dandy.application.bot.bot import Bot
4
+ from dandy.shared.settings.load import get_cli_llm_config
5
+ from dandy.domain.intel.intel import DefaultIntel
6
+ from dandy.domain.prompt.prompt import Prompt
7
+
8
+
9
+ class InitBot(Bot):
10
+ role = 'Project Documentation Engineer'
11
+
12
+ def __post_init__(self) -> None:
13
+ super().__post_init__()
14
+ self.llm_config = get_cli_llm_config('CODING')
15
+ self.guidelines = init_guidelines_prompt()
16
+
17
+ task = (
18
+ 'Create or refresh the project AGENTS.md file: concise, factual guidance '
19
+ 'for AI coding agents working in this repository.'
20
+ )
21
+
22
+ def process(self, user_input: Prompt | str) -> DefaultIntel:
23
+ return self.llm.tools.prompt_to_intel(prompt=user_input, tools=AGENT_TOOLS)