kolega-code 0.6.0__tar.gz → 0.7.0__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 (273) hide show
  1. {kolega_code-0.6.0 → kolega_code-0.7.0}/PKG-INFO +13 -3
  2. {kolega_code-0.6.0 → kolega_code-0.7.0}/README.md +7 -2
  3. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/concepts/tools.md +8 -2
  4. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/configuration/environment-variables.md +15 -0
  5. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/configuration/settings-and-api-keys.mdx +15 -3
  6. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/index.mdx +3 -2
  7. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/__init__.py +1 -1
  8. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/__init__.py +1 -1
  9. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_agent_tools_inventory.py +1 -0
  10. kolega_code-0.7.0/kolega_code/agent/tests/tool_backend/test_web_search_tool.py +366 -0
  11. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/__init__.py +83 -0
  12. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/base.py +58 -0
  13. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/duckduckgo.py +56 -0
  14. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/errors.py +23 -0
  15. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/firecrawl.py +117 -0
  16. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/models.py +28 -0
  17. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/searxng.py +62 -0
  18. kolega_code-0.7.0/kolega_code/agent/tool_backend/search_backends/tavily.py +70 -0
  19. kolega_code-0.7.0/kolega_code/agent/tool_backend/web_search_tool.py +114 -0
  20. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tools.py +29 -0
  21. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/app.py +99 -1
  22. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/config.py +36 -0
  23. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/settings.py +13 -0
  24. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_app.py +84 -0
  25. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_cli_config.py +67 -0
  26. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_settings.py +35 -0
  27. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/config.py +12 -0
  28. {kolega_code-0.6.0 → kolega_code-0.7.0}/pyproject.toml +6 -1
  29. {kolega_code-0.6.0 → kolega_code-0.7.0}/uv.lock +829 -2
  30. {kolega_code-0.6.0 → kolega_code-0.7.0}/.env.example +0 -0
  31. {kolega_code-0.6.0 → kolega_code-0.7.0}/.github/workflows/ci.yml +0 -0
  32. {kolega_code-0.6.0 → kolega_code-0.7.0}/.github/workflows/docs.yml +0 -0
  33. {kolega_code-0.6.0 → kolega_code-0.7.0}/.github/workflows/release.yml +0 -0
  34. {kolega_code-0.6.0 → kolega_code-0.7.0}/.gitignore +0 -0
  35. {kolega_code-0.6.0 → kolega_code-0.7.0}/CONTRIBUTING.md +0 -0
  36. {kolega_code-0.6.0 → kolega_code-0.7.0}/LICENSE +0 -0
  37. {kolega_code-0.6.0 → kolega_code-0.7.0}/RELEASING.md +0 -0
  38. {kolega_code-0.6.0 → kolega_code-0.7.0}/SECURITY.md +0 -0
  39. {kolega_code-0.6.0 → kolega_code-0.7.0}/conftest.py +0 -0
  40. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/.gitignore +0 -0
  41. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/README.md +0 -0
  42. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/astro.config.mjs +0 -0
  43. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/package-lock.json +0 -0
  44. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/package.json +0 -0
  45. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/public/favicon.svg +0 -0
  46. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/assets/demo.gif +0 -0
  47. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/assets/kolega-dark.svg +0 -0
  48. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/assets/kolega-light.svg +0 -0
  49. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/cli/ask.md +0 -0
  50. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/cli/doctor.md +0 -0
  51. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/cli/overview.md +0 -0
  52. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/cli/sessions.md +0 -0
  53. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/concepts/agents.md +0 -0
  54. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/concepts/how-it-works.md +0 -0
  55. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/configuration/providers-and-models.mdx +0 -0
  56. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/getting-started/installation.mdx +0 -0
  57. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/getting-started/quick-start.mdx +0 -0
  58. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/gigacode.mdx +0 -0
  59. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/hooks.md +0 -0
  60. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/skills.mdx +0 -0
  61. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/tui/composer.md +0 -0
  62. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/tui/interface.md +0 -0
  63. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/tui/modes.md +0 -0
  64. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/tui/sessions-and-resume.md +0 -0
  65. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content/docs/tui/slash-commands.md +0 -0
  66. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/content.config.ts +0 -0
  67. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/src/styles/brand.css +0 -0
  68. {kolega_code-0.6.0 → kolega_code-0.7.0}/docs/tsconfig.json +0 -0
  69. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/baseagent.py +0 -0
  70. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/browseragent.py +0 -0
  71. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/coder.py +0 -0
  72. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/common.py +0 -0
  73. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/compression.py +0 -0
  74. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/context.py +0 -0
  75. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/conversation.py +0 -0
  76. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/generalagent.py +0 -0
  77. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/investigationagent.py +0 -0
  78. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/__init__.py +0 -0
  79. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/budget.py +0 -0
  80. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/errors.py +0 -0
  81. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/executor.py +0 -0
  82. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/guide.py +0 -0
  83. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/journal.py +0 -0
  84. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/runtime.py +0 -0
  85. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/tests/__init__.py +0 -0
  86. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/tests/test_orchestration.py +0 -0
  87. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/orchestration/types.py +0 -0
  88. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/planningagent.py +0 -0
  89. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_provider.py +0 -0
  90. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/auxiliary/compression/summary.system.md +0 -0
  91. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/auxiliary/compression/summary.user.md.j2 +0 -0
  92. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/auxiliary/terminal/output_summary.system.md +0 -0
  93. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/auxiliary/terminal/safety.system.md +0 -0
  94. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/auxiliary/tools/think_hard.system.md +0 -0
  95. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/extensions/cli/planning_questions.md +0 -0
  96. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/extensions/cli/shared_task_list.md +0 -0
  97. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/extensions/skills/catalog.md.j2 +0 -0
  98. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/agents/browser.md.j2 +0 -0
  99. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/agents/coder_cli.md.j2 +0 -0
  100. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/agents/general.md.j2 +0 -0
  101. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/agents/investigation.md.j2 +0 -0
  102. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/agents/planning.md.j2 +0 -0
  103. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/includes/agents_md_instructions.md +0 -0
  104. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/includes/environment_variables/workspace_env_vars.md +0 -0
  105. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/includes/frontend_guidance.md +0 -0
  106. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/includes/starter_templates/expo.md +0 -0
  107. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/includes/starter_templates/html_website.md +0 -0
  108. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/includes/starter_templates/mern_stack.md +0 -0
  109. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/system/includes/starter_templates/react_vite_shadcdn.md +0 -0
  110. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/user_tasks/cli/implement_plan.md.j2 +0 -0
  111. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompt_templates/user_tasks/cli/init_agents.md.j2 +0 -0
  112. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/prompts.py +0 -0
  113. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/__init__.py +0 -0
  114. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/__init__.py +0 -0
  115. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_anthropic_token_counting.py +0 -0
  116. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_billing_openai_cache.py +0 -0
  117. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_client.py +0 -0
  118. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_dashscope_mapping.py +0 -0
  119. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_error_boundary.py +0 -0
  120. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_exceptions.py +0 -0
  121. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_google_thought_signature.py +0 -0
  122. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_google_tool_signature_live.py +0 -0
  123. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_instrumented_client.py +0 -0
  124. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_instrumented_client_integration.py +0 -0
  125. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_langfuse_normalization.py +0 -0
  126. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_live_providers.py +0 -0
  127. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_model_specs.py +0 -0
  128. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_openai_cached_tokens.py +0 -0
  129. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +0 -0
  130. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_openai_message_conversion.py +0 -0
  131. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_openai_token_counting.py +0 -0
  132. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_thinking_effort.py +0 -0
  133. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/llm/test_tool_execution_ids.py +0 -0
  134. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/__init__.py +0 -0
  135. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/test_browser.py +0 -0
  136. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/test_browser_parity.py +0 -0
  137. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/test_file_system.py +0 -0
  138. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/test_sandbox_terminal_input.py +0 -0
  139. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/test_terminal.py +0 -0
  140. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/test_terminal_buffer.py +0 -0
  141. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/services/test_terminal_state_serializer.py +0 -0
  142. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_base_agent.py +0 -0
  143. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_coder_attachments.py +0 -0
  144. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_coder_prompt_extensions.py +0 -0
  145. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_commands.py +0 -0
  146. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_duplicate_tool_results.py +0 -0
  147. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_empty_message_handling.py +0 -0
  148. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_event_emitter.py +0 -0
  149. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_general_agent.py +0 -0
  150. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_gigacode_gating.py +0 -0
  151. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_hooks_integration.py +0 -0
  152. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_html.py +0 -0
  153. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_parallel_tool_calls.py +0 -0
  154. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_permissions.py +0 -0
  155. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_planning_agent.py +0 -0
  156. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_prompt_provider.py +0 -0
  157. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_prompts.py +0 -0
  158. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_tool_registry.py +0 -0
  159. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/test_tools.py +0 -0
  160. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  161. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_agent_tool.py +0 -0
  162. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_base_tool.py +0 -0
  163. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_browser_tool.py +0 -0
  164. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_build_tool.py +0 -0
  165. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_create_file_tool.py +0 -0
  166. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_edit_preview.py +0 -0
  167. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_glob_tool.py +0 -0
  168. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +0 -0
  169. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +0 -0
  170. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_read_file_tool.py +0 -0
  171. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +0 -0
  172. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +0 -0
  173. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +0 -0
  174. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +0 -0
  175. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_terminal_tool.py +0 -0
  176. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +0 -0
  177. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +0 -0
  178. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +0 -0
  179. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tests/tool_backend/test_workflow_tool.py +0 -0
  180. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/agent_tool.py +0 -0
  181. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/base_tool.py +0 -0
  182. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/browser_tool.py +0 -0
  183. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/build_tool.py +0 -0
  184. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/create_file_tool.py +0 -0
  185. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/edit_preview.py +0 -0
  186. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/glob_tool.py +0 -0
  187. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/list_directory_tool.py +0 -0
  188. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/memory_tool.py +0 -0
  189. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/read_file_tool.py +0 -0
  190. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/replace_entire_file_tool.py +0 -0
  191. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/replace_lines_tool.py +0 -0
  192. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/search_and_replace_tool.py +0 -0
  193. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/search_codebase_tool.py +0 -0
  194. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/streaming_tool.py +0 -0
  195. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/terminal_tool.py +0 -0
  196. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/think_hard_tool.py +0 -0
  197. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/web_fetch_tool.py +0 -0
  198. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/tool_backend/workflow_tool.py +0 -0
  199. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/agent/utils/commands.py +0 -0
  200. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/__init__.py +0 -0
  201. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/connection.py +0 -0
  202. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/file_index.py +0 -0
  203. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/main.py +0 -0
  204. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/mentions.py +0 -0
  205. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/messages.py +0 -0
  206. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/provider_registry.py +0 -0
  207. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/session_store.py +0 -0
  208. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/skills.py +0 -0
  209. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/slash_commands.py +0 -0
  210. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/__init__.py +0 -0
  211. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_connection.py +0 -0
  212. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_file_index.py +0 -0
  213. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_main.py +0 -0
  214. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_mentions.py +0 -0
  215. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_session_store.py +0 -0
  216. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_skills.py +0 -0
  217. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_slash_commands.py +0 -0
  218. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_themes.py +0 -0
  219. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/tests/test_updater.py +0 -0
  220. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/theme.py +0 -0
  221. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/cli/updater.py +0 -0
  222. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/events.py +0 -0
  223. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/__init__.py +0 -0
  224. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/backends.py +0 -0
  225. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/config.py +0 -0
  226. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/dispatcher.py +0 -0
  227. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/events.py +0 -0
  228. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/matcher.py +0 -0
  229. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/outcome.py +0 -0
  230. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/tests/__init__.py +0 -0
  231. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/hooks/tests/test_hooks.py +0 -0
  232. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/client.py +0 -0
  233. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/exceptions.py +0 -0
  234. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/instrumented_client.py +0 -0
  235. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/models.py +0 -0
  236. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/providers/__init__.py +0 -0
  237. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/providers/anthropic.py +0 -0
  238. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/providers/base.py +0 -0
  239. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/providers/google.py +0 -0
  240. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/providers/models.py +0 -0
  241. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/providers/openai.py +0 -0
  242. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/ratelimit.py +0 -0
  243. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/specs.py +0 -0
  244. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/llm/tool_execution_ids.py +0 -0
  245. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/models/__init__.py +0 -0
  246. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/models/sandbox_terminal_state.py +0 -0
  247. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/permissions.py +0 -0
  248. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/runtime.py +0 -0
  249. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/README.md +0 -0
  250. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/__init__.py +0 -0
  251. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/async_filesystem.py +0 -0
  252. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/base.py +0 -0
  253. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/browser.py +0 -0
  254. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/event_loop.py +0 -0
  255. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/filesystem.py +0 -0
  256. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/local.py +0 -0
  257. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/serializer.py +0 -0
  258. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/terminal.py +0 -0
  259. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/sandbox/utils.py +0 -0
  260. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/services/base.py +0 -0
  261. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/services/browser.py +0 -0
  262. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/services/file_system.py +0 -0
  263. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/services/html.py +0 -0
  264. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/services/terminal.py +0 -0
  265. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/services/terminal_buffer.py +0 -0
  266. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/tools/__init__.py +0 -0
  267. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/tools/core.py +0 -0
  268. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/tools/definitions.py +0 -0
  269. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/tools/registry.py +0 -0
  270. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/tools/tests/__init__.py +0 -0
  271. {kolega_code-0.6.0 → kolega_code-0.7.0}/kolega_code/tools/tests/test_definitions.py +0 -0
  272. {kolega_code-0.6.0 → kolega_code-0.7.0}/run_tests.sh +0 -0
  273. {kolega_code-0.6.0 → kolega_code-0.7.0}/scripts/install-kolega-code.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kolega-code
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Local-first AI coding agent for the terminal
5
5
  Project-URL: Homepage, https://kolega-ai.github.io/kolega-code/
