echo-agent 0.2.1__tar.gz → 0.2.3__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 (302) hide show
  1. {echo_agent-0.2.1 → echo_agent-0.2.3}/PKG-INFO +4 -1
  2. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/__init__.py +1 -1
  3. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/loop.py +8 -3
  4. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/pipeline/context_stage.py +39 -1
  5. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/pipeline/inference_stage.py +38 -0
  6. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/__init__.py +42 -25
  7. echo_agent-0.2.3/echo_agent/agent/tools/image_gen_fal.py +231 -0
  8. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/app.py +6 -3
  9. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/base.py +15 -0
  10. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/dingtalk.py +2 -0
  11. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/email.py +2 -0
  12. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/feishu.py +2 -0
  13. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/matrix.py +2 -0
  14. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/qqbot.py +2 -0
  15. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/webhook.py +2 -0
  16. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/wecom.py +2 -0
  17. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/weixin.py +2 -0
  18. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/whatsapp.py +2 -0
  19. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/i18n/en.py +8 -0
  20. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/i18n/zh.py +8 -0
  21. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/setup.py +32 -10
  22. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/config/schema.py +11 -2
  23. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/dependencies/lazy_deps.py +1 -0
  24. echo_agent-0.2.3/echo_agent/gateway/api/__init__.py +54 -0
  25. echo_agent-0.2.3/echo_agent/gateway/api/channels.py +53 -0
  26. echo_agent-0.2.3/echo_agent/gateway/api/config.py +85 -0
  27. echo_agent-0.2.3/echo_agent/gateway/api/knowledge.py +136 -0
  28. echo_agent-0.2.3/echo_agent/gateway/api/lifecycle.py +41 -0
  29. echo_agent-0.2.3/echo_agent/gateway/api/memory.py +137 -0
  30. echo_agent-0.2.3/echo_agent/gateway/api/skills.py +66 -0
  31. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/health.py +8 -0
  32. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/server.py +34 -1
  33. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/capabilities.py +2 -2
  34. {echo_agent-0.2.1 → echo_agent-0.2.3}/pyproject.toml +3 -2
  35. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/ppt-author/scripts/create_pptx.py +15 -10
  36. {echo_agent-0.2.1 → echo_agent-0.2.3}/.gitignore +0 -0
  37. {echo_agent-0.2.1 → echo_agent-0.2.3}/LICENSE +0 -0
  38. {echo_agent-0.2.1 → echo_agent-0.2.3}/README.md +0 -0
  39. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/__main__.py +0 -0
  40. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/a2a/__init__.py +0 -0
  41. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/a2a/client.py +0 -0
  42. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/a2a/models.py +0 -0
  43. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/a2a/protocol.py +0 -0
  44. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/a2a/server.py +0 -0
  45. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/__init__.py +0 -0
  46. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/approval_gate.py +0 -0
  47. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/__init__.py +0 -0
  48. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/assembler.py +0 -0
  49. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/boundary.py +0 -0
  50. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/compressor.py +0 -0
  51. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/engine.py +0 -0
  52. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/pruner.py +0 -0
  53. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/summarizer.py +0 -0
  54. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/types.py +0 -0
  55. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/compression/validator.py +0 -0
  56. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/consolidation.py +0 -0
  57. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/context.py +0 -0
  58. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/context_cache.py +0 -0
  59. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/executors/__init__.py +0 -0
  60. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/executors/base.py +0 -0
  61. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/executors/factory.py +0 -0
  62. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/executors/remote.py +0 -0
  63. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/multi_agent/__init__.py +0 -0
  64. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/multi_agent/audit.py +0 -0
  65. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/multi_agent/error_messages.py +0 -0
  66. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/multi_agent/error_types.py +0 -0
  67. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/multi_agent/models.py +0 -0
  68. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/multi_agent/registry.py +0 -0
  69. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/multi_agent/runtime.py +0 -0
  70. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/pipeline/__init__.py +0 -0
  71. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/pipeline/response_stage.py +0 -0
  72. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/pipeline/types.py +0 -0
  73. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/planning/__init__.py +0 -0
  74. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/planning/models.py +0 -0
  75. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/planning/planner.py +0 -0
  76. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/planning/reflection.py +0 -0
  77. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/planning/strategies.py +0 -0
  78. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/streaming.py +0 -0
  79. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/base.py +0 -0
  80. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/circuit_breaker.py +0 -0
  81. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/clarify.py +0 -0
  82. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/code_exec.py +0 -0
  83. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/cronjob.py +0 -0
  84. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/delegate.py +0 -0
  85. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/filesystem.py +0 -0
  86. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/image_gen.py +0 -0
  87. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/knowledge.py +0 -0
  88. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/memory.py +0 -0
  89. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/message.py +0 -0
  90. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/notify.py +0 -0
  91. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/patch.py +0 -0
  92. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/process.py +0 -0
  93. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/registry.py +0 -0
  94. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/search.py +0 -0
  95. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/session_search.py +0 -0
  96. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/shell.py +0 -0
  97. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/skill_install.py +0 -0
  98. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/skills.py +0 -0
  99. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/task.py +0 -0
  100. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/todo.py +0 -0
  101. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/tts.py +0 -0
  102. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/vision.py +0 -0
  103. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/web.py +0 -0
  104. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/agent/tools/workflow.py +0 -0
  105. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/bus/__init__.py +0 -0
  106. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/bus/events.py +0 -0
  107. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/bus/queue.py +0 -0
  108. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/bus/rate_limiter.py +0 -0
  109. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/__init__.py +0 -0
  110. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/cli.py +0 -0
  111. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/cron.py +0 -0
  112. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/discord.py +0 -0
  113. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/manager.py +0 -0
  114. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/qqbot_media.py +0 -0
  115. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/slack.py +0 -0
  116. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/channels/telegram.py +0 -0
  117. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/__init__.py +0 -0
  118. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/colors.py +0 -0
  119. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/evolution_cmd.py +0 -0
  120. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/i18n/__init__.py +0 -0
  121. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/plugins_cmd.py +0 -0
  122. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/prompt.py +0 -0
  123. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/service.py +0 -0
  124. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/cli/status.py +0 -0
  125. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/config/__init__.py +0 -0
  126. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/config/default.yaml +0 -0
  127. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/config/loader.py +0 -0
  128. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/dependencies/__init__.py +0 -0
  129. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/dependencies/cli.py +0 -0
  130. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/dependencies/skill_require.py +0 -0
  131. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evaluation/__init__.py +0 -0
  132. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evaluation/dataset.py +0 -0
  133. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evaluation/metrics.py +0 -0
  134. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evaluation/reporter.py +0 -0
  135. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evaluation/runner.py +0 -0
  136. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/__init__.py +0 -0
  137. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/engine.py +0 -0
  138. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/evolver.py +0 -0
  139. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/gate.py +0 -0
  140. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/recorder.py +0 -0
  141. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/scheduler.py +0 -0
  142. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/store.py +0 -0
  143. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/tools.py +0 -0
  144. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/types.py +0 -0
  145. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/evolution/validation.py +0 -0
  146. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/__init__.py +0 -0
  147. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/auth.py +0 -0
  148. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/editor.py +0 -0
  149. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/hooks.py +0 -0
  150. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/media.py +0 -0
  151. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/rate_limiter.py +0 -0
  152. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/router.py +0 -0
  153. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/session_context.py +0 -0
  154. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/session_policy.py +0 -0
  155. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/gateway/static/index.html +0 -0
  156. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/knowledge/__init__.py +0 -0
  157. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/knowledge/index.py +0 -0
  158. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/mcp/__init__.py +0 -0
  159. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/mcp/client.py +0 -0
  160. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/mcp/manager.py +0 -0
  161. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/mcp/oauth.py +0 -0
  162. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/mcp/security.py +0 -0
  163. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/mcp/tool_adapter.py +0 -0
  164. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/mcp/transport.py +0 -0
  165. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/__init__.py +0 -0
  166. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/consolidator.py +0 -0
  167. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/contradiction.py +0 -0
  168. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/forgetting.py +0 -0
  169. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/retrieval.py +0 -0
  170. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/reviewer.py +0 -0
  171. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/store.py +0 -0
  172. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/tiers.py +0 -0
  173. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/types.py +0 -0
  174. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/memory/vectors.py +0 -0
  175. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/__init__.py +0 -0
  176. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/credential_pool.py +0 -0
  177. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/inference.py +0 -0
  178. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/provider.py +0 -0
  179. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/providers/__init__.py +0 -0
  180. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/providers/anthropic_provider.py +0 -0
  181. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/providers/bedrock_provider.py +0 -0
  182. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/providers/format_utils.py +0 -0
  183. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/providers/gemini_provider.py +0 -0
  184. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/providers/openai_provider.py +0 -0
  185. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/providers/openrouter_provider.py +0 -0
  186. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/rate_limiter.py +0 -0
  187. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/router.py +0 -0
  188. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/stub.py +0 -0
  189. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/models/tokenizer.py +0 -0
  190. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/observability/__init__.py +0 -0
  191. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/observability/monitor.py +0 -0
  192. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/observability/spans.py +0 -0
  193. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/observability/telemetry.py +0 -0
  194. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/permissions/__init__.py +0 -0
  195. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/permissions/allowlist.py +0 -0
  196. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/permissions/manager.py +0 -0
  197. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/__init__.py +0 -0
  198. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/context.py +0 -0
  199. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/errors.py +0 -0
  200. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/hooks.py +0 -0
  201. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/loader.py +0 -0
  202. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/manager.py +0 -0
  203. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/manifest.py +0 -0
  204. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/plugins/sandbox.py +0 -0
  205. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/runtime_paths.py +0 -0
  206. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/scheduler/__init__.py +0 -0
  207. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/scheduler/delivery.py +0 -0
  208. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/scheduler/service.py +0 -0
  209. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/__init__.py +0 -0
  210. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/guards.py +0 -0
  211. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/normalizer.py +0 -0
  212. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/path_policy.py +0 -0
  213. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/risk_classifier.py +0 -0
  214. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/smart_approval.py +0 -0
  215. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/tokenizer.py +0 -0
  216. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/security/tool_policy.py +0 -0
  217. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/session/__init__.py +0 -0
  218. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/session/manager.py +0 -0
  219. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/session/media_ref.py +0 -0
  220. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/skills/__init__.py +0 -0
  221. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/skills/manager.py +0 -0
  222. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/skills/reviewer.py +0 -0
  223. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/skills/store.py +0 -0
  224. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/storage/__init__.py +0 -0
  225. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/storage/backend.py +0 -0
  226. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/storage/sqlite.py +0 -0
  227. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/tasks/__init__.py +0 -0
  228. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/tasks/manager.py +0 -0
  229. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/tasks/models.py +0 -0
  230. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/tasks/workflow.py +0 -0
  231. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/tools/__init__.py +0 -0
  232. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/tools/base.py +0 -0
  233. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/utils/__init__.py +0 -0
  234. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/utils/async_io.py +0 -0
  235. {echo_agent-0.2.1 → echo_agent-0.2.3}/echo_agent/utils/text.py +0 -0
  236. {echo_agent-0.2.1 → echo_agent-0.2.3}/scripts/install.sh +0 -0
  237. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/excel-author/SKILL.md +0 -0
  238. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/excel-author/scripts/create_xlsx.py +0 -0
  239. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/image-gen/SKILL.md +0 -0
  240. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/image-gen/scripts/generate_image.py +0 -0
  241. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/meme-gen/SKILL.md +0 -0
  242. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/meme-gen/scripts/make_meme.py +0 -0
  243. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/creative/ppt-author/SKILL.md +0 -0
  244. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/code-runner/SKILL.md +0 -0
  245. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/code-runner/scripts/safe_exec.py +0 -0
  246. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/github-ops/SKILL.md +0 -0
  247. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/plan/SKILL.md +0 -0
  248. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/skill-creator/SKILL.md +0 -0
  249. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/skill-creator/scripts/init_skill.py +0 -0
  250. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/skill-creator/scripts/package_skill.py +0 -0
  251. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/skill-creator/scripts/quick_validate.py +0 -0
  252. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/workflow-chain/SKILL.md +0 -0
  253. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/development/workflow-chain/scripts/workflow_engine.py +0 -0
  254. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/devops/docker-manage/SKILL.md +0 -0
  255. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/devops/system-monitor/SKILL.md +0 -0
  256. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/devops/system-monitor/scripts/system_check.py +0 -0
  257. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/finance/finance-tracker/SKILL.md +0 -0
  258. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/finance/finance-tracker/scripts/finance_manager.py +0 -0
  259. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/finance/stocks/SKILL.md +0 -0
  260. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/finance/stocks/scripts/market_query.py +0 -0
  261. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/health/fitness-nutrition/SKILL.md +0 -0
  262. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/health/fitness-nutrition/scripts/health_query.py +0 -0
  263. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/learning/flashcards/SKILL.md +0 -0
  264. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/learning/flashcards/scripts/flashcard_engine.py +0 -0
  265. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/media/tts-voice/SKILL.md +0 -0
  266. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/media/tts-voice/scripts/text_to_speech.py +0 -0
  267. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/media/voice-note/SKILL.md +0 -0
  268. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/media/voice-note/scripts/voice_process.py +0 -0
  269. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/calendar/SKILL.md +0 -0
  270. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/calendar/scripts/calendar_client.py +0 -0
  271. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/daily-briefing/SKILL.md +0 -0
  272. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/daily-briefing/scripts/generate_briefing.py +0 -0
  273. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/email-assistant/SKILL.md +0 -0
  274. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/email-assistant/scripts/email_client.py +0 -0
  275. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/note-taking/SKILL.md +0 -0
  276. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/note-taking/scripts/notes_manager.py +0 -0
  277. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/notion-sync/SKILL.md +0 -0
  278. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/notion-sync/scripts/notion_client.py +0 -0
  279. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/ocr-document/SKILL.md +0 -0
  280. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/ocr-document/scripts/extract_document.py +0 -0
  281. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/reminder/SKILL.md +0 -0
  282. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/reminder/scripts/reminder_store.py +0 -0
  283. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/summarize/SKILL.md +0 -0
  284. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/productivity/weather/SKILL.md +0 -0
  285. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/arxiv/SKILL.md +0 -0
  286. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/arxiv/scripts/search_arxiv.py +0 -0
  287. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/deep-research/SKILL.md +0 -0
  288. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/deep-research/scripts/research_report.py +0 -0
  289. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/rss-watcher/SKILL.md +0 -0
  290. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/rss-watcher/scripts/feed_monitor.py +0 -0
  291. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/web-extract/SKILL.md +0 -0
  292. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/web-extract/scripts/extract_url.py +0 -0
  293. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/web-search/SKILL.md +0 -0
  294. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/research/web-search/scripts/web_search.py +0 -0
  295. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/calculator/SKILL.md +0 -0
  296. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/calculator/scripts/calc.py +0 -0
  297. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/file-convert/SKILL.md +0 -0
  298. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/file-convert/scripts/convert.py +0 -0
  299. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/maps-poi/SKILL.md +0 -0
  300. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/maps-poi/scripts/geo_query.py +0 -0
  301. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/text-tools/SKILL.md +0 -0
  302. {echo_agent-0.2.1 → echo_agent-0.2.3}/skills/utility/text-tools/scripts/text_process.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echo-agent
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: A modular AI agent framework with multi-channel support
5
5
  Author: Echo Agent contributors
6
6
  License: MIT
@@ -25,6 +25,7 @@ Requires-Dist: anthropic>=0.40; extra == 'all'
25
25
  Requires-Dist: boto3>=1.34; extra == 'all'
26
26
  Requires-Dist: cryptography>=41.0; extra == 'all'
27
27
  Requires-Dist: faiss-cpu>=1.7; extra == 'all'
28
+ Requires-Dist: fal-client>=0.5; extra == 'all'
28
29
  Requires-Dist: google-generativeai>=0.8; extra == 'all'
29
30
  Requires-Dist: openai>=1.30; extra == 'all'
30
31
  Requires-Dist: psutil>=5.9; extra == 'all'
@@ -45,6 +46,8 @@ Provides-Extra: dev
45
46
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
46
47
  Requires-Dist: pytest>=8.0; extra == 'dev'
47
48
  Requires-Dist: ruff>=0.4; extra == 'dev'
49
+ Provides-Extra: fal
50
+ Requires-Dist: fal-client>=0.5; extra == 'fal'
48
51
  Provides-Extra: gemini
49
52
  Requires-Dist: google-generativeai>=0.8; extra == 'gemini'
50
53
  Provides-Extra: openai
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.2.3"
@@ -259,6 +259,7 @@ class AgentLoop:
259
259
  memory_snapshots=self._memory_snapshots,
