multacd 2.0.0b4__tar.gz → 2.0.0b5__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 (193) hide show
  1. {multacd-2.0.0b4 → multacd-2.0.0b5}/.github/workflows/release.yml +1 -1
  2. {multacd-2.0.0b4 → multacd-2.0.0b5}/CHANGELOG.md +7 -0
  3. {multacd-2.0.0b4 → multacd-2.0.0b5}/PKG-INFO +1 -1
  4. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/providers/openai_compat.py +24 -1
  5. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/version.py +1 -1
  6. {multacd-2.0.0b4 → multacd-2.0.0b5}/memory/context.py +9 -0
  7. {multacd-2.0.0b4 → multacd-2.0.0b5}/pyproject.toml +1 -1
  8. {multacd-2.0.0b4 → multacd-2.0.0b5}/scripts/install.sh +1 -1
  9. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/integration/test_agent_loop.py +4 -0
  10. {multacd-2.0.0b4 → multacd-2.0.0b5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {multacd-2.0.0b4 → multacd-2.0.0b5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {multacd-2.0.0b4 → multacd-2.0.0b5}/.github/pull_request_template.md +0 -0
  13. {multacd-2.0.0b4 → multacd-2.0.0b5}/.github/workflows/test.yml +0 -0
  14. {multacd-2.0.0b4 → multacd-2.0.0b5}/.gitignore +0 -0
  15. {multacd-2.0.0b4 → multacd-2.0.0b5}/CONTRIBUTING.md +0 -0
  16. {multacd-2.0.0b4 → multacd-2.0.0b5}/DESIGN.md +0 -0
  17. {multacd-2.0.0b4 → multacd-2.0.0b5}/LICENSE +0 -0
  18. {multacd-2.0.0b4 → multacd-2.0.0b5}/README.md +0 -0
  19. {multacd-2.0.0b4 → multacd-2.0.0b5}/briefing/__init__.py +0 -0
  20. {multacd-2.0.0b4 → multacd-2.0.0b5}/briefing/generator.py +0 -0
  21. {multacd-2.0.0b4 → multacd-2.0.0b5}/briefing/privacy.py +0 -0
  22. {multacd-2.0.0b4 → multacd-2.0.0b5}/briefing/sources/__init__.py +0 -0
  23. {multacd-2.0.0b4 → multacd-2.0.0b5}/briefing/sources/git_source.py +0 -0
  24. {multacd-2.0.0b4 → multacd-2.0.0b5}/briefing/sources/news_source.py +0 -0
  25. {multacd-2.0.0b4 → multacd-2.0.0b5}/briefing/sources/todo_source.py +0 -0
  26. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/__init__.py +0 -0
  27. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/agent_loop.py +0 -0
  28. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/clipboard.py +0 -0
  29. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/codebase.py +0 -0
  30. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/config.py +0 -0
  31. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/llm_client.py +0 -0
  32. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/mode_manager.py +0 -0
  33. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/permissions.py +0 -0
  34. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/plugin_loader.py +0 -0
  35. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/prompt_composer.py +0 -0
  36. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/providers/__init__.py +0 -0
  37. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/providers/__main__.py +0 -0
  38. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/providers/anthropic.py +0 -0
  39. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/research/__init__.py +0 -0
  40. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/research/bus.py +0 -0
  41. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/research/orchestrator.py +0 -0
  42. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/research/query_generator.py +0 -0
  43. {multacd-2.0.0b4 → multacd-2.0.0b5}/core/updater.py +0 -0
  44. {multacd-2.0.0b4 → multacd-2.0.0b5}/daemon/__init__.py +0 -0
  45. {multacd-2.0.0b4 → multacd-2.0.0b5}/daemon/ipc.py +0 -0
  46. {multacd-2.0.0b4 → multacd-2.0.0b5}/daemon/process.py +0 -0
  47. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/configuration.md +0 -0
  48. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/index.md +0 -0
  49. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/install.md +0 -0
  50. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/mkdocs.yml +0 -0
  51. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/modes/coding.md +0 -0
  52. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/modes/personal.md +0 -0
  53. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/modes/research.md +0 -0
  54. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/plugins.md +0 -0
  55. {multacd-2.0.0b4 → multacd-2.0.0b5}/docs/quickstart.md +0 -0
  56. {multacd-2.0.0b4 → multacd-2.0.0b5}/main.py +0 -0
  57. {multacd-2.0.0b4 → multacd-2.0.0b5}/memory/__init__.py +0 -0
  58. {multacd-2.0.0b4 → multacd-2.0.0b5}/memory/store.py +0 -0
  59. {multacd-2.0.0b4 → multacd-2.0.0b5}/multacd.spec +0 -0
  60. {multacd-2.0.0b4 → multacd-2.0.0b5}/plan/PLAN-phase1.md +0 -0
  61. {multacd-2.0.0b4 → multacd-2.0.0b5}/plan/PLAN-phase2.md +0 -0
  62. {multacd-2.0.0b4 → multacd-2.0.0b5}/plugins/README.md +0 -0
  63. {multacd-2.0.0b4 → multacd-2.0.0b5}/plugins/example_plugin.py +0 -0
  64. {multacd-2.0.0b4 → multacd-2.0.0b5}/requirements.txt +0 -0
  65. {multacd-2.0.0b4 → multacd-2.0.0b5}/roadmap/ROADMAP.md +0 -0
  66. {multacd-2.0.0b4 → multacd-2.0.0b5}/scheduler/__init__.py +0 -0
  67. {multacd-2.0.0b4 → multacd-2.0.0b5}/scheduler/engine.py +0 -0
  68. {multacd-2.0.0b4 → multacd-2.0.0b5}/scheduler/jobs/__init__.py +0 -0
  69. {multacd-2.0.0b4 → multacd-2.0.0b5}/scheduler/jobs/briefing_job.py +0 -0
  70. {multacd-2.0.0b4 → multacd-2.0.0b5}/scheduler/jobs/research_job.py +0 -0
  71. {multacd-2.0.0b4 → multacd-2.0.0b5}/scripts/install.ps1 +0 -0
  72. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/__init__.py +0 -0
  73. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/__main__.py +0 -0
  74. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/base.py +0 -0
  75. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/brave.py +0 -0
  76. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/duckduckgo.py +0 -0
  77. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/exa.py +0 -0
  78. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/serpapi.py +0 -0
  79. {multacd-2.0.0b4 → multacd-2.0.0b5}/search_providers/tavily.py +0 -0
  80. {multacd-2.0.0b4 → multacd-2.0.0b5}/soul.md +0 -0
  81. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/__init__.py +0 -0
  82. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/conftest.py +0 -0
  83. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/integration/__init__.py +0 -0
  84. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/test_selftests.py +0 -0
  85. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/__init__.py +0 -0
  86. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_binary.py +0 -0
  87. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_config.py +0 -0
  88. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_docs.py +0 -0
  89. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_entrypoint.py +0 -0
  90. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_github.py +0 -0
  91. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_installer.py +0 -0
  92. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_permissions.py +0 -0
  93. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_plugin_loader.py +0 -0
  94. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_privacy_filter.py +0 -0
  95. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_search_providers/__init__.py +0 -0
  96. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_search_providers/test_tavily.py +0 -0
  97. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_tools/__init__.py +0 -0
  98. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_tools/test_bash.py +0 -0
  99. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_tools/test_read_file.py +0 -0
  100. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_tools/test_web_scrape.py +0 -0
  101. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_tools/test_write_file.py +0 -0
  102. {multacd-2.0.0b4 → multacd-2.0.0b5}/tests/unit/test_updater.py +0 -0
  103. {multacd-2.0.0b4 → multacd-2.0.0b5}/tg/__init__.py +0 -0
  104. {multacd-2.0.0b4 → multacd-2.0.0b5}/tg/access_control.py +0 -0
  105. {multacd-2.0.0b4 → multacd-2.0.0b5}/tg/agent.py +0 -0
  106. {multacd-2.0.0b4 → multacd-2.0.0b5}/tg/bot.py +0 -0
  107. {multacd-2.0.0b4 → multacd-2.0.0b5}/tg/file_handler.py +0 -0
  108. {multacd-2.0.0b4 → multacd-2.0.0b5}/tg/formatter.py +0 -0
  109. {multacd-2.0.0b4 → multacd-2.0.0b5}/tg/handlers.py +0 -0
  110. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/__init__.py +0 -0
  111. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/agent/__init__.py +0 -0
  112. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/agent/ask.py +0 -0
  113. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/agent/skill.py +0 -0
  114. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/agent/task.py +0 -0
  115. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/agent/todo_write.py +0 -0
  116. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/code/__init__.py +0 -0
  117. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/code/lint_python.py +0 -0
  118. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/code/run_python.py +0 -0
  119. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/code/run_tests.py +0 -0
  120. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/codebase/__init__.py +0 -0
  121. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/codebase/scan_codebase.py +0 -0
  122. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/common.py +0 -0
  123. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/__init__.py +0 -0
  124. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/apply_patch.py +0 -0
  125. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/delete_file.py +0 -0
  126. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/edit_file.py +0 -0
  127. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/glob.py +0 -0
  128. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/grep.py +0 -0
  129. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/list_dir.py +0 -0
  130. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/move_file.py +0 -0
  131. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/multi_edit.py +0 -0
  132. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/read_file.py +0 -0
  133. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/read_many_files.py +0 -0
  134. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/filesystem/write_file.py +0 -0
  135. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/__init__.py +0 -0
  136. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/_helper.py +0 -0
  137. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_add.py +0 -0
  138. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_branch.py +0 -0
  139. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_checkout.py +0 -0
  140. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_commit.py +0 -0
  141. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_diff.py +0 -0
  142. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_log.py +0 -0
  143. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_merge.py +0 -0
  144. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_pull.py +0 -0
  145. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_push.py +0 -0
  146. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/git/git_status.py +0 -0
  147. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/memory/__init__.py +0 -0
  148. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/memory/forget.py +0 -0
  149. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/memory/recall.py +0 -0
  150. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/memory/remember.py +0 -0
  151. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/__init__.py +0 -0
  152. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/cancel_job.py +0 -0
  153. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/daemon_status.py +0 -0
  154. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/generate_briefing.py +0 -0
  155. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/get_jobs.py +0 -0
  156. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/schedule_job.py +0 -0
  157. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/send_telegram.py +0 -0
  158. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/personal/user_manager.py +0 -0
  159. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/registry.py +0 -0
  160. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/research/__init__.py +0 -0
  161. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/research/deep_research.py +0 -0
  162. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/research/export_research.py +0 -0
  163. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/research/quick_research.py +0 -0
  164. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/research/web_scrape.py +0 -0
  165. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/research/web_search.py +0 -0
  166. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/shell/__init__.py +0 -0
  167. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/shell/bash.py +0 -0
  168. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/streaming.py +0 -0
  169. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/web/__init__.py +0 -0
  170. {multacd-2.0.0b4 → multacd-2.0.0b5}/tools/web/web_fetch.py +0 -0
  171. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/__init__.py +0 -0
  172. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/app.py +0 -0
  173. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/icons.py +0 -0
  174. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/screens/__init__.py +0 -0
  175. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/screens/main_screen.py +0 -0
  176. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/screens/setup_wizard.py +0 -0
  177. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/themes/__init__.py +0 -0
  178. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/themes/__main__.py +0 -0
  179. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/tokens.py +0 -0
  180. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/__init__.py +0 -0
  181. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/chat_panel.py +0 -0
  182. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/confirm_dialog.py +0 -0
  183. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/diff_viewer.py +0 -0
  184. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/file_tree.py +0 -0
  185. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/info_panel.py +0 -0
  186. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/input_bar.py +0 -0
  187. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/model_selector.py +0 -0
  188. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/permission_popup.py +0 -0
  189. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/slash_palette.py +0 -0
  190. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/sources_panel.py +0 -0
  191. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/status_bar.py +0 -0
  192. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/thinking_bar.py +0 -0
  193. {multacd-2.0.0b4 → multacd-2.0.0b5}/tui/widgets/tool_activity.py +0 -0
@@ -101,7 +101,7 @@ jobs:
101
101
 
102
102
  ```bash
103
103
  pkg install -y python git
104
- pip install "multacd==2.0.0b4"
104
+ pip install "multacd==2.0.0b5"
105
105
  ```
106
106
 
107
107
  Windows (PowerShell):
@@ -2,6 +2,13 @@
2
2
 
3
3
  Format: Keep a Changelog. Versi: Semantic Versioning.
4
4
 
5
+ ## [2.0.0b5] - 2026-09-12
6
+
7
+ Beta v2 kelima — fix chat 2-turn 400 (#25): `add_assistant_tool_calls`
8
+ omit key `tool_calls` saat tanpa tool + `_sanitize_messages` safety net
9
+ di openai_compat. Belajar dari opencode `transform.ts` (part kosong
10
+ tidak pernah dikirim). Install: `pip install "multacd==2.0.0b5"`.
11
+
5
12
  ## [2.0.0b4] - 2026-09-12
6
13
 
7
14
  Beta v2 keempat — Termux harusnya tembus sekarang (#24): `pydantic`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: multacd
3
- Version: 2.0.0b4
3
+ Version: 2.0.0b5
4
4
  Summary: Agentic TUI for coding, research, and personal automation
5
5
  Project-URL: Homepage, https://github.com/prototypeall850-creator/multacd
6
6
  Project-URL: Issues, https://github.com/prototypeall850-creator/multacd/issues
@@ -136,6 +136,29 @@ def map_http_error(status: int, body: str, model: str) -> ProviderError:
136
136
  return ProviderError(f"Provider error HTTP {status}. {snippet}".strip())
137
137
 
138
138
 
139
+ def _sanitize_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
140
+ """Bersihkan messages sebelum kirim (ala opencode transform.ts).
141
+
142
+ - `tool_calls: []` / None → key di-drop (provider strict 400 kalau
143
+ array kosong ikut terkirim — issue #25).
144
+ - `content: None` tanpa tool_calls → `""` (beberapa gateway nolak null).
145
+ - `content: None` + tool_calls non-kosong → dibiarkan (valid OpenAI).
146
+ Pure function — input tidak dimutasi.
147
+ """
148
+ clean: list[dict[str, Any]] = []
149
+ for m in messages:
150
+ if not isinstance(m, dict):
151
+ continue
152
+ msg = dict(m)
153
+ tcs = msg.get("tool_calls")
154
+ if not tcs:
155
+ msg.pop("tool_calls", None)
156
+ if msg.get("content") is None and not msg.get("tool_calls"):
157
+ msg["content"] = ""
158
+ clean.append(msg)
159
+ return clean
160
+
161
+
139
162
  async def stream_chat(spec: ProviderSpec, api_key: str,
140
163
  messages: list[dict[str, Any]],
141
164
  tools: list[dict[str, Any]] | None,
@@ -146,7 +169,7 @@ async def stream_chat(spec: ProviderSpec, api_key: str,
146
169
  """Kirim chat completions streaming. `client` injeksi buat test."""
147
170
  body: dict[str, Any] = {
148
171
  "model": spec.model,
149
- "messages": messages,
172
+ "messages": _sanitize_messages(messages),
150
173
  "stream": True,
151
174
  "stream_options": {"include_usage": True},
152
175
  "max_tokens": max_tokens,
@@ -8,7 +8,7 @@ Saat bump versi (Step 10 → 1.0.0): ubah pyproject + FALLBACK di sini.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- FALLBACK = "2.0.0b4" # beta v2; samakan dengan pyproject.toml
11
+ FALLBACK = "2.0.0b5" # beta v2; samakan dengan pyproject.toml
12
12
 
13
13
 
14
14
  def get_version() -> str:
@@ -56,9 +56,18 @@ class ConversationContext:
56
56
  (mis. ToolCallRequest dari core.llm_client). Wajib ada agar
57
57
  provider tidak 400 pada turn berikutnya (tool_call_id harus
58
58
  merujuk ke assistant message sebelumnya).
59
+
60
+ Belajar dari opencode (AI SDK): tool adalah content part —
61
+ part kosong tidak pernah dikirim. Jadi kalau `calls` kosong,
62
+ key `tool_calls` di-OMIT total (bukan `[]`): provider strict
63
+ (400 `tool_calls must be non-empty if provided`) nolak array
64
+ kosong. Lihat issue #25.
59
65
  """
60
66
  import json as _json
61
67
 
68
+ if not calls:
69
+ self._messages.append({"role": "assistant", "content": text or ""})
70
+ return
62
71
  self._messages.append({
63
72
  "role": "assistant",
64
73
  "content": text or None,
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "multacd"
11
- version = "2.0.0b4"
11
+ version = "2.0.0b5"
12
12
  description = "Agentic TUI for coding, research, and personal automation"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.10"
@@ -78,7 +78,7 @@ termux_pip_guide() {
78
78
  echo " (ringan: tanpa kompilasi Rust — dependensi wheel murni)"
79
79
  echo ""
80
80
  echo " 2. Install multacd:"
81
- echo " pip install \"multacd==2.0.0b4\""
81
+ echo " pip install \"multacd==2.0.0b5\""
82
82
  echo ""
83
83
  echo " 3. Jalankan: multacd"
84
84
  echo ""
@@ -34,6 +34,10 @@ def test_plain_text_no_tool(sample_config, fake_llm_factory):
34
34
  events = _run(_drain(run_agent("hi", ctx, sample_config, llm_client=fake)))
35
35
  assert isinstance(events[-1], AgentDone)
36
36
  assert "halo" in events[-1].text
37
+ # #25: assistant tanpa tool TIDAK boleh bawa key tool_calls
38
+ # (provider strict 400 kalau array kosong ikut terkirim).
39
+ assts = [m for m in ctx.get_messages() if m.get("role") == "assistant"]
40
+ assert assts and all("tool_calls" not in m for m in assts), assts
37
41
 
38
42
 
39
43
  def test_single_auto_tool_runs(sample_config, fake_llm_factory, tmp_path):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes