multacd 2.0.0b4__tar.gz → 2.0.0b6__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.0b6}/.github/workflows/release.yml +1 -1
  2. {multacd-2.0.0b4 → multacd-2.0.0b6}/CHANGELOG.md +14 -0
  3. {multacd-2.0.0b4 → multacd-2.0.0b6}/PKG-INFO +1 -1
  4. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/agent_loop.py +5 -0
  5. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/providers/openai_compat.py +24 -1
  6. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/version.py +1 -1
  7. {multacd-2.0.0b4 → multacd-2.0.0b6}/memory/context.py +21 -0
  8. {multacd-2.0.0b4 → multacd-2.0.0b6}/pyproject.toml +1 -1
  9. {multacd-2.0.0b4 → multacd-2.0.0b6}/scripts/install.sh +1 -1
  10. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/integration/test_agent_loop.py +23 -0
  11. {multacd-2.0.0b4 → multacd-2.0.0b6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {multacd-2.0.0b4 → multacd-2.0.0b6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {multacd-2.0.0b4 → multacd-2.0.0b6}/.github/pull_request_template.md +0 -0
  14. {multacd-2.0.0b4 → multacd-2.0.0b6}/.github/workflows/test.yml +0 -0
  15. {multacd-2.0.0b4 → multacd-2.0.0b6}/.gitignore +0 -0
  16. {multacd-2.0.0b4 → multacd-2.0.0b6}/CONTRIBUTING.md +0 -0
  17. {multacd-2.0.0b4 → multacd-2.0.0b6}/DESIGN.md +0 -0
  18. {multacd-2.0.0b4 → multacd-2.0.0b6}/LICENSE +0 -0
  19. {multacd-2.0.0b4 → multacd-2.0.0b6}/README.md +0 -0
  20. {multacd-2.0.0b4 → multacd-2.0.0b6}/briefing/__init__.py +0 -0
  21. {multacd-2.0.0b4 → multacd-2.0.0b6}/briefing/generator.py +0 -0
  22. {multacd-2.0.0b4 → multacd-2.0.0b6}/briefing/privacy.py +0 -0
  23. {multacd-2.0.0b4 → multacd-2.0.0b6}/briefing/sources/__init__.py +0 -0
  24. {multacd-2.0.0b4 → multacd-2.0.0b6}/briefing/sources/git_source.py +0 -0
  25. {multacd-2.0.0b4 → multacd-2.0.0b6}/briefing/sources/news_source.py +0 -0
  26. {multacd-2.0.0b4 → multacd-2.0.0b6}/briefing/sources/todo_source.py +0 -0
  27. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/__init__.py +0 -0
  28. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/clipboard.py +0 -0
  29. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/codebase.py +0 -0
  30. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/config.py +0 -0
  31. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/llm_client.py +0 -0
  32. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/mode_manager.py +0 -0
  33. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/permissions.py +0 -0
  34. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/plugin_loader.py +0 -0
  35. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/prompt_composer.py +0 -0
  36. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/providers/__init__.py +0 -0
  37. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/providers/__main__.py +0 -0
  38. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/providers/anthropic.py +0 -0
  39. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/research/__init__.py +0 -0
  40. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/research/bus.py +0 -0
  41. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/research/orchestrator.py +0 -0
  42. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/research/query_generator.py +0 -0
  43. {multacd-2.0.0b4 → multacd-2.0.0b6}/core/updater.py +0 -0
  44. {multacd-2.0.0b4 → multacd-2.0.0b6}/daemon/__init__.py +0 -0
  45. {multacd-2.0.0b4 → multacd-2.0.0b6}/daemon/ipc.py +0 -0
  46. {multacd-2.0.0b4 → multacd-2.0.0b6}/daemon/process.py +0 -0
  47. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/configuration.md +0 -0
  48. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/index.md +0 -0
  49. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/install.md +0 -0
  50. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/mkdocs.yml +0 -0
  51. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/modes/coding.md +0 -0
  52. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/modes/personal.md +0 -0
  53. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/modes/research.md +0 -0
  54. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/plugins.md +0 -0
  55. {multacd-2.0.0b4 → multacd-2.0.0b6}/docs/quickstart.md +0 -0
  56. {multacd-2.0.0b4 → multacd-2.0.0b6}/main.py +0 -0
  57. {multacd-2.0.0b4 → multacd-2.0.0b6}/memory/__init__.py +0 -0
  58. {multacd-2.0.0b4 → multacd-2.0.0b6}/memory/store.py +0 -0
  59. {multacd-2.0.0b4 → multacd-2.0.0b6}/multacd.spec +0 -0
  60. {multacd-2.0.0b4 → multacd-2.0.0b6}/plan/PLAN-phase1.md +0 -0
  61. {multacd-2.0.0b4 → multacd-2.0.0b6}/plan/PLAN-phase2.md +0 -0
  62. {multacd-2.0.0b4 → multacd-2.0.0b6}/plugins/README.md +0 -0
  63. {multacd-2.0.0b4 → multacd-2.0.0b6}/plugins/example_plugin.py +0 -0
  64. {multacd-2.0.0b4 → multacd-2.0.0b6}/requirements.txt +0 -0
  65. {multacd-2.0.0b4 → multacd-2.0.0b6}/roadmap/ROADMAP.md +0 -0
  66. {multacd-2.0.0b4 → multacd-2.0.0b6}/scheduler/__init__.py +0 -0
  67. {multacd-2.0.0b4 → multacd-2.0.0b6}/scheduler/engine.py +0 -0
  68. {multacd-2.0.0b4 → multacd-2.0.0b6}/scheduler/jobs/__init__.py +0 -0
  69. {multacd-2.0.0b4 → multacd-2.0.0b6}/scheduler/jobs/briefing_job.py +0 -0
  70. {multacd-2.0.0b4 → multacd-2.0.0b6}/scheduler/jobs/research_job.py +0 -0
  71. {multacd-2.0.0b4 → multacd-2.0.0b6}/scripts/install.ps1 +0 -0
  72. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/__init__.py +0 -0
  73. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/__main__.py +0 -0
  74. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/base.py +0 -0
  75. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/brave.py +0 -0
  76. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/duckduckgo.py +0 -0
  77. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/exa.py +0 -0
  78. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/serpapi.py +0 -0
  79. {multacd-2.0.0b4 → multacd-2.0.0b6}/search_providers/tavily.py +0 -0
  80. {multacd-2.0.0b4 → multacd-2.0.0b6}/soul.md +0 -0
  81. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/__init__.py +0 -0
  82. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/conftest.py +0 -0
  83. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/integration/__init__.py +0 -0
  84. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/test_selftests.py +0 -0
  85. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/__init__.py +0 -0
  86. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_binary.py +0 -0
  87. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_config.py +0 -0
  88. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_docs.py +0 -0
  89. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_entrypoint.py +0 -0
  90. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_github.py +0 -0
  91. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_installer.py +0 -0
  92. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_permissions.py +0 -0
  93. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_plugin_loader.py +0 -0
  94. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_privacy_filter.py +0 -0
  95. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_search_providers/__init__.py +0 -0
  96. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_search_providers/test_tavily.py +0 -0
  97. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_tools/__init__.py +0 -0
  98. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_tools/test_bash.py +0 -0
  99. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_tools/test_read_file.py +0 -0
  100. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_tools/test_web_scrape.py +0 -0
  101. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_tools/test_write_file.py +0 -0
  102. {multacd-2.0.0b4 → multacd-2.0.0b6}/tests/unit/test_updater.py +0 -0
  103. {multacd-2.0.0b4 → multacd-2.0.0b6}/tg/__init__.py +0 -0
  104. {multacd-2.0.0b4 → multacd-2.0.0b6}/tg/access_control.py +0 -0
  105. {multacd-2.0.0b4 → multacd-2.0.0b6}/tg/agent.py +0 -0
  106. {multacd-2.0.0b4 → multacd-2.0.0b6}/tg/bot.py +0 -0
  107. {multacd-2.0.0b4 → multacd-2.0.0b6}/tg/file_handler.py +0 -0
  108. {multacd-2.0.0b4 → multacd-2.0.0b6}/tg/formatter.py +0 -0
  109. {multacd-2.0.0b4 → multacd-2.0.0b6}/tg/handlers.py +0 -0
  110. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/__init__.py +0 -0
  111. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/agent/__init__.py +0 -0
  112. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/agent/ask.py +0 -0
  113. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/agent/skill.py +0 -0
  114. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/agent/task.py +0 -0
  115. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/agent/todo_write.py +0 -0
  116. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/code/__init__.py +0 -0
  117. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/code/lint_python.py +0 -0
  118. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/code/run_python.py +0 -0
  119. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/code/run_tests.py +0 -0
  120. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/codebase/__init__.py +0 -0
  121. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/codebase/scan_codebase.py +0 -0
  122. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/common.py +0 -0
  123. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/__init__.py +0 -0
  124. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/apply_patch.py +0 -0
  125. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/delete_file.py +0 -0
  126. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/edit_file.py +0 -0
  127. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/glob.py +0 -0
  128. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/grep.py +0 -0
  129. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/list_dir.py +0 -0
  130. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/move_file.py +0 -0
  131. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/multi_edit.py +0 -0
  132. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/read_file.py +0 -0
  133. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/read_many_files.py +0 -0
  134. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/filesystem/write_file.py +0 -0
  135. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/__init__.py +0 -0
  136. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/_helper.py +0 -0
  137. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_add.py +0 -0
  138. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_branch.py +0 -0
  139. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_checkout.py +0 -0
  140. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_commit.py +0 -0
  141. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_diff.py +0 -0
  142. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_log.py +0 -0
  143. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_merge.py +0 -0
  144. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_pull.py +0 -0
  145. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_push.py +0 -0
  146. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/git/git_status.py +0 -0
  147. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/memory/__init__.py +0 -0
  148. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/memory/forget.py +0 -0
  149. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/memory/recall.py +0 -0
  150. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/memory/remember.py +0 -0
  151. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/__init__.py +0 -0
  152. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/cancel_job.py +0 -0
  153. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/daemon_status.py +0 -0
  154. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/generate_briefing.py +0 -0
  155. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/get_jobs.py +0 -0
  156. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/schedule_job.py +0 -0
  157. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/send_telegram.py +0 -0
  158. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/personal/user_manager.py +0 -0
  159. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/registry.py +0 -0
  160. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/research/__init__.py +0 -0
  161. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/research/deep_research.py +0 -0
  162. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/research/export_research.py +0 -0
  163. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/research/quick_research.py +0 -0
  164. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/research/web_scrape.py +0 -0
  165. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/research/web_search.py +0 -0
  166. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/shell/__init__.py +0 -0
  167. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/shell/bash.py +0 -0
  168. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/streaming.py +0 -0
  169. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/web/__init__.py +0 -0
  170. {multacd-2.0.0b4 → multacd-2.0.0b6}/tools/web/web_fetch.py +0 -0
  171. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/__init__.py +0 -0
  172. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/app.py +0 -0
  173. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/icons.py +0 -0
  174. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/screens/__init__.py +0 -0
  175. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/screens/main_screen.py +0 -0
  176. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/screens/setup_wizard.py +0 -0
  177. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/themes/__init__.py +0 -0
  178. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/themes/__main__.py +0 -0
  179. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/tokens.py +0 -0
  180. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/__init__.py +0 -0
  181. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/chat_panel.py +0 -0
  182. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/confirm_dialog.py +0 -0
  183. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/diff_viewer.py +0 -0
  184. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/file_tree.py +0 -0
  185. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/info_panel.py +0 -0
  186. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/input_bar.py +0 -0
  187. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/model_selector.py +0 -0
  188. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/permission_popup.py +0 -0
  189. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/slash_palette.py +0 -0
  190. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/sources_panel.py +0 -0
  191. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/status_bar.py +0 -0
  192. {multacd-2.0.0b4 → multacd-2.0.0b6}/tui/widgets/thinking_bar.py +0 -0
  193. {multacd-2.0.0b4 → multacd-2.0.0b6}/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.0b6"
105
105
  ```
106
106
 
107
107
  Windows (PowerShell):
@@ -2,6 +2,20 @@
2
2
 
3
3
  Format: Keep a Changelog. Versi: Semantic Versioning.
4
4
 
5
+ ## [2.0.0b6] - 2026-09-12
6
+
7
+ Beta v2 keenam — fix mode lupa ingatan (#26): system message di-refresh
8
+ tiap turn dari composer (ala opencode: system disusun ulang tiap request).
9
+ Ganti /code /research /personal sekarang beneran ganti persona yang dibaca
10
+ LLM. Install: `pip install "multacd==2.0.0b6"`.
11
+
12
+ ## [2.0.0b5] - 2026-09-12
13
+
14
+ Beta v2 kelima — fix chat 2-turn 400 (#25): `add_assistant_tool_calls`
15
+ omit key `tool_calls` saat tanpa tool + `_sanitize_messages` safety net
16
+ di openai_compat. Belajar dari opencode `transform.ts` (part kosong
17
+ tidak pernah dikirim). Install: `pip install "multacd==2.0.0b5"`.
18
+
5
19
  ## [2.0.0b4] - 2026-09-12
6
20
 
7
21
  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.0b6
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
@@ -174,6 +174,11 @@ async def run_agent(
174
174
 
175
175
  if not context.get_messages():
176
176
  context.add_message("system", _system_prompt())
177
+ elif composer is not None:
178
+ # #26: mode ganti → composer baru, tapi history masih bawa
179
+ # system lama. Refresh tiap turn (ala opencode: system disusun
180
+ # ulang tiap request) biar LLM baca persona yang benar.
181
+ context.set_system(_system_prompt())
177
182
  context.add_message("user", user_input)
178
183
 
179
184
  iteration = 0
@@ -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.0b6" # 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,
@@ -72,6 +81,18 @@ class ConversationContext:
72
81
  ],
73
82
  })
74
83
 
84
+ def set_system(self, content: str) -> None:
85
+ """Ganti system message dengan prompt terbaru (issue #26).
86
+
87
+ Ganti mode (/code→/research) cuma update composer — history
88
+ masih bawa system lama kalau tidak di-refresh. Panggil tiap
89
+ turn sebelum kirim ke LLM (lihat core/agent_loop.py).
90
+ """
91
+ if self._messages and self._messages[0].get("role") == "system":
92
+ self._messages[0] = {"role": "system", "content": content}
93
+ else:
94
+ self._messages.insert(0, {"role": "system", "content": content})
95
+
75
96
  def get_messages(self) -> list[dict[str, Any]]:
76
97
  """Return semua messages (copy list — jangan mutasi hasilnya)."""
77
98
  return list(self._messages)
@@ -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.0b6"
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.0b6\""
82
82
  echo ""
83
83
  echo " 3. Jalankan: multacd"
84
84
  echo ""
@@ -13,6 +13,7 @@ import asyncio
13
13
 
14
14
  from core.agent_loop import AgentDone, AgentError, run_agent
15
15
  from core.llm_client import StreamDone, ToolCallRequest
16
+ from core.prompt_composer import PromptComposer
16
17
  from memory.context import ConversationContext
17
18
 
18
19
 
@@ -34,6 +35,10 @@ def test_plain_text_no_tool(sample_config, fake_llm_factory):
34
35
  events = _run(_drain(run_agent("hi", ctx, sample_config, llm_client=fake)))
35
36
  assert isinstance(events[-1], AgentDone)
36
37
  assert "halo" in events[-1].text
38
+ # #25: assistant tanpa tool TIDAK boleh bawa key tool_calls
39
+ # (provider strict 400 kalau array kosong ikut terkirim).
40
+ assts = [m for m in ctx.get_messages() if m.get("role") == "assistant"]
41
+ assert assts and all("tool_calls" not in m for m in assts), assts
37
42
 
38
43
 
39
44
  def test_single_auto_tool_runs(sample_config, fake_llm_factory, tmp_path):
@@ -51,6 +56,24 @@ def test_single_auto_tool_runs(sample_config, fake_llm_factory, tmp_path):
51
56
  assert "a.txt" in blob or "a.txt" in events[-1].text
52
57
 
53
58
 
59
+ def test_mode_switch_refreshes_system_prompt(sample_config, fake_llm_factory):
60
+ # #26: ganti mode harus ganti system yang dilihat LLM berikutnya.
61
+ composer = PromptComposer(soul="S")
62
+ composer.update_mode("MODE-A")
63
+ ctx = ConversationContext()
64
+ fake = fake_llm_factory([StreamDone("a", [])])
65
+ _run(_drain(run_agent("hi", ctx, sample_config, llm_client=fake,
66
+ composer=composer)))
67
+ assert "MODE-A" in ctx.get_messages()[0]["content"]
68
+ composer.update_mode("MODE-B")
69
+ fake = fake_llm_factory([StreamDone("b", [])])
70
+ _run(_drain(run_agent("halo lagi", ctx, sample_config, llm_client=fake,
71
+ composer=composer)))
72
+ sys_msgs = [m for m in ctx.get_messages() if m["role"] == "system"]
73
+ assert len(sys_msgs) == 1 and "MODE-B" in sys_msgs[0]["content"], sys_msgs
74
+ assert "MODE-B" in fake.seen[-1][0]["content"]
75
+
76
+
54
77
  def test_max_iterations_stops_gracefully(sample_config, fake_llm_factory):
55
78
  sample_config.max_tool_iterations = 2
56
79
  script = [StreamDone("", [ToolCallRequest(id=f"c{i}", name="list_dir",
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