260
260
  snapshot_enabled=self._snapshot_enabled,
261
261
  tool_definitions_fn=self.tools.get_definitions,
262
+ bus=bus,
262
263
  )
263
264
  self._inference_stage = InferenceStage(
264
265
  config=config,
@@ -730,9 +731,13 @@ class AgentLoop:
730
731
  return template
731
732
 
732
733
  def _should_stream_channel(self, channel: str) -> bool:
733
- if channel.startswith("gateway:"):
734
- return False
735
- return channel in set(self.config.channels.stream_channels)
734
+ channels = set(self.config.channels.stream_channels)
735
+ if channel in channels:
736
+ return True
737
+ for pattern in channels:
738
+ if pattern.endswith(":*") and channel.startswith(pattern[:-1]):
739
+ return True
740
+ return False
736
741
 
737
742
  async def process_direct(self, content: str, session_key: str = "cli:direct") -> str:
738
743
  """Process a message directly (for CLI or testing)."""
@@ -15,7 +15,7 @@ from echo_agent.agent.context import (
15
15
  build_skills_context,
16
16
  )
17
17
  from echo_agent.agent.pipeline.types import PipelineContext
18
- from echo_agent.bus.events import InboundEvent
18
+ from echo_agent.bus.events import InboundEvent, OutboundEvent
19
19
  from echo_agent.session.manager import Session
20
20
 
21
21
  if TYPE_CHECKING:
@@ -56,6 +56,7 @@ class ContextStage:
56
56
  memory_snapshots: OrderedDict,
57
57
  snapshot_enabled: bool,
58
58
  tool_definitions_fn: Any,
59
+ bus: Any = None,
59
60
  ):