6
6
  Project-URL: Documentation, https://kolega-ai.github.io/kolega-code/
@@ -43,9 +43,13 @@ Classifier: Topic :: Utilities
43
43
  Requires-Python: >=3.11
44
44
  Requires-Dist: anthropic==0.101.0
45
45
  Requires-Dist: beautifulsoup4==4.14.3
46
+ Requires-Dist: ddgs==9.14.4
47
+ Requires-Dist: firecrawl-py==4.28.2
46
48
  Requires-Dist: google-genai==2.0.1
49
+ Requires-Dist: httpx==0.28.1
47
50
  Requires-Dist: jinja2==3.1.6
48
51
  Requires-Dist: langfuse==3.14.5
52
+ Requires-Dist: lxml-html-clean==0.4.4
49
53
  Requires-Dist: nest-asyncio==1.6.0
50
54
  Requires-Dist: openai==1.109.1
51
55
  Requires-Dist: packaging==25.0
@@ -55,6 +59,7 @@ Requires-Dist: psutil==7.2.2
55
59
  Requires-Dist: pydantic==2.13.4
56
60
  Requires-Dist: python-dotenv==1.2.2
57
61
  Requires-Dist: pyyaml==6.0.3
62
+ Requires-Dist: tavily-python==0.7.26
58
63
  Requires-Dist: tenacity==9.1.4