60
61
  self._config = config
61
62
  self._sessions = sessions
@@ -71,6 +72,19 @@ class ContextStage:
71
72
  self._memory_snapshots = memory_snapshots
72
73
  self._snapshot_enabled = snapshot_enabled
73
74
  self._tool_definitions_fn = tool_definitions_fn
75
+ self._bus = bus
76
+
77
+ async def _emit_progress(self, event: InboundEvent, metadata: dict[str, Any]) -> None:
78
+ if not getattr(self._config.gateway, 'emit_progress_events', True):
79
+ return
80
+ out = OutboundEvent.text_reply(
81
+ channel=event.channel, chat_id=event.chat_id, text="", reply_to_id=event.reply_to_id,
82
+ )
83
+ out.is_final = False
84
+ out.message_kind = "progress"
85
+ out.metadata = {"_progress": True, "_inbound_event_id": event.event_id}
86
+ out.metadata.update(metadata)
87
+ await self._bus.publish_outbound(out)
74
88
 
75
89
  async def build(
76
90
  self,
@@ -155,6 +169,18 @@ class ContextStage:
155
169
  "Relevant memory:\n"
156
170
  + "\n".join(f"- {r.key}: {r.content}" for r, _ in scored)
157
171
  )
172
+ if publish_response and self._bus:
173
+ _debug = getattr(self._config.gateway, 'progress_debug', False)
174
+ _mem_meta: dict[str, Any] = {
175
+ "progress_type": "memory_retrieved",
176
+ "count": len(scored),
177
+ }
178
+ if _debug:
179
+ _mem_meta["entries"] = [
180
+ {"key": r.key, "content_preview": r.content[:100]}
181
+ for r, _ in scored[:5]
182
+ ]
183
+ await self._emit_progress(event, _mem_meta)
158
184
 
159
185
  if self._knowledge:
160
186
  knowledge_results = self._knowledge.search(
@@ -165,6 +191,18 @@ class ContextStage:
165
191
  knowledge_context = self._knowledge.format_results(knowledge_results)
166
192
  if knowledge_context:
167
193
  retrieval_parts.append(knowledge_context)
194
+ if publish_response and self._bus:
195
+ _debug = getattr(self._config.gateway, 'progress_debug', False)
196
+ _know_meta: dict[str, Any] = {
197
+ "progress_type": "knowledge_cited",
198
+ "count": len(knowledge_results) if knowledge_results else 0,
199
+ }
200
+ if _debug:
201
+ _know_meta["citations"] = [
202
+ {"path": getattr(r, 'path', ''), "chunk_preview": getattr(r, 'text', '')[:200], "score": getattr(r, 'score', 0.0)}
203
+ for r in (knowledge_results[:5] if knowledge_results else [])
204
+ ]
205
+ await self._emit_progress(event, _know_meta)
168
206
 
169
207
  task_type = self._infer_task_type(event.text)
170
208
 
@@ -92,6 +92,20 @@ class InferenceStage:
92
92
  out.metadata.update({"_progress": True, "_tool_hint": tool_hint, "_inbound_event_id": event.event_id})
93
93
  await self._bus.publish_outbound(out)
94
94
 
95
+ async def _emit_tool_event(metadata: dict[str, Any]) -> None:
96
+ if not ctx.publish_response:
97
+ return
98
+ if not getattr(self._config.gateway, 'emit_progress_events', True):
99
+ return
100
+ out = OutboundEvent.text_reply(
101
+ channel=event.channel, chat_id=event.chat_id, text="", reply_to_id=event.reply_to_id,
102
+ )
103
+ out.is_final = False
104
+ out.message_kind = "progress"
105
+ out.metadata = {"_progress": True, "_inbound_event_id": event.event_id}
106
+ out.metadata.update(metadata)
107
+ await self._bus.publish_outbound(out)
108
+
95
109
  # Standard inference loop
96
110
  response_text = ""
97
111
  should_review_skills = False
@@ -272,6 +286,19 @@ class InferenceStage:
272
286
  if _hook_cancelled:
273
287
  continue
274
288
 
289
+ import time as _time
290
+ _tool_start_ts = _time.monotonic()
291
+
292
+ _debug_progress = getattr(self._config.gateway, 'progress_debug', False)
293
+ _tool_start_meta: dict[str, Any] = {
294
+ "progress_type": "tool_call",
295
+ "tool": tool_call.name,
296
+ "status": "started",
297
+ }
298
+ if _debug_progress:
299
+ _tool_start_meta["args"] = str(tool_call.arguments)[:500]
300
+ await _emit_tool_event(_tool_start_meta)
301
+
275
302
  result = await self._tools.execute(tool_call.name, tool_call.arguments, tool_exec_ctx)
276
303
 
277
304
  # post_tool_call hook
@@ -280,6 +307,17 @@ class InferenceStage:
280
307
  "post_tool_call", result, tool_call.name, tool_call.arguments, tool_exec_ctx,
281
308
  )
282
309
 
310
+ _tool_duration_ms = int((_time.monotonic() - _tool_start_ts) * 1000)
311
+ _tool_result_meta: dict[str, Any] = {
312
+ "progress_type": "tool_result",
313
+ "tool": tool_call.name,
314
+ "duration_ms": _tool_duration_ms,
315
+ "status": "done" if result.success else "error",
316
+ }
317
+ if _debug_progress:
318
+ _tool_result_meta["result_preview"] = result.text[:500]
319
+ await _emit_tool_event(_tool_result_meta)
320
+
283
321
  result_text = result.text
284
322
  if len(result_text) > self._MAX_TOOL_RESULT_CHARS:
285
323
  result_text = result_text[:self._MAX_TOOL_RESULT_CHARS] + "\n...(truncated)"
@@ -189,30 +189,42 @@ def _infer_tts_model(api_base: str) -> str:
189
189
 
190
190
  def _try_register_image_gen(tools: list[Tool], config: Config, provider: LLMProvider | None = None) -> None:
191
191
  ig = getattr(config.tools, "image_gen", None)
192
- explicit_key = getattr(ig, "api_key", "") if ig else ""
192
+ backend = getattr(ig, "backend", "openai") if ig else "openai"
193
+
194
+ if backend == "fal":
195
+ fal_key = getattr(ig, "fal_key", "") if ig else ""
196
+ fal_model = getattr(ig, "fal_model", "") if ig else ""
197
+ if not fal_key:
198
+ logger.info(
199
+ "image_generate tool not registered: no fal_key configured. "
200
+ "Set tools.image_gen.fal_key and tools.image_gen.fal_model in config to enable."
201
+ )
202
+ return
203
+ from echo_agent.agent.tools.image_gen_fal import FalImageGenTool, FAL_MODELS
204
+ if fal_model and fal_model not in FAL_MODELS:
205
+ logger.warning(
206
+ "image_generate: configured fal_model '{}' is not in the built-in catalog. "
207
+ "Supported: {}. The tool will error at execution time.",
208
+ fal_model, ", ".join(sorted(FAL_MODELS.keys())),
209
+ )
210
+ tools.append(FalImageGenTool(fal_key=fal_key, model=fal_model))
211
+ return
212
+
213
+ api_key = getattr(ig, "api_key", "") if ig else ""
193
214
  api_base = getattr(ig, "api_base", "") if ig else ""
194
215
  model = getattr(ig, "model", "") if ig else ""
195
216
 
196
- if explicit_key:
197
- # User explicitly configured image_gen — use their settings as-is
198
- api_key = explicit_key
199
- elif _is_openai_compatible_provider(provider):
200
- # Fallback only for OpenAI-compatible providers — use unwrapped to get real key/base
201
- real = _unwrap_provider(provider)
202
- api_key = getattr(real, "api_key", "")
203
- if not api_base:
204
- api_base = getattr(real, "api_base", "")
205
- # Reset default model so we infer from api_base
206
- model = ""
207
- else:
208
- return
209
-
210
217
  if not api_key:
218
+ if _is_openai_compatible_provider(provider):
219
+ logger.info(
220
+ "image_generate tool not registered: no explicit image_gen.api_key configured. "
221
+ "Set tools.image_gen.api_key, tools.image_gen.api_base, and tools.image_gen.model in config to enable."
222
+ )
211
223
  return
212
224
 
213
- # Infer model from api_base if not explicitly set by user
214
- if not model or (not explicit_key and model == "dall-e-3"):
225
+ if not model:
215
226
  model = _infer_image_model(api_base)
227
+ logger.debug("image_generate: model not configured, inferred '{}' from api_base", model)
216
228
 
217
229
  from echo_agent.agent.tools.image_gen import ImageGenTool
218
230
  tools.append(ImageGenTool(api_key=api_key, api_base=api_base, model=model))