59
64
  Requires-Dist: textual==8.2.5
60
65
  Requires-Dist: tiktoken==0.12.0
@@ -103,8 +108,9 @@ sessions, settings, and API keys stay on your own machine.
103
108
  and applies precise edits.
104
109
  - **Runs commands.** Executes shell commands and watches their output in a dedicated terminal view.
105
110
  - **Plans before it builds.** Plan mode investigates and proposes; Build mode executes.
106
- - **Browses the web.** A built-in browser agent (powered by Playwright) fetches pages and
107
- interacts with sites when a task needs it.
111
+ - **Searches and browses the web.** The agent can search for relevant pages, fetch URLs
112
+ directly, or use the built-in browser agent (powered by Playwright) when a task needs
113
+ site interaction.
108
114
  - **Dispatches sub-agents.** Hands work off to specialized agents and tracks their activity live.
109
115
  - **Orchestrates workflows.** With [gigacode](https://kolega-ai.github.io/kolega-code/gigacode/),
110
116
  it fans out many sub-agents in parallel.
@@ -187,6 +193,10 @@ Settings. Local session state lives under your platform's state directory unless
187
193
  [Configuration docs](https://kolega-ai.github.io/kolega-code/configuration/settings-and-api-keys/)
188
194
  for the full story.
189
195
 
196
+ The `web_search` tool uses DuckDuckGo by default without a key. To choose another backend,
197
+ set it in **Settings** or export `KOLEGA_CODE_WEB_SEARCH_BACKEND` as `firecrawl`, `tavily`,
198
+ or `searxng`; use `FIRECRAWL_API_KEY`, `TAVILY_API_KEY`, or `SEARXNG_BASE_URL` as needed.
199
+
190
200
  ## Requirements
191
201
 
192
202
  - **Python 3.11+**
@@ -35,8 +35,9 @@ sessions, settings, and API keys stay on your own machine.
35
35
  and applies precise edits.
36
36
  - **Runs commands.** Executes shell commands and watches their output in a dedicated terminal view.
37
37
  - **Plans before it builds.** Plan mode investigates and proposes; Build mode executes.
38
- - **Browses the web.** A built-in browser agent (powered by Playwright) fetches pages and
39
- interacts with sites when a task needs it.
38
+ - **Searches and browses the web.** The agent can search for relevant pages, fetch URLs
39
+ directly, or use the built-in browser agent (powered by Playwright) when a task needs
40
+ site interaction.
40
41
  - **Dispatches sub-agents.** Hands work off to specialized agents and tracks their activity live.
41
42
  - **Orchestrates workflows.** With [gigacode](https://kolega-ai.github.io/kolega-code/gigacode/),
42
43
  it fans out many sub-agents in parallel.
@@ -119,6 +120,10 @@ Settings. Local session state lives under your platform's state directory unless
119
120
  [Configuration docs](https://kolega-ai.github.io/kolega-code/configuration/settings-and-api-keys/)
120
121
  for the full story.
121
122
 
123
+ The `web_search` tool uses DuckDuckGo by default without a key. To choose another backend,
124
+ set it in **Settings** or export `KOLEGA_CODE_WEB_SEARCH_BACKEND` as `firecrawl`, `tavily`,
125
+ or `searxng`; use `FIRECRAWL_API_KEY`, `TAVILY_API_KEY`, or `SEARXNG_BASE_URL` as needed.
126
+
122
127
  ## Requirements
123
128
 
124
129
  - **Python 3.11+**
@@ -38,8 +38,14 @@ the [TUI](../../cli/overview/) or [`ask`](../../cli/ask/).
38
38
 
39
39
  ### Web
40
40
 
41
+ - `web_search` — search the web for relevant pages and return ranked results.
41
42
  - `web_fetch` — fetch and parse a web page without a full browser.
42
43
 
44
+ Use `web_search` when you do not already know the right URL, then follow up with
45
+ `web_fetch` to read a result in depth. The default backend is keyless DuckDuckGo;
46
+ Firecrawl, Tavily, and self-hosted SearXNG can be selected in Settings or with
47
+ environment variables.
48
+
43
49
  ### Reasoning & memory
44
50
 
45
51
  - `think_hard` — an extended-reasoning step that uses the
@@ -57,8 +63,8 @@ and `dispatch_general_agent`.
57
63
  Tools are gated by mode. In a read-only context — like [Plan mode](../../tui/modes/)
58
64
  or an investigation sub-agent — only non-mutating tools are available
59
65
  (`list_directory`, `read_entire_file`, `read_file_section`, `search_codebase`,
60
- `find_files_by_pattern`, `web_fetch`, `think_hard`, and reading memory). Editing
61
- files and running commands require Build mode's full toolset.
66
+ `find_files_by_pattern`, `web_search`, `web_fetch`, `think_hard`, and reading
67
+ memory). Editing files and running commands require Build mode's full toolset.
62
68
 
63
69
  This separation is what makes Plan mode safe to run against any codebase: the
64
70
  planning agent can look but not touch.
@@ -87,6 +87,18 @@ section.
87
87
  | `KOLEGA_CODE_STATE_DIR` | Override where settings and sessions are stored |
88
88
  | `KOLEGA_CODE_ENVIRONMENT` | Environment label attached to tracing/metadata (default `development`) |
89
89
 
90
+ ## Web search
91
+
92
+ The `web_search` tool defaults to the keyless DuckDuckGo backend. Set a backend
93
+ explicitly when you want a cloud provider or a self-hosted SearXNG instance.
94
+
95
+ | Variable | Purpose |
96
+ | --- | --- |
97
+ | `KOLEGA_CODE_WEB_SEARCH_BACKEND` | Backend for `web_search`: `duckduckgo`, `firecrawl`, `tavily`, or `searxng` |
98
+ | `FIRECRAWL_API_KEY` | Optional Firecrawl key for higher rate limits |
99
+ | `TAVILY_API_KEY` | Tavily API key |
100
+ | `SEARXNG_BASE_URL` | Base URL for a self-hosted SearXNG instance |
101
+
90
102
  ## Telemetry (Langfuse)
91
103
 
92
104
  Optional [Langfuse](https://langfuse.com/) tracing of LLM usage.
@@ -113,6 +125,9 @@ KOLEGA_CODE_PROVIDER=moonshot
113
125
  KOLEGA_CODE_MODEL=kimi-k2.7-code
114
126
  KOLEGA_CODE_THINKING_EFFORT=auto
115
127
 
128
+ # Optional: web search
129
+ KOLEGA_CODE_WEB_SEARCH_BACKEND=duckduckgo
130
+
116
131
  # Optional: Langfuse tracing
117
132
  LANGFUSE_HOST=https://us.cloud.langfuse.com
118
133
  LANGFUSE_PUBLIC_KEY=
@@ -32,6 +32,13 @@ agents (planning, building, investigation, general, browser) their own provider,
32
32
  model, and effort. Leave a role on **Default** to inherit the active model. See
33
33
  [Per-agent models](../providers-and-models/#per-agent-models).
34
34
 
35
+ The **Web Search** section configures the `web_search` tool. DuckDuckGo is the
36
+ default and needs no key. Firecrawl can also run without a key, with an optional
37
+ key for higher rate limits. Tavily requires a key, and SearXNG requires the base
38
+ URL of your self-hosted instance. The same settings can be supplied with
39
+ [`KOLEGA_CODE_WEB_SEARCH_BACKEND`, `FIRECRAWL_API_KEY`, `TAVILY_API_KEY`, and
40
+ `SEARXNG_BASE_URL`](../environment-variables/#web-search).
41
+
35
42
  ## Where settings live
36
43
 
37
44
  Settings are stored as JSON in your platform's state directory, in a file named
@@ -56,7 +63,8 @@ The file looks like this:
56
63
  "active_thinking_effort": "auto",
57
64
  "api_keys": {
58
65
  "moonshot": "sk-...",
59
- "deepseek": "sk-..."
66
+ "deepseek": "sk-...",
67
+ "tavily": "tvly-..."
60
68
  },
61
69
  "agent_models": {
62
70
  "investigation": {
@@ -64,13 +72,17 @@ The file looks like this:
64
72
  "model": "deepseek-v4-flash",
65
73
  "thinking_effort": "high"
66
74
  }
67
- }
75
+ },
76
+ "web_search_backend": "tavily",
77
+ "web_search_base_url": null
68
78
  }
69
79
  ```
70
80
 
71
81
  `agent_models` holds per-agent overrides keyed by role (`planning`, `building`,
72
82
  `investigation`, `general`, `browser`); a role that is absent inherits the active
73
83
  model. The field is optional and older `schema_version` files load unchanged.
84
+ Cloud web-search keys are stored in `api_keys` under their backend names
85
+ (`firecrawl` or `tavily`); `web_search_base_url` is used by SearXNG.
74
86
 
75
87
  <Aside type="caution" title="Keys are stored in plaintext">
76
88
  API keys are written to `settings.json` in plaintext, but the file is created with
@@ -83,7 +95,7 @@ store keys on disk at all, supply them via
83
95
 
84
96
  <FileTree>
85
97
  - kolega-code/
86
- - settings.json Provider, model, thinking effort, and API keys (0600)
98
+ - settings.json Provider, model, web search, thinking effort, and API keys (0600)
87
99
  - sessions/
88
100
  - &lt;session-id&gt;.json One file per saved session
89
101
  </FileTree>
@@ -19,8 +19,9 @@ and your sessions, settings, and API keys are stored on your own machine.
19
19
  dedicated terminal view in the UI.
20
20
  - **Plans before it builds.** A read-only **Plan mode** produces a reviewable plan
21
21
  and a shared task list; **Build mode** implements it.
22
- - **Browses the web.** A built-in browser agent (powered by Playwright) can fetch
23
- pages and interact with sites when a task needs it.
22
+ - **Searches and browses the web.** The agent can search for relevant pages,
23
+ fetch URLs directly, or use the built-in browser agent (powered by Playwright)
24
+ when a task needs site interaction.
24
25
  - **Dispatches sub-agents.** For larger tasks, the main agent can hand off to
25
26
  specialized agents (investigation, browser, coding, general) and track their
26
27
  activity live.
@@ -148,4 +148,4 @@ __all__ = [
148
148
  "parse_git_status_output",
149
149
  ]
150
150
 
151
- __version__ = "0.6.0"
151
+ __version__ = "0.7.0"
@@ -39,4 +39,4 @@ __all__ = [
39
39
  "ToolExtension",
40
40
  ]
41
41
 
42
- __version__ = "0.6.0"
42
+ __version__ = "0.7.0"
@@ -104,6 +104,7 @@ def test_investigation_agent_tools(project_path, mock_connection_manager, agent_
104
104
  "sleep",
105
105
  "think_hard",
106
106
  "web_fetch",
107
+ "web_search",
107
108
  ]
108
109
 
109
110
  assert len(tools) == len(expected_tools)
@@ -0,0 +1,366 @@
1
+ from types import SimpleNamespace
2
+ from unittest.mock import AsyncMock, Mock, patch
3
+
4
+ import httpx
5
+ import pytest
6
+
7
+ from kolega_code.agent.tool_backend.search_backends import (
8
+ DEFAULT_BACKEND,
9
+ SearchBackendError,
10
+ SearchBackendNotConfigured,
11
+ SearchBackendRateLimited,
12
+ SearchBackendUnavailable,
13
+ SearchResponse,
14
+ SearchResult,
15
+ available_backends,
16
+ backend_names,
17
+ build_search_backend,
18
+ get_backend_class,
19
+ )
20
+ from kolega_code.agent.tool_backend.search_backends.base import clamp_results
21
+ from kolega_code.agent.tool_backend.web_search_tool import WebSearchTool
22
+ from kolega_code.config import AgentConfig, ModelConfig, ModelProvider
23
+
24
+ _DUCK = "kolega_code.agent.tool_backend.search_backends.duckduckgo"
25
+ _TAVILY = "kolega_code.agent.tool_backend.search_backends.tavily"
26
+ _FIRE = "kolega_code.agent.tool_backend.search_backends.firecrawl"
27
+ _SEARX = "kolega_code.agent.tool_backend.search_backends.searxng"
28
+
29
+
30
+ # --------------------------------------------------------------------------- registry
31
+
32
+
33
+ def test_registry_lists_all_backends() -> None:
34
+ assert set(backend_names()) == {"duckduckgo", "firecrawl", "tavily", "searxng"}
35
+ # Default backend is first in the TUI option list and is keyless.
36
+ labels = available_backends()
37
+ assert labels[0][1] == DEFAULT_BACKEND == "duckduckgo"
38
+ assert get_backend_class("duckduckgo").requires_api_key is False
39
+ # Optional-key model: Firecrawl is keyless-capable but accepts a key for higher limits;
40
+ # Tavily requires one; DuckDuckGo/SearXNG take no key.
41
+ assert get_backend_class("firecrawl").requires_api_key is False
42
+ assert get_backend_class("firecrawl").accepts_api_key is True
43
+ assert get_backend_class("tavily").requires_api_key is True
44
+ assert get_backend_class("tavily").accepts_api_key is True
45
+ assert get_backend_class("duckduckgo").accepts_api_key is False
46
+ assert get_backend_class("searxng").accepts_api_key is False
47
+
48
+
49
+ def test_build_unknown_backend_raises_typed_error() -> None:
50
+ with pytest.raises(SearchBackendError, match="Unknown web_search backend"):
51
+ build_search_backend("nope")
52
+
53
+
54
+ @pytest.mark.parametrize(("requested", "expected"), [(-3, 1), (0, 1), (5, 5), (50, 10), ("bad", 5)])
55
+ def test_clamp_results(requested, expected) -> None:
56
+ assert clamp_results(requested) == expected
57
+
58
+
59
+ # ------------------------------------------------------------------------- duckduckgo
60
+
61
+
62
+ class _DummyDDGS:
63
+ def __init__(self, rows=None, exc=None):
64
+ self._rows = rows or []
65
+ self._exc = exc
66
+
67
+ def __enter__(self):
68
+ return self
69
+
70
+ def __exit__(self, *args):
71
+ return False
72
+
73
+ def text(self, query, max_results=5):
74
+ if self._exc:
75
+ raise self._exc
76
+ return self._rows
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_duckduckgo_maps_rows() -> None:
81
+ rows = [{"title": "T", "href": "https://a.test", "body": "snippet"}, {"title": "", "href": "", "body": "x"}]
82
+ with patch(f"{_DUCK}.DDGS", lambda: _DummyDDGS(rows=rows)):
83
+ response = await build_search_backend("duckduckgo").search("q", max_results=3)
84
+ assert response.backend == "duckduckgo"
85
+ # The second row has no href and is dropped.
86
+ assert [r.url for r in response.results] == ["https://a.test"]
87
+ assert response.results[0].snippet == "snippet"
88
+
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_duckduckgo_rate_limit_mapped() -> None:
92
+ from ddgs.exceptions import RatelimitException
93
+
94
+ with patch(f"{_DUCK}.DDGS", lambda: _DummyDDGS(exc=RatelimitException("429"))):
95
+ with pytest.raises(SearchBackendRateLimited):
96
+ await build_search_backend("duckduckgo").search("q")
97
+
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_duckduckgo_generic_error_unavailable() -> None:
101
+ with patch(f"{_DUCK}.DDGS", lambda: _DummyDDGS(exc=RuntimeError("boom"))):
102
+ with pytest.raises(SearchBackendUnavailable):
103
+ await build_search_backend("duckduckgo").search("q")
104
+
105
+
106
+ # ----------------------------------------------------------------------------- tavily
107
+
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_tavily_requires_key() -> None:
111
+ with pytest.raises(SearchBackendNotConfigured):
112
+ await build_search_backend("tavily", api_key=None).search("q")
113
+
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_tavily_success_with_answer() -> None:
117
+ payload = {
118
+ "answer": "the answer",
119
+ "results": [{"title": "T", "url": "https://a.test", "content": "body"}, {"url": ""}],
120
+ }
121
+ client = Mock()
122
+ client.search.return_value = payload
123
+ with patch(f"{_TAVILY}.TavilyClient", return_value=client):
124
+ response = await build_search_backend("tavily", api_key="k").search("q", max_results=2)
125
+ assert response.answer == "the answer"
126
+ assert [r.url for r in response.results] == ["https://a.test"]
127
+ # Key + count are passed through.
128
+ _, kwargs = client.search.call_args
129
+ assert kwargs["max_results"] == 2 and kwargs["include_answer"] is True
130
+
131
+
132
+ @pytest.mark.asyncio
133
+ async def test_tavily_invalid_key_mapped_to_not_configured() -> None:
134
+ from tavily.errors import InvalidAPIKeyError
135
+
136
+ client = Mock()
137
+ client.search.side_effect = InvalidAPIKeyError("bad key")
138
+ with patch(f"{_TAVILY}.TavilyClient", return_value=client):
139
+ with pytest.raises(SearchBackendNotConfigured):
140
+ await build_search_backend("tavily", api_key="bad").search("q")
141
+
142
+
143
+ # --------------------------------------------------------------------------- firecrawl
144
+
145
+
146
+ @pytest.mark.asyncio
147
+ async def test_firecrawl_keyless_uses_rest() -> None:
148
+ # No key -> direct REST (Firecrawl's free tier), no SDK involved.
149
+ payload = {
150
+ "success": True,
151
+ "data": {
152
+ "web": [
153
+ {"url": "https://a.test", "title": "T", "description": "desc"},
154
+ {"url": "", "title": "skip"},
155
+ ]
156
+ },
157
+ }
158
+ with patch(f"{_FIRE}.httpx.AsyncClient", _post_client(_DummyResponse(200, payload))):
159
+ response = await build_search_backend("firecrawl", api_key=None).search("q", max_results=3)
160
+ assert response.backend == "firecrawl"
161
+ assert [r.url for r in response.results] == ["https://a.test"]
162
+ assert response.results[0].snippet == "desc"
163
+
164
+
165
+ @pytest.mark.asyncio
166
+ async def test_firecrawl_keyless_429_rate_limited() -> None:
167
+ with patch(f"{_FIRE}.httpx.AsyncClient", _post_client(_DummyResponse(429))):
168
+ with pytest.raises(SearchBackendRateLimited):
169
+ await build_search_backend("firecrawl", api_key=None).search("q")
170
+
171
+
172
+ @pytest.mark.asyncio
173
+ async def test_firecrawl_keyed_uses_sdk() -> None:
174
+ data = SimpleNamespace(
175
+ web=[
176
+ SimpleNamespace(url="https://a.test", title="T", description="desc", markdown="full"),
177
+ SimpleNamespace(url=None, title="skip", description="", markdown=""),
178
+ ]
179
+ )
180
+ client = Mock()
181
+ client.search.return_value = data
182
+ with patch(f"{_FIRE}.Firecrawl", return_value=client):
183
+ response = await build_search_backend("firecrawl", api_key="fc").search("q", max_results=4)
184
+ assert [r.url for r in response.results] == ["https://a.test"]
185
+ assert response.results[0].snippet == "desc"
186
+ assert response.results[0].content == "full"
187
+ # limit is forwarded; firecrawl timeout is milliseconds.
188
+ _, kwargs = client.search.call_args
189
+ assert kwargs["limit"] == 4 and kwargs["timeout"] == int(WebSearchTool.SEARCH_TIMEOUT_SECONDS * 1000)
190
+
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_firecrawl_401_mapped_to_not_configured() -> None:
194
+ client = Mock()
195
+ client.search.side_effect = RuntimeError("HTTP 401 Unauthorized")
196
+ with patch(f"{_FIRE}.Firecrawl", return_value=client):
197
+ with pytest.raises(SearchBackendNotConfigured):
198
+ await build_search_backend("firecrawl", api_key="fc").search("q")
199
+
200
+
201
+ # ----------------------------------------------------------------------------- searxng
202
+
203
+
204
+ class _DummyResponse:
205
+ def __init__(self, status_code=200, payload=None, json_error=False):
206
+ self.status_code = status_code
207
+ self._payload = payload or {}
208
+ self._json_error = json_error
209
+
210
+ def json(self):
211
+ if self._json_error:
212
+ raise ValueError("not json")
213
+ return self._payload
214
+
215
+
216
+ def _searx_client(response=None, get_exc=None):
217
+ class _DummyAsyncClient:
218
+ def __init__(self, *args, **kwargs):
219
+ pass
220
+
221
+ async def __aenter__(self):
222
+ return self
223
+
224
+ async def __aexit__(self, *args):
225
+ return False
226
+
227
+ async def get(self, url, params=None):
228
+ if get_exc:
229
+ raise get_exc
230
+ return response
231
+
232
+ return _DummyAsyncClient
233
+
234
+
235
+ def _post_client(response=None, post_exc=None):
236
+ class _DummyAsyncClient:
237
+ def __init__(self, *args, **kwargs):
238
+ pass
239
+
240
+ async def __aenter__(self):
241
+ return self
242
+
243
+ async def __aexit__(self, *args):
244
+ return False
245
+
246
+ async def post(self, url, json=None, headers=None):
247
+ if post_exc:
248
+ raise post_exc
249
+ return response
250
+
251
+ return _DummyAsyncClient
252
+
253
+
254
+ @pytest.mark.asyncio
255
+ async def test_searxng_requires_base_url() -> None:
256
+ with pytest.raises(SearchBackendNotConfigured):
257
+ await build_search_backend("searxng", base_url=None).search("q")
258
+
259
+
260
+ @pytest.mark.asyncio
261
+ async def test_searxng_success() -> None:
262
+ payload = {"results": [{"title": "T", "url": "https://a.test", "content": "c"}, {"url": ""}]}
263
+ with patch(f"{_SEARX}.httpx.AsyncClient", _searx_client(_DummyResponse(200, payload))):
264
+ response = await build_search_backend("searxng", base_url="https://s.test/").search("q", max_results=5)
265
+ assert [r.url for r in response.results] == ["https://a.test"]
266
+
267
+
268
+ @pytest.mark.asyncio
269
+ async def test_searxng_429_rate_limited() -> None:
270
+ with patch(f"{_SEARX}.httpx.AsyncClient", _searx_client(_DummyResponse(429))):
271
+ with pytest.raises(SearchBackendRateLimited):
272
+ await build_search_backend("searxng", base_url="https://s.test").search("q")
273
+
274
+
275
+ @pytest.mark.asyncio
276
+ async def test_searxng_http_error_unavailable() -> None:
277
+ with patch(f"{_SEARX}.httpx.AsyncClient", _searx_client(_DummyResponse(503))):
278
+ with pytest.raises(SearchBackendUnavailable):
279
+ await build_search_backend("searxng", base_url="https://s.test").search("q")
280
+
281
+
282
+ @pytest.mark.asyncio
283
+ async def test_searxng_timeout_unavailable() -> None:
284
+ with patch(f"{_SEARX}.httpx.AsyncClient", _searx_client(get_exc=httpx.TimeoutException("slow"))):
285
+ with pytest.raises(SearchBackendUnavailable):
286
+ await build_search_backend("searxng", base_url="https://s.test").search("q")
287
+
288
+
289
+ # ------------------------------------------------------------------------- WebSearchTool
290
+
291
+
292
+ @pytest.fixture
293
+ def agent_config():
294
+ return AgentConfig(
295
+ anthropic_api_key="k",
296
+ long_context_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="m"),
297
+ fast_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="f"),
298
+ thinking_config=ModelConfig(provider=ModelProvider.ANTHROPIC, model="t"),
299
+ )
300
+
301
+
302
+ @pytest.fixture
303
+ def caller():
304
+ c = Mock()
305
+ c.agent_name = "coder"
306
+ c.current_tool_call_id = "call-1"
307
+ c.sub_agent = False
308
+ return c
309
+
310
+
311
+ @pytest.fixture
312
+ def tool(tmp_path, agent_config, caller):
313
+ return WebSearchTool(tmp_path, "ws", "th", AsyncMock(), agent_config, caller)
314
+
315
+
316
+ @pytest.mark.asyncio
317
+ async def test_tool_empty_query_guard(tool) -> None:
318
+ assert (await tool.web_search(" ")).startswith("Error: Provide a non-empty search query.")
319
+
320
+
321
+ @pytest.mark.asyncio
322
+ async def test_tool_formats_results_and_streams(tool) -> None:
323
+ response = SearchResponse(
324
+ query="cats",
325
+ backend="duckduckgo",
326
+ results=[SearchResult(title="Cats", url="https://cats.test", snippet="meow")],
327
+ )
328
+ backend = Mock()
329
+ backend.search = AsyncMock(return_value=response)
330
+ with patch("kolega_code.agent.tool_backend.web_search_tool.build_search_backend", return_value=backend):
331
+ result = await tool.web_search("cats", max_results=3)
332
+ # The URL appears as its own line (exact match, not a substring check).
333
+ assert any(line.strip() == "https://cats.test" for line in result.splitlines())
334
+ assert "Cats" in result
335
+ assert "meow" in result
336
+ # A final (is_complete) streaming update is emitted.
337
+ final = [c for c in tool.connection_manager.broadcast_event.call_args_list]
338
+ assert final, "expected streaming updates"
339
+
340
+
341
+ @pytest.mark.asyncio
342
+ async def test_tool_empty_results_message(tool) -> None:
343
+ response = SearchResponse(query="zzz", backend="duckduckgo", results=[])
344
+ backend = Mock()
345
+ backend.search = AsyncMock(return_value=response)
346
+ with patch("kolega_code.agent.tool_backend.web_search_tool.build_search_backend", return_value=backend):
347
+ result = await tool.web_search("zzz")
348
+ assert result == "No results found for: zzz"
349
+
350
+
351
+ @pytest.mark.asyncio
352
+ async def test_tool_not_configured_is_friendly(tool) -> None:
353
+ backend = Mock()
354
+ backend.search = AsyncMock(side_effect=SearchBackendNotConfigured("needs a key"))
355
+ with patch("kolega_code.agent.tool_backend.web_search_tool.build_search_backend", return_value=backend):
356
+ result = await tool.web_search("q")
357
+ assert result.startswith("Error:") and "Settings > Web Search" in result
358
+
359
+
360
+ @pytest.mark.asyncio
361
+ async def test_tool_never_raises_on_unexpected(tool) -> None:
362
+ backend = Mock()
363
+ backend.search = AsyncMock(side_effect=RuntimeError("kaboom"))
364
+ with patch("kolega_code.agent.tool_backend.web_search_tool.build_search_backend", return_value=backend):
365
+ result = await tool.web_search("q")
366
+ assert result.startswith("Error: web_search failed:")
@@ -0,0 +1,83 @@
1
+ """Pluggable web-search backends for the ``web_search`` tool.
2
+
3
+ To add a backend: create a module with a ``SearchBackend`` subclass, import it below,
4
+ and add it to ``_BACKENDS``. The TUI Select options and config validation derive from
5
+ this registry, so no other wiring is needed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Dict, List, Optional, Tuple, Type
11
+
12
+ from .base import DEFAULT_RESULTS, DEFAULT_TIMEOUT_SECONDS, SearchBackend, clamp_results
13
+ from .duckduckgo import DuckDuckGoBackend
14
+ from .errors import (
15
+ SearchBackendError,
16
+ SearchBackendNotConfigured,
17
+ SearchBackendRateLimited,
18
+ SearchBackendUnavailable,
19
+ )
20
+ from .firecrawl import FirecrawlBackend
21
+ from .models import SearchResponse, SearchResult
22
+ from .searxng import SearxngBackend
23
+ from .tavily import TavilyBackend
24
+
25
+ DEFAULT_BACKEND = "duckduckgo"
26
+
27
+ _BACKENDS: Dict[str, Type[SearchBackend]] = {
28
+ backend.name: backend
29
+ for backend in (DuckDuckGoBackend, FirecrawlBackend, TavilyBackend, SearxngBackend)
30
+ }
31
+
32
+
33
+ def backend_names() -> List[str]:
34
+ """All registered backend names."""
35
+ return list(_BACKENDS)
36
+
37
+
38
+ def available_backends() -> List[Tuple[str, str]]:
39
+ """``(label, name)`` pairs for the TUI Select, with the default backend first."""
40
+ ordered = [_BACKENDS[DEFAULT_BACKEND]] + [
41
+ backend for name, backend in _BACKENDS.items() if name != DEFAULT_BACKEND
42
+ ]
43
+ return [(backend.label, backend.name) for backend in ordered]
44
+
45
+
46
+ def get_backend_class(name: str) -> Type[SearchBackend]:
47
+ """Look up a backend class by name, or raise SearchBackendError for an unknown name."""
48
+ try:
49
+ return _BACKENDS[name]
50
+ except KeyError as exc:
51
+ valid = ", ".join(_BACKENDS)
52
+ raise SearchBackendError(f"Unknown web_search backend '{name}'. Valid: {valid}.") from exc
53
+
54
+
55
+ def build_search_backend(
56
+ name: Optional[str],
57
+ *,
58
+ api_key: Optional[str] = None,
59
+ base_url: Optional[str] = None,
60
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
61
+ ) -> SearchBackend:
62
+ """Construct the configured backend instance."""
63
+ backend_cls = get_backend_class(name or DEFAULT_BACKEND)
64
+ return backend_cls(api_key=api_key, base_url=base_url, timeout=timeout)
65
+
66
+
67
+ __all__ = [
68
+ "DEFAULT_BACKEND",
69
+ "DEFAULT_RESULTS",
70
+ "DEFAULT_TIMEOUT_SECONDS",
71
+ "SearchBackend",
72
+ "SearchBackendError",
73
+ "SearchBackendNotConfigured",
74
+ "SearchBackendRateLimited",
75
+ "SearchBackendUnavailable",
76
+ "SearchResponse",
77
+ "SearchResult",
78
+ "available_backends",
79
+ "backend_names",
80
+ "build_search_backend",
81
+ "clamp_results",
82
+ "get_backend_class",
83
+ ]