@@ -222,18 +234,23 @@ def _try_register_tts(tools: list[Tool], config: Config, ws: str, provider: LLMP
222
234
  from echo_agent.agent.tools.tts import TTSTool
223
235
  tts_cfg = getattr(config.tools, "tts", None)
224
236
  openai_key = getattr(tts_cfg, "openai_api_key", "") if tts_cfg else ""
225
- openai_base = ""
237
+ openai_base = getattr(tts_cfg, "openai_api_base", "") if tts_cfg else ""
238
+ tts_model = getattr(tts_cfg, "model", "") if tts_cfg else ""
226
239
  default_backend = getattr(tts_cfg, "default_backend", "edge") if tts_cfg else "edge"
227
240
  default_voice = getattr(tts_cfg, "default_voice", "") if tts_cfg else ""
228
241
 
229
- # Fallback only for OpenAI-compatible providers
230
242
  if not openai_key and _is_openai_compatible_provider(provider):
231
- real = _unwrap_provider(provider)
232
- openai_key = getattr(real, "api_key", "")
233
- openai_base = getattr(real, "api_base", "")
234
-
235
- # Infer TTS model from api_base
236
- tts_model = _infer_tts_model(openai_base)
243
+ logger.info(
244
+ "TTS openai backend not available: no explicit tts.openai_api_key configured. "
245
+ "Set tools.tts.openai_api_key, tools.tts.openai_api_base, and tools.tts.model in config to enable. "
246
+ "edge backend remains available without configuration."
247
+ )
248
+
249
+ if not tts_model and openai_key:
250
+ tts_model = _infer_tts_model(openai_base)
251
+ logger.debug("TTS: model not configured, inferred '{}' from openai_api_base", tts_model)
252
+ if not tts_model:
253
+ tts_model = "tts-1"
237
254
 
238
255
  tools.append(TTSTool(
239
256
  workspace=ws,
@@ -0,0 +1,231 @@
1
+ """Image generation tool — FAL.ai backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import threading
8
+ from typing import Any
9
+
10
+ from echo_agent.agent.tools.base import Tool, ToolExecutionContext, ToolResult
11
+
12
+ DEFAULT_MODEL = "fal-ai/flux/schnell"
13
+
14
+ FAL_MODELS: dict[str, dict[str, Any]] = {
15
+ "fal-ai/flux/schnell": {
16
+ "display": "FLUX Schnell",
17
+ "size_style": "image_size_preset",
18
+ "sizes": {
19
+ "landscape": "landscape_16_9",
20
+ "square": "square_hd",
21
+ "portrait": "portrait_16_9",
22
+ },
23
+ "defaults": {
24
+ "num_images": 1,
25
+ "output_format": "png",
26
+ "enable_safety_checker": False,
27
+ "sync_mode": True,
28
+ },
29
+ "supports": {
30
+ "prompt", "image_size", "num_images", "output_format",
31
+ "enable_safety_checker", "sync_mode", "seed",
32
+ },
33
+ },
34
+ "fal-ai/flux-2-pro": {
35
+ "display": "FLUX 2 Pro",
36
+ "size_style": "image_size_preset",
37
+ "sizes": {
38
+ "landscape": "landscape_16_9",
39
+ "square": "square_hd",
40
+ "portrait": "portrait_16_9",
41
+ },
42
+ "defaults": {
43
+ "num_inference_steps": 50,
44
+ "guidance_scale": 4.5,
45
+ "num_images": 1,
46
+ "output_format": "png",
47
+ "enable_safety_checker": False,
48
+ "sync_mode": True,
49
+ },
50
+ "supports": {
51
+ "prompt", "image_size", "num_inference_steps", "guidance_scale",
52
+ "num_images", "output_format", "enable_safety_checker",
53
+ "sync_mode", "seed",
54
+ },
55
+ },
56
+ "fal-ai/ideogram/v3": {
57
+ "display": "Ideogram V3",
58
+ "size_style": "image_size_preset",
59
+ "sizes": {
60
+ "landscape": "landscape_16_9",
61
+ "square": "square_hd",
62
+ "portrait": "portrait_16_9",
63
+ },
64
+ "defaults": {
65
+ "rendering_speed": "BALANCED",
66
+ "expand_prompt": True,
67
+ "style": "AUTO",
68
+ },
69
+ "supports": {
70
+ "prompt", "image_size", "rendering_speed", "expand_prompt",
71
+ "style", "seed",
72
+ },
73
+ },
74
+ "fal-ai/recraft/v4/pro/text-to-image": {
75
+ "display": "Recraft V4 Pro",
76
+ "size_style": "image_size_preset",
77
+ "sizes": {
78
+ "landscape": "landscape_16_9",
79
+ "square": "square_hd",
80
+ "portrait": "portrait_16_9",
81
+ },
82
+ "defaults": {
83
+ "enable_safety_checker": False,
84
+ },
85
+ "supports": {
86
+ "prompt", "image_size", "enable_safety_checker",
87
+ },
88
+ },
89
+ "fal-ai/qwen-image": {
90
+ "display": "Qwen Image",
91
+ "size_style": "image_size_preset",
92
+ "sizes": {
93
+ "landscape": "landscape_16_9",
94
+ "square": "square_hd",
95
+ "portrait": "portrait_16_9",
96
+ },
97
+ "defaults": {
98
+ "num_inference_steps": 30,
99
+ "guidance_scale": 2.5,
100
+ "num_images": 1,
101
+ "output_format": "png",
102
+ },
103
+ "supports": {
104
+ "prompt", "image_size", "num_inference_steps", "guidance_scale",
105
+ "num_images", "output_format", "seed", "sync_mode",
106
+ },
107
+ },
108
+ }
109
+
110
+ VALID_ASPECT_RATIOS = ("landscape", "square", "portrait")
111
+
112
+
113
+ def _load_fal_client():
114
+ try:
115
+ from echo_agent.dependencies import ensure
116
+ ensure("tool.image-gen-fal")
117
+ except Exception:
118
+ pass
119
+ import fal_client # type: ignore
120
+ return fal_client
121
+
122
+
123
+ _fal_env_lock = threading.Lock()
124
+
125
+
126
+ def _build_payload(model_id: str, prompt: str, aspect_ratio: str) -> dict[str, Any]:
127
+ meta = FAL_MODELS[model_id]
128
+ size_style = meta["size_style"]
129
+ sizes = meta["sizes"]
130
+
131
+ aspect = aspect_ratio if aspect_ratio in sizes else "landscape"
132
+ payload: dict[str, Any] = dict(meta.get("defaults", {}))
133
+ payload["prompt"] = prompt.strip()
134
+
135
+ if size_style == "image_size_preset":
136
+ payload["image_size"] = sizes[aspect]
137
+ elif size_style == "aspect_ratio":
138
+ payload["aspect_ratio"] = sizes[aspect]
139
+
140
+ supports = meta["supports"]
141
+ return {k: v for k, v in payload.items() if k in supports}
142
+
143
+
144
+ class FalImageGenTool(Tool):
145
+ name = "image_generate"
146
+ description = "Generate an image from a text prompt using FAL.ai (supports FLUX, Ideogram, Recraft, Qwen and more)."
147
+ parameters = {
148
+ "type": "object",
149
+ "properties": {
150
+ "prompt": {"type": "string", "description": "Text description of the image to generate."},
151
+ "aspect_ratio": {
152
+ "type": "string",
153
+ "enum": ["landscape", "square", "portrait"],
154
+ "description": "Image aspect ratio.",
155
+ },
156
+ },
157
+ "required": ["prompt"],
158
+ }
159
+ timeout_seconds = 120
160
+
161
+ def __init__(self, fal_key: str = "", model: str = ""):
162
+ self._fal_key = fal_key
163
+ self._model = model or DEFAULT_MODEL
164
+
165
+ def is_ready(self) -> bool:
166
+ return bool(self._fal_key)
167
+
168
+ def readiness_detail(self) -> tuple[bool, str]:
169
+ if self._fal_key:
170
+ return True, "ok"
171
+ return False, "FAL.ai API key not configured"
172
+
173
+ async def execute(self, params: dict[str, Any], ctx: ToolExecutionContext | None = None) -> ToolResult:
174
+ if not self._fal_key:
175
+ return ToolResult(success=False, error="FAL.ai API key not configured")
176
+
177
+ prompt = params.get("prompt", "").strip()
178
+ if not prompt:
179
+ return ToolResult(success=False, error="prompt is required")
180
+
181
+ aspect_ratio = params.get("aspect_ratio", "landscape").lower().strip()
182
+ if aspect_ratio not in VALID_ASPECT_RATIOS:
183
+ aspect_ratio = "landscape"
184
+
185
+ model_id = self._model
186
+ if model_id not in FAL_MODELS:
187
+ supported = ", ".join(sorted(FAL_MODELS.keys()))
188
+ return ToolResult(
189
+ success=False,
190
+ error=f"Unknown FAL model '{model_id}'. Supported models: {supported}",
191
+ )
192
+
193
+ arguments = _build_payload(model_id, prompt, aspect_ratio)
194
+
195
+ try:
196
+ fal = _load_fal_client()
197
+ except ImportError:
198
+ return ToolResult(
199
+ success=False,
200
+ error="fal-client package not installed. Run: pip install fal-client",
201
+ )
202
+
203
+ def _submit_with_key():
204
+ with _fal_env_lock:
205
+ prev_key = os.environ.get("FAL_KEY")
206
+ os.environ["FAL_KEY"] = self._fal_key
207
+ try:
208
+ handler = fal.submit(model_id, arguments=arguments)
209
+ return handler.get()
210
+ finally:
211
+ if prev_key is not None:
212
+ os.environ["FAL_KEY"] = prev_key
213
+ else:
214
+ os.environ.pop("FAL_KEY", None)
215
+
216
+ try:
217
+ result = await asyncio.to_thread(_submit_with_key)
218
+ except Exception as e:
219
+ return ToolResult(success=False, error=f"FAL.ai generation failed: {e}")
220
+
221
+ images = result.get("images") if isinstance(result, dict) else None
222
+ if not images:
223
+ return ToolResult(success=False, error="No images returned from FAL.ai")
224
+
225
+ image_url = images[0].get("url", "")
226
+ width = images[0].get("width", "")
227
+ height = images[0].get("height", "")
228
+ output = f"Image URL: {image_url}"
229
+ if width and height:
230
+ output += f"\nSize: {width}x{height}"
231
+ return ToolResult(output=output, metadata={"url": image_url})
@@ -230,10 +230,11 @@ class AppRuntime:
230
230
  the storage close — from running.
231
231
  """
232
232
 
233
- def __init__(self, ctx: BootstrapResult):
233
+ def __init__(self, ctx: BootstrapResult, shutdown_event: asyncio.Event | None = None):
234
234
  self._ctx = ctx
235
235
  self._gateway: Any = None
236
236
  self._started = False
237
+ self._shutdown_event = shutdown_event
237
238
 
238
239
  @property
239
240
  def gateway(self) -> Any:
@@ -271,6 +272,8 @@ class AppRuntime:
271
272
  agent_loop=ctx.agent,
272
273
  a2a_config=ctx.config.a2a,
273
274
  )
275
+ if self._shutdown_event:
276
+ self._gateway.set_shutdown_event(self._shutdown_event)
274
277
  await self._gateway.start()
275
278
  logger.info("Gateway started on {}:{}", ctx.config.gateway.host, ctx.config.gateway.port)
276
279
  return True
@@ -313,7 +316,7 @@ async def run(config_path: str | None = None, workspace: str | None = None) -> N
313
316
  logger.info("Echo Agent starting — workspace: {}", ctx.workspace)
314
317
 
315
318
  install_signal_handler(shutdown)
316
- runtime = AppRuntime(ctx)
319
+ runtime = AppRuntime(ctx, shutdown_event=shutdown)
317
320
  try:
318
321
  if not await runtime.start():
319
322
  return
@@ -347,7 +350,7 @@ async def run_gateway(
347
350
  ctx.config.gateway.port = port
348
351
 
349
352
  install_signal_handler(shutdown)
350
- runtime = AppRuntime(ctx)
353
+ runtime = AppRuntime(ctx, shutdown_event=shutdown)
351
354
  try:
352
355
  if not await runtime.start():
353
356
  return
@@ -21,6 +21,7 @@ class SendResult:
21
21
  success: bool
22
22
  message_id: str = ""
23
23
  error: str = ""
24
+ skipped: bool = False
24
25
 
25
26
 
26
27
  class BaseChannel(ABC):
@@ -52,6 +53,20 @@ class BaseChannel(ABC):
52
53
  async def send(self, event: OutboundEvent) -> SendResult | None:
53
54
  """Send a message through this channel."""
54
55
 
56
+ def should_deliver(self, event: OutboundEvent) -> bool:
57
+ """Return False to silently skip non-final messages on channels that cannot edit.
58
+
59
+ Channels that support message editing (Telegram, Discord) can show progress
60
+ then overwrite it with the final response. Channels without edit support would
61
+ deliver every intermediate chunk as a separate, irrevocable message — confusing
62
+ the user with duplicates. This guard prevents that at the base layer.
63
+ """
64
+ if self.supports_edit:
65
+ return True
66
+ if event.is_final:
67
+ return True
68
+ return False
69
+
55
70
  async def edit_message(
56
71
  self,
57
72
  chat_id: str,
@@ -56,6 +56,8 @@ class DingTalkChannel(BaseChannel):
56
56
  await self._session.close()
57
57
 
58
58
  async def send(self, event: OutboundEvent) -> SendResult | None:
59
+ if not self.should_deliver(event):
60
+ return SendResult(success=True, skipped=True)
59
61
  text = event.text or ""
60
62
  if not text or not self._session:
61
63
  return SendResult(success=False, error="no text or no session")
@@ -44,6 +44,8 @@ class EmailChannel(BaseChannel):
44
44
  pass
45
45
 
46
46
  async def send(self, event: OutboundEvent) -> SendResult | None:
47
+ if not self.should_deliver(event):
48
+ return SendResult(success=True, skipped=True)
47
49
  text = event.text or ""
48
50
  if not text:
49
51
  return SendResult(success=False, error="no text")
@@ -56,6 +56,8 @@ class FeishuChannel(BaseChannel):
56
56
  await self._session.close()
57
57
 
58
58
  async def send(self, event: OutboundEvent) -> SendResult | None:
59
+ if not self.should_deliver(event):
60
+ return SendResult(success=True, skipped=True)
59
61
  text = event.text or ""
60
62
  if not text or not self._session:
61
63
  return SendResult(success=False, error="no text or no session")
@@ -54,6 +54,8 @@ class MatrixChannel(BaseChannel):
54
54
  await self._session.close()
55
55
 
56
56
  async def send(self, event: OutboundEvent) -> SendResult | None:
57
+ if not self.should_deliver(event):
58
+ return SendResult(success=True, skipped=True)
57
59
  text = event.text or ""
58
60
  if not text or not self._session:
59
61
  return SendResult(success=False, error="no text or no session")
@@ -191,6 +191,8 @@ class QQBotChannel(BaseChannel):
191
191
  # ── Send ─────────────────────────────────────────────────────────────────
192
192
 
193
193
  async def send(self, event: OutboundEvent) -> SendResult | None:
194
+ if not self.should_deliver(event):
195
+ return SendResult(success=True, skipped=True)
194
196
  if not self._session:
195
197
  return SendResult(success=False, error="session not initialized")
196
198
  await self._ensure_token()
@@ -43,6 +43,8 @@ class WebhookChannel(BaseChannel):
43
43
  await self._runner.cleanup()
44
44
 
45
45
  async def send(self, event: OutboundEvent) -> SendResult | None:
46
+ if not self.should_deliver(event):
47
+ return SendResult(success=True, skipped=True)
46
48
  future = self._pending_responses.pop(event.reply_to_id or "", None)
47
49
  if future and not future.done():
48
50
  future.set_result(event.text)
@@ -55,6 +55,8 @@ class WeComChannel(BaseChannel):
55
55
  await self._session.close()
56
56
 
57
57
  async def send(self, event: OutboundEvent) -> SendResult | None:
58
+ if not self.should_deliver(event):
59
+ return SendResult(success=True, skipped=True)
58
60
  text = event.text or ""
59
61
  if not text or not self._session:
60
62
  return SendResult(success=False, error="no text or no session")
@@ -367,6 +367,8 @@ class WeixinChannel(BaseChannel):
367
367
  # ── Send ─────────────────────────────────────────────────────────────────
368
368
 
369
369
  async def send(self, event: OutboundEvent) -> SendResult | None:
370
+ if not self.should_deliver(event):
371
+ return SendResult(success=True, skipped=True)
370
372
  text = event.text or ""
371
373
  if not text or not self._send_session or not self._token:
372
374
  return SendResult(success=False, error="no text, no session, or no token")