cade-cli 0.19.2__tar.gz → 0.20.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 (222) hide show
  1. {cade_cli-0.19.2 → cade_cli-0.20.0}/PKG-INFO +8 -4
  2. {cade_cli-0.19.2 → cade_cli-0.20.0}/README.md +3 -3
  3. {cade_cli-0.19.2 → cade_cli-0.20.0}/pyproject.toml +6 -1
  4. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/app.py +5 -0
  5. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/runner_main.py +3 -0
  6. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/server.py +4 -0
  7. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/execution/orchestrator.py +110 -61
  8. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/execution/parallel.py +70 -53
  9. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/.claude-plugin/plugin.json +1 -1
  10. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/.cursor-plugin/plugin.json +1 -1
  11. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/providers/anthropic.py +19 -0
  12. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/providers/ollama.py +15 -4
  13. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/providers/openai.py +48 -5
  14. cade_cli-0.20.0/src/cadecoder/telemetry/__init__.py +45 -0
  15. cade_cli-0.20.0/src/cadecoder/telemetry/otel.py +349 -0
  16. {cade_cli-0.19.2 → cade_cli-0.20.0}/.gitignore +0 -0
  17. {cade_cli-0.19.2 → cade_cli-0.20.0}/LICENSE +0 -0
  18. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/__init__.py +0 -0
  19. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/__main__.py +0 -0
  20. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/_metadata.py +0 -0
  21. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/config.py +0 -0
  22. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/errors.py +0 -0
  23. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/grants.py +0 -0
  24. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/resources.py +0 -0
  25. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/sandbox.py +0 -0
  26. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/server.py +0 -0
  27. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/__init__.py +0 -0
  28. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/_read_cache.py +0 -0
  29. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/access.py +0 -0
  30. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/agents.py +0 -0
  31. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/filesystem.py +0 -0
  32. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/git.py +0 -0
  33. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/notifications.py +0 -0
  34. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/questions.py +0 -0
  35. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/results.py +0 -0
  36. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/schemas.py +0 -0
  37. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/search.py +0 -0
  38. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/shell.py +0 -0
  39. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/tasks.py +0 -0
  40. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/web_fetch.py +0 -0
  41. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/tools/web_search.py +0 -0
  42. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/utils.py +0 -0
  43. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/__init__.py +0 -0
  44. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/arcade_client.py +0 -0
  45. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/cache.py +0 -0
  46. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/extract.py +0 -0
  47. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/markdown.py +0 -0
  48. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/preapproved.py +0 -0
  49. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/prompts.py +0 -0
  50. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/scrape.py +0 -0
  51. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/search/__init__.py +0 -0
  52. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/search/arcade.py +0 -0
  53. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/search/base.py +0 -0
  54. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cade_mcp_local/web/search/http.py +0 -0
  55. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/__init__.py +0 -0
  56. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ai/__init__.py +0 -0
  57. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ai/prompts.py +0 -0
  58. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/__init__.py +0 -0
  59. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/auth.py +0 -0
  60. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/__init__.py +0 -0
  61. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/account.py +0 -0
  62. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/agent_group.py +0 -0
  63. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/agents.py +0 -0
  64. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/auth.py +0 -0
  65. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/chat.py +0 -0
  66. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/cron.py +0 -0
  67. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/daemon.py +0 -0
  68. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/grant.py +0 -0
  69. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/hooks.py +0 -0
  70. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/mcp.py +0 -0
  71. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/mem.py +0 -0
  72. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/tasks.py +0 -0
  73. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/thread.py +0 -0
  74. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/commands/tools.py +0 -0
  75. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/mcp_hosts.py +0 -0
  76. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/onboarding.py +0 -0
  77. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/cli/ui.py +0 -0
  78. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/__init__.py +0 -0
  79. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/agent.py +0 -0
  80. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/binary.py +0 -0
  81. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/comms.py +0 -0
  82. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/config.py +0 -0
  83. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/constants.py +0 -0
  84. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/errors.py +0 -0
  85. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/frozen.py +0 -0
  86. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/git.py +0 -0
  87. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/inbox.py +0 -0
  88. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/logging.py +0 -0
  89. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/memory.py +0 -0
  90. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/model_specs.py +0 -0
  91. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/names.py +0 -0
  92. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/paths.py +0 -0
  93. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/runtime.py +0 -0
  94. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/secrets.py +0 -0
  95. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/spawn.py +0 -0
  96. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/text.py +0 -0
  97. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/core/types.py +0 -0
  98. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/__init__.py +0 -0
  99. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/adapters/__init__.py +0 -0
  100. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/adapters/base.py +0 -0
  101. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/adapters/factory.py +0 -0
  102. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/adapters/imessage.py +0 -0
  103. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/adapters/registry.py +0 -0
  104. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/adapters/stub.py +0 -0
  105. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/adapters/telegram.py +0 -0
  106. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/allowlist.py +0 -0
  107. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/attach.py +0 -0
  108. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/chunker.py +0 -0
  109. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/client.py +0 -0
  110. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/commands.py +0 -0
  111. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/config.py +0 -0
  112. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/control.py +0 -0
  113. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/cron.py +0 -0
  114. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/elicitation.py +0 -0
  115. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/ensure.py +0 -0
  116. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/format.py +0 -0
  117. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/__init__.py +0 -0
  118. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/app.py +0 -0
  119. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/auth.py +0 -0
  120. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/constants.py +0 -0
  121. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/elicitation.py +0 -0
  122. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/exposure.py +0 -0
  123. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/host_agent.py +0 -0
  124. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/limits.py +0 -0
  125. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/middleware.py +0 -0
  126. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/resources.py +0 -0
  127. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/subscriptions.py +0 -0
  128. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/gateway/turn_bridge.py +0 -0
  129. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/grants.py +0 -0
  130. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/inbox.py +0 -0
  131. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/inbox_cron.py +0 -0
  132. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/install.py +0 -0
  133. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/lock.py +0 -0
  134. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/mesh.py +0 -0
  135. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/message_pump.py +0 -0
  136. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/observability.py +0 -0
  137. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/owner.py +0 -0
  138. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/progress_sink.py +0 -0
  139. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/registry.py +0 -0
  140. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/router.py +0 -0
  141. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/runner.py +0 -0
  142. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/secrets.py +0 -0
  143. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/secrets_broker.py +0 -0
  144. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/services.py +0 -0
  145. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/sinks.py +0 -0
  146. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/supervisor.py +0 -0
  147. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/tokens.py +0 -0
  148. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/traffic.py +0 -0
  149. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/turns.py +0 -0
  150. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/daemon/wake.py +0 -0
  151. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/execution/__init__.py +0 -0
  152. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/execution/context_window.py +0 -0
  153. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/execution/mcp_schema_index.py +0 -0
  154. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/execution/tool_result_store.py +0 -0
  155. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/execution/tool_schema_cache.py +0 -0
  156. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/__init__.py +0 -0
  157. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/config.py +0 -0
  158. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/engine.py +0 -0
  159. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/executors/__init__.py +0 -0
  160. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/executors/callback.py +0 -0
  161. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/executors/command.py +0 -0
  162. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/executors/http.py +0 -0
  163. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/executors/prompt.py +0 -0
  164. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/executors/ssrf_guard.py +0 -0
  165. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/matchers.py +0 -0
  166. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/registry.py +0 -0
  167. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/hooks/types.py +0 -0
  168. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/__init__.py +0 -0
  169. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/.claude-plugin/marketplace.json +0 -0
  170. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/.cursor-plugin/marketplace.json +0 -0
  171. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/agents/operator.md +0 -0
  172. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/commands/ask.md +0 -0
  173. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/commands/memory.md +0 -0
  174. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/commands/tasks.md +0 -0
  175. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/mcp.json +0 -0
  176. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/rules/gateway.mdc +0 -0
  177. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/skills/ask/SKILL.md +0 -0
  178. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/skills/memory/SKILL.md +0 -0
  179. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/assets/skills/tasks/SKILL.md +0 -0
  180. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/doctor.py +0 -0
  181. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/install.py +0 -0
  182. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/paths.py +0 -0
  183. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/host_plugin/state.py +0 -0
  184. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/providers/__init__.py +0 -0
  185. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/providers/base.py +0 -0
  186. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/providers/http_client.py +0 -0
  187. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/storage/__init__.py +0 -0
  188. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/storage/threads.py +0 -0
  189. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tasks/__init__.py +0 -0
  190. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tasks/scheduler.py +0 -0
  191. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tasks/store.py +0 -0
  192. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tasks/types.py +0 -0
  193. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/templates/login_failed.html +0 -0
  194. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/templates/login_success.html +0 -0
  195. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/templates/serve/launchd.plist.tmpl +0 -0
  196. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/templates/serve/serve.toml.example +0 -0
  197. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/templates/serve/systemd.service.tmpl +0 -0
  198. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/templates/styles.css +0 -0
  199. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/__init__.py +0 -0
  200. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/manager/__init__.py +0 -0
  201. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/manager/_request_ctx.py +0 -0
  202. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/manager/base.py +0 -0
  203. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/manager/composite.py +0 -0
  204. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/manager/config.py +0 -0
  205. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/manager/mcp.py +0 -0
  206. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/search/__init__.py +0 -0
  207. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/search/discovered.py +0 -0
  208. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/search/scoring.py +0 -0
  209. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/tools/search/service.py +0 -0
  210. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ui/__init__.py +0 -0
  211. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ui/completion.py +0 -0
  212. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ui/display.py +0 -0
  213. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ui/elicitation.py +0 -0
  214. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ui/input.py +0 -0
  215. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ui/session.py +0 -0
  216. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/ui/slash.py +0 -0
  217. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/voice/__init__.py +0 -0
  218. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/voice/audio.py +0 -0
  219. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/voice/cleanup.py +0 -0
  220. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/voice/session.py +0 -0
  221. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/voice/stt.py +0 -0
  222. {cade_cli-0.19.2 → cade_cli-0.20.0}/src/cadecoder/voice/tts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cade-cli
3
- Version: 0.19.2
3
+ Version: 0.20.0
4
4
  Summary: Cade - The CLI Agent from Arcade.dev (install: curl -fsSL https://arcadeagent.dev/install | sh)
5
5
  Project-URL: Homepage, https://arcade.dev
6
6
  Project-URL: Documentation, https://arcadeagent.dev/docs
@@ -76,6 +76,10 @@ Requires-Dist: ruff<1.0.0,>=0.5.0; extra == 'dev'
76
76
  Provides-Extra: docs
77
77
  Requires-Dist: mkdocs-material<10.0.0,>=9.5.0; extra == 'docs'
78
78
  Requires-Dist: mkdocs<2.0.0,>=1.6.0; extra == 'docs'
79
+ Provides-Extra: otel
80
+ Requires-Dist: opentelemetry-api<2.0.0,>=1.27.0; extra == 'otel'
81
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http<2.0.0,>=1.27.0; extra == 'otel'
82
+ Requires-Dist: opentelemetry-sdk<2.0.0,>=1.27.0; extra == 'otel'
79
83
  Provides-Extra: training
80
84
  Requires-Dist: accelerate>=0.27.0; extra == 'training'
81
85
  Requires-Dist: safetensors>=0.4.2; extra == 'training'
@@ -137,9 +141,9 @@ CADE_VERSION=0.16.9 CADE_INSTALL_DIR=/usr/local/bin \
137
141
  sh -c "$(curl -fsSL https://arcadeagent.dev/install)"
138
142
  ```
139
143
 
140
- On Linux the installer auto-picks a **cpu** or **gpu** build (GPU when
141
- `nvidia-smi` detects a card; the GPU build bundles the CUDA stack and is a much
142
- larger download). Force it with `CADE_VARIANT=cpu` or `CADE_VARIANT=gpu`.
144
+ On Linux the installer ships the **cpu** build today (gpu release assets are
145
+ paused). Force a variant later with `CADE_VARIANT=cpu` or `CADE_VARIANT=gpu`
146
+ when gpu builds return.
143
147
 
144
148
  ### Alternatives
145
149
 
@@ -46,9 +46,9 @@ CADE_VERSION=0.16.9 CADE_INSTALL_DIR=/usr/local/bin \
46
46
  sh -c "$(curl -fsSL https://arcadeagent.dev/install)"
47
47
  ```
48
48
 
49
- On Linux the installer auto-picks a **cpu** or **gpu** build (GPU when
50
- `nvidia-smi` detects a card; the GPU build bundles the CUDA stack and is a much
51
- larger download). Force it with `CADE_VARIANT=cpu` or `CADE_VARIANT=gpu`.
49
+ On Linux the installer ships the **cpu** build today (gpu release assets are
50
+ paused). Force a variant later with `CADE_VARIANT=cpu` or `CADE_VARIANT=gpu`
51
+ when gpu builds return.
52
52
 
53
53
  ### Alternatives
54
54
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cade-cli"
3
- version = "0.19.2"
3
+ version = "0.20.0"
4
4
  description = "Cade - The CLI Agent from Arcade.dev (install: curl -fsSL https://arcadeagent.dev/install | sh)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -85,6 +85,11 @@ voice = [
85
85
  # PyInstaller (see .github/workflows/release.yml), and cade.spec bundles it
86
86
  # so misaki never downloads it at runtime in the frozen app.
87
87
  ]
88
+ otel = [
89
+ "opentelemetry-api>=1.27.0,<2.0.0",
90
+ "opentelemetry-sdk>=1.27.0,<2.0.0",
91
+ "opentelemetry-exporter-otlp-proto-http>=1.27.0,<2.0.0",
92
+ ]
88
93
  training = [
89
94
  "torch>=2.1.0",
90
95
  "transformers>=4.38.0",
@@ -365,6 +365,11 @@ def main_callback(
365
365
  ctx.ensure_object(dict)
366
366
  ctx.obj["verbose"] = verbose
367
367
 
368
+ # Optional OpenTelemetry (CADE_OTEL=1 + cade-cli[otel]). No-op otherwise.
369
+ from cadecoder.telemetry import init_telemetry
370
+
371
+ init_telemetry()
372
+
368
373
  # Agent management and inspection commands manage their own (possibly empty)
369
374
  # agent state and must not auto-create a `default` agent — that would clobber
370
375
  # the empty-state UX of `cade list` / `cade status` on a fresh install.
@@ -27,6 +27,9 @@ async def _serve(agent_name: str, fd: int) -> None:
27
27
  from cadecoder.core import agent as agent_mod
28
28
  from cadecoder.daemon.control import ControlServer
29
29
  from cadecoder.daemon.runner import InProcessRunner
30
+ from cadecoder.telemetry import init_telemetry
31
+
32
+ init_telemetry()
30
33
 
31
34
  agent = agent_mod.resolve(agent_name)
32
35
  if agent is None:
@@ -115,6 +115,10 @@ async def run_daemon(
115
115
  # Same SSL_CERT_FILE footgun as the CLI — scrub before Telegram/MCP/Anthropic.
116
116
  prepare_ssl_env()
117
117
 
118
+ from cadecoder.telemetry import init_telemetry
119
+
120
+ init_telemetry()
121
+
118
122
  host, port = _parse_bind(bind)
119
123
  _guard_auth_bind(host, require_auth)
120
124
  prewarm = prewarm or []
@@ -246,9 +246,23 @@ class Orchestrator:
246
246
  )
247
247
 
248
248
  async def stream(self, context: ExecutionContext) -> AsyncIterator[ExecutionEvent]:
249
- """Stream execution events."""
250
- async for event in self._stream_inner(context):
251
- yield event
249
+ """Stream execution events, wrapping the turn in an OTEL span when enabled."""
250
+ from cadecoder.telemetry import set_span_output, trace_turn
251
+
252
+ with trace_turn(
253
+ thread_id=str(context.metadata.get("thread_id") or ""),
254
+ conversation_id=str(context.metadata.get("conversation_id") or ""),
255
+ agent=str(context.metadata.get("agent") or self._persona or ""),
256
+ model=self.default_model,
257
+ prompt=context.task,
258
+ ) as turn_span:
259
+ reply_parts: list[str] = []
260
+ async for event in self._stream_inner(context):
261
+ if event.type == ExecutionEventType.CONTENT and event.content:
262
+ reply_parts.append(event.content)
263
+ yield event
264
+ if reply_parts:
265
+ set_span_output(turn_span, "".join(reply_parts))
252
266
 
253
267
  async def _stream_inner(self, context: ExecutionContext) -> AsyncIterator[ExecutionEvent]:
254
268
  """Core streaming implementation."""
@@ -368,69 +382,104 @@ class Orchestrator:
368
382
  invalid_tool_pairs = False
369
383
  stream_error = False
370
384
 
385
+ from cadecoder.telemetry import (
386
+ record_message_parts,
387
+ record_usage,
388
+ set_span_output,
389
+ trace_llm_call,
390
+ )
391
+
392
+ provider_name = ""
371
393
  try:
372
- async for stream_event in self.provider.stream(request):
373
- # Handle error events from the provider
374
- if stream_event.type == "error" and stream_event.content:
375
- error_text = str(stream_event.content)
376
- if "prompt is too long" in error_text or (
377
- "too long" in error_text and "token" in error_text
378
- ):
379
- log.warning(
380
- f"Prompt too long for API, forcing compaction: {error_text[:200]}"
381
- )
382
- prompt_too_long = True
383
- break
384
- elif "tool_use" in error_text and "tool_result" in error_text:
385
- # Orphaned tool_use/tool_result from bad compaction
386
- log.warning(
387
- f"Invalid tool_use/tool_result pairing, "
388
- f"attempting repair: {error_text[:200]}"
389
- )
390
- invalid_tool_pairs = True
391
- break
392
- else:
393
- # Surface non-recoverable stream errors to the user
394
- log.error(f"Provider stream error: {error_text[:300]}")
394
+ provider_name = str(getattr(self.provider, "provider_type", "") or "")
395
+ except Exception: # noqa: BLE001
396
+ provider_name = ""
397
+
398
+ with trace_llm_call(
399
+ model=self.default_model,
400
+ iteration=iteration,
401
+ provider=provider_name,
402
+ estimated_prompt_tokens=context_status.get("token_count"),
403
+ ) as llm_span:
404
+ record_message_parts(
405
+ llm_span,
406
+ messages,
407
+ chars_per_token=self.context_manager.chars_per_token,
408
+ )
409
+ try:
410
+ async for stream_event in self.provider.stream(request):
411
+ usage = stream_event.metadata.get("usage")
412
+ if usage:
413
+ record_usage(llm_span, usage)
414
+
415
+ # Handle error events from the provider
416
+ if stream_event.type == "error" and stream_event.content:
417
+ error_text = str(stream_event.content)
418
+ if "prompt is too long" in error_text or (
419
+ "too long" in error_text and "token" in error_text
420
+ ):
421
+ log.warning(
422
+ f"Prompt too long for API, forcing compaction: "
423
+ f"{error_text[:200]}"
424
+ )
425
+ prompt_too_long = True
426
+ break
427
+ elif "tool_use" in error_text and "tool_result" in error_text:
428
+ # Orphaned tool_use/tool_result from bad compaction
429
+ log.warning(
430
+ f"Invalid tool_use/tool_result pairing, "
431
+ f"attempting repair: {error_text[:200]}"
432
+ )
433
+ invalid_tool_pairs = True
434
+ break
435
+ else:
436
+ # Surface non-recoverable stream errors to the user
437
+ log.error(f"Provider stream error: {error_text[:300]}")
438
+ yield ExecutionEvent(
439
+ type=ExecutionEventType.CONTENT,
440
+ content=f"\n\nError from provider: {error_text}\n",
441
+ )
442
+ stream_error = True
443
+ break
444
+
445
+ if stream_event.content:
446
+ accumulated_content += stream_event.content
395
447
  yield ExecutionEvent(
396
448
  type=ExecutionEventType.CONTENT,
397
- content=f"\n\nError from provider: {error_text}\n",
449
+ content=stream_event.content,
398
450
  )
399
- stream_error = True
400
- break
401
-
402
- if stream_event.content:
403
- accumulated_content += stream_event.content
404
- yield ExecutionEvent(
405
- type=ExecutionEventType.CONTENT,
406
- content=stream_event.content,
407
- )
408
451
 
409
- # Handle tool call events from provider
410
- if stream_event.type == ExecutionEventType.TOOL_CALL.value:
411
- tool_call = stream_event.metadata.get("tool_call")
412
- if tool_call:
413
- tool_calls.append(tool_call)
414
- # Surface read-only classification so the UI can
415
- # render mutating calls more prominently than
416
- # idempotent reads. Best-effort — old/incompatible
417
- # tool managers just won't set the flag.
418
- tc_name = tool_call.get("function", {}).get("name", "")
419
- is_read_only = self._classify_read_only(tc_name)
420
- event_meta: dict[str, Any] = {"tool_call": tool_call}
421
- if is_read_only is not None:
422
- event_meta["is_read_only"] = is_read_only
423
- yield ExecutionEvent(
424
- type=ExecutionEventType.TOOL_CALL,
425
- metadata=event_meta,
426
- )
427
- except Exception as e:
428
- log.error(f"Provider stream failed: {e}")
429
- yield ExecutionEvent(
430
- type=ExecutionEventType.CONTENT,
431
- content=f"\n\nProvider error: {e}\n",
432
- )
433
- stream_error = True
452
+ # Handle tool call events from provider
453
+ if stream_event.type == ExecutionEventType.TOOL_CALL.value:
454
+ tool_call = stream_event.metadata.get("tool_call")
455
+ if tool_call:
456
+ tool_calls.append(tool_call)
457
+ # Surface read-only classification so the UI can
458
+ # render mutating calls more prominently than
459
+ # idempotent reads. Best-effort — old/incompatible
460
+ # tool managers just won't set the flag.
461
+ tc_name = tool_call.get("function", {}).get("name", "")
462
+ is_read_only = self._classify_read_only(tc_name)
463
+ event_meta: dict[str, Any] = {"tool_call": tool_call}
464
+ if is_read_only is not None:
465
+ event_meta["is_read_only"] = is_read_only
466
+ yield ExecutionEvent(
467
+ type=ExecutionEventType.TOOL_CALL,
468
+ metadata=event_meta,
469
+ )
470
+ except Exception as e:
471
+ log.error(f"Provider stream failed: {e}")
472
+ yield ExecutionEvent(
473
+ type=ExecutionEventType.CONTENT,
474
+ content=f"\n\nProvider error: {e}\n",
475
+ )
476
+ stream_error = True
477
+ finally:
478
+ if accumulated_content:
479
+ set_span_output(llm_span, accumulated_content)
480
+ if tool_calls:
481
+ if llm_span is not None:
482
+ llm_span.set_attribute("cade.tool_call_count", len(tool_calls))
434
483
 
435
484
  # Stop the execution loop on non-recoverable stream errors
436
485
  if stream_error:
@@ -194,78 +194,95 @@ class AsyncToolExecutor:
194
194
  timeout: float = 120.0,
195
195
  ) -> ToolExecutionResult:
196
196
  """Execute a single tool with timeout and cancellation support."""
197
+ from cadecoder.telemetry import set_tool_result, trace_tool_call
198
+
197
199
  function = tool_call.get("function", {})
198
200
  name = function.get("name", "unknown")
199
201
  tool_call_id = tool_call.get("id", "unknown")
202
+ args_str = function.get("arguments", "{}") or "{}"
200
203
 
201
204
  try:
202
- args_str = function.get("arguments", "{}") or "{}"
203
205
  args = json.loads(args_str)
204
- log.debug(f"Executing tool: {name} with args: {args}")
206
+ except json.JSONDecodeError:
207
+ args = args_str # span preview only; execute path will error below
205
208
 
209
+ with trace_tool_call(name=name, tool_call_id=tool_call_id, arguments=args) as tool_span:
206
210
  try:
207
- result_content = await asyncio.wait_for(
208
- self.tool_manager.execute(name, args),
209
- timeout=timeout,
210
- )
211
- except TimeoutError:
212
- log.warning(f"Tool {name} timed out after {timeout}s")
213
- if isinstance(name, str) and name.startswith("Memory_"):
214
- content = (
215
- f"Memory tool timed out after {timeout:.0f}s (often a cold "
216
- "embedder load). Retry Memory_SearchLibrary once; do not fall "
217
- "back to Local_* filesystem tools for library recall."
211
+ if not isinstance(args, dict):
212
+ raise ValueError(f"Tool arguments must be a JSON object, got: {args_str[:120]}")
213
+ log.debug(f"Executing tool: {name} with args: {args}")
214
+
215
+ try:
216
+ result_content = await asyncio.wait_for(
217
+ self.tool_manager.execute(name, args),
218
+ timeout=timeout,
218
219
  )
219
- else:
220
- content = f"Tool execution timed out after {timeout} seconds"
220
+ except TimeoutError:
221
+ log.warning(f"Tool {name} timed out after {timeout}s")
222
+ if isinstance(name, str) and name.startswith("Memory_"):
223
+ content = (
224
+ f"Memory tool timed out after {timeout:.0f}s (often a cold "
225
+ "embedder load). Retry Memory_SearchLibrary once; do not fall "
226
+ "back to Local_* filesystem tools for library recall."
227
+ )
228
+ else:
229
+ content = f"Tool execution timed out after {timeout} seconds"
230
+ set_tool_result(tool_span, status="error", content=content, error="timeout")
231
+ return ToolExecutionResult(
232
+ tool_call_id=tool_call_id,
233
+ name=name,
234
+ content=content,
235
+ status="error",
236
+ error="timeout",
237
+ authorization_url=None,
238
+ )
239
+ except asyncio.CancelledError:
240
+ log.info(f"Tool {name} was cancelled")
241
+ set_tool_result(
242
+ tool_span,
243
+ status="cancelled",
244
+ content="Tool execution was cancelled",
245
+ error="cancelled",
246
+ )
247
+ return ToolExecutionResult(
248
+ tool_call_id=tool_call_id,
249
+ name=name,
250
+ content="Tool execution was cancelled",
251
+ status="cancelled",
252
+ error="cancelled",
253
+ authorization_url=None,
254
+ )
255
+
256
+ actual_content = extract_tool_output_content(result_content)
257
+ set_tool_result(tool_span, status="success", content=actual_content)
221
258
  return ToolExecutionResult(
222
259
  tool_call_id=tool_call_id,
223
260
  name=name,
224
- content=content,
225
- status="error",
226
- error="timeout",
261
+ content=actual_content,
262
+ status="success",
263
+ error=None,
227
264
  authorization_url=None,
228
265
  )
229
- except asyncio.CancelledError:
230
- log.info(f"Tool {name} was cancelled")
266
+
267
+ except Exception as e:
268
+ auth_url = None
269
+ if isinstance(e, ToolAuthorizationRequired):
270
+ auth_url = e.authorization_url
271
+ log.error(f"Authorization required for tool {name}")
272
+ else:
273
+ log.error(f"Tool execution failed for {name}: {e}")
274
+ log.exception(e)
275
+
276
+ set_tool_result(tool_span, status="error", content=str(e), error=str(e))
231
277
  return ToolExecutionResult(
232
278
  tool_call_id=tool_call_id,
233
279
  name=name,
234
- content="Tool execution was cancelled",
235
- status="cancelled",
236
- error="cancelled",
237
- authorization_url=None,
280
+ content=str(e),
281
+ status="error",
282
+ error=str(e),
283
+ authorization_url=auth_url,
238
284
  )
239
285
 
240
- actual_content = extract_tool_output_content(result_content)
241
-
242
- return ToolExecutionResult(
243
- tool_call_id=tool_call_id,
244
- name=name,
245
- content=actual_content,
246
- status="success",
247
- error=None,
248
- authorization_url=None,
249
- )
250
-
251
- except Exception as e:
252
- auth_url = None
253
- if isinstance(e, ToolAuthorizationRequired):
254
- auth_url = e.authorization_url
255
- log.error(f"Authorization required for tool {name}")
256
- else:
257
- log.error(f"Tool execution failed for {name}: {e}")
258
- log.exception(e)
259
-
260
- return ToolExecutionResult(
261
- tool_call_id=tool_call_id,
262
- name=name,
263
- content=str(e),
264
- status="error",
265
- error=str(e),
266
- authorization_url=auth_url,
267
- )
268
-
269
286
  def _analyze_tool_dependencies(self, tool_calls: ToolCallList) -> ToolGroups:
270
287
  """Analyze tool dependencies to determine execution groups.
271
288
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cade",
3
3
  "description": "Your persistent on-device agent: notes memory, durable tasks, and connected tools via Ask.",
4
- "version": "0.19.2",
4
+ "version": "0.20.0",
5
5
  "author": {
6
6
  "name": "Arcade.dev",
7
7
  "url": "https://docs.cade.dev"
@@ -2,7 +2,7 @@
2
2
  "name": "cade",
3
3
  "displayName": "Cade",
4
4
  "description": "Your persistent on-device agent: notes memory, durable tasks, and connected tools via Ask.",
5
- "version": "0.19.2",
5
+ "version": "0.20.0",
6
6
  "author": {
7
7
  "name": "Arcade.dev",
8
8
  "email": "support@arcade.dev"
@@ -329,6 +329,25 @@ class AnthropicProvider(Provider):
329
329
  },
330
330
  )
331
331
 
332
+ # Final message carries authoritative input/output token counts.
333
+ try:
334
+ final = await stream.get_final_message()
335
+ if final is not None and getattr(final, "usage", None) is not None:
336
+ usage = {
337
+ "prompt_tokens": int(final.usage.input_tokens or 0),
338
+ "completion_tokens": int(final.usage.output_tokens or 0),
339
+ "total_tokens": int(
340
+ (final.usage.input_tokens or 0) + (final.usage.output_tokens or 0)
341
+ ),
342
+ }
343
+ yield StreamEvent(
344
+ type=ExecutionEventType.COMPLETE.value,
345
+ content=None,
346
+ metadata={"usage": usage, "model": request.model},
347
+ )
348
+ except Exception as exc: # noqa: BLE001
349
+ log.debug(f"Anthropic stream usage unavailable: {exc}")
350
+
332
351
  @staticmethod
333
352
  def _default_max_tokens(request: ProviderRequest, streaming: bool) -> int:
334
353
  """Return the default output-token cap for a request without one.
@@ -354,13 +354,24 @@ class OllamaProvider(Provider):
354
354
  )
355
355
 
356
356
  if chunk.get("done"):
357
+ complete_meta: dict[str, Any] = {
358
+ "finish_reason": "tool_calls" if saw_tool_calls else "stop",
359
+ "model": request.model,
360
+ }
361
+ prompt_tokens = chunk.get("prompt_eval_count")
362
+ completion_tokens = chunk.get("eval_count")
363
+ if prompt_tokens is not None or completion_tokens is not None:
364
+ p = int(prompt_tokens or 0)
365
+ c = int(completion_tokens or 0)
366
+ complete_meta["usage"] = {
367
+ "prompt_tokens": p,
368
+ "completion_tokens": c,
369
+ "total_tokens": p + c,
370
+ }
357
371
  yield StreamEvent(
358
372
  type=ExecutionEventType.COMPLETE.value,
359
373
  content=None,
360
- metadata={
361
- "finish_reason": "tool_calls" if saw_tool_calls else "stop",
362
- "model": request.model,
363
- },
374
+ metadata=complete_meta,
364
375
  )
365
376
  break
366
377
 
@@ -217,13 +217,44 @@ class OpenAIProvider(Provider):
217
217
  """Yield one attempt's worth of stream events. Raises on error."""
218
218
  openai_request = self._build_openai_request(request)
219
219
  openai_request["stream"] = True
220
+ # Ask OpenAI (and compatible servers that honor it) for a final usage
221
+ # chunk so OTEL spans can record prompt/completion tokens. Some local
222
+ # OpenAI-compatible servers reject stream_options — fall back cleanly.
223
+ want_usage = os.environ.get("CADE_OTEL", "").strip().lower() in (
224
+ "1",
225
+ "true",
226
+ "yes",
227
+ "on",
228
+ )
229
+ if want_usage:
230
+ openai_request["stream_options"] = {"include_usage": True}
220
231
 
221
- stream = await self.client.chat.completions.create(**openai_request)
232
+ try:
233
+ stream = await self.client.chat.completions.create(**openai_request)
234
+ except Exception as exc:
235
+ if want_usage and "stream_options" in openai_request:
236
+ log.debug(
237
+ f"stream_options.include_usage rejected by endpoint; "
238
+ f"retrying without usage: {exc}"
239
+ )
240
+ openai_request.pop("stream_options", None)
241
+ stream = await self.client.chat.completions.create(**openai_request)
242
+ else:
243
+ raise
222
244
  current_tool_calls: dict[int, dict[str, Any]] = {}
245
+ usage: dict[str, int] | None = None
246
+ usage_emitted = False
223
247
 
224
248
  async for chunk in stream:
225
249
  chunk = cast(ChatCompletionChunk, chunk)
226
250
 
251
+ if chunk.usage is not None:
252
+ usage = {
253
+ "prompt_tokens": chunk.usage.prompt_tokens or 0,
254
+ "completion_tokens": chunk.usage.completion_tokens or 0,
255
+ "total_tokens": chunk.usage.total_tokens or 0,
256
+ }
257
+
227
258
  # Handle content delta
228
259
  if chunk.choices and chunk.choices[0].delta.content:
229
260
  yield StreamEvent(
@@ -259,15 +290,27 @@ class OpenAIProvider(Provider):
259
290
  metadata={"tool_call": tool_call},
260
291
  )
261
292
 
293
+ complete_meta: dict[str, Any] = {
294
+ "finish_reason": chunk.choices[0].finish_reason,
295
+ "model": chunk.model,
296
+ }
297
+ if usage:
298
+ complete_meta["usage"] = usage
299
+ usage_emitted = True
262
300
  yield StreamEvent(
263
301
  type=ExecutionEventType.COMPLETE.value,
264
302
  content=None,
265
- metadata={
266
- "finish_reason": chunk.choices[0].finish_reason,
267
- "model": chunk.model,
268
- },
303
+ metadata=complete_meta,
269
304
  )
270
305
 
306
+ # Usage often arrives on a trailing empty-choices chunk after finish.
307
+ if usage and not usage_emitted:
308
+ yield StreamEvent(
309
+ type=ExecutionEventType.COMPLETE.value,
310
+ content=None,
311
+ metadata={"usage": usage},
312
+ )
313
+
271
314
  def _build_openai_request(self, request: ProviderRequest) -> dict[str, Any]:
272
315
  """Build OpenAI API request from unified request.
273
316
 
@@ -0,0 +1,45 @@
1
+ """Optional OpenTelemetry tracing for Cade agent turns.
2
+
3
+ Enable with ``CADE_OTEL=1`` (requires the ``otel`` extra). Spans cover each
4
+ agent turn, every LLM call inside it, and every tool invocation — with
5
+ latency, tool names/args/status, and token usage when the provider reports it.
6
+
7
+ Export defaults to OTLP HTTP at ``http://127.0.0.1:6006/v1/traces`` (Arize
8
+ Phoenix). Override with the standard ``OTEL_EXPORTER_OTLP_*`` env vars.
9
+
10
+ Example::
11
+
12
+ CADE_OTEL=1 cade chat
13
+ # another terminal:
14
+ make otel-ui
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from cadecoder.telemetry.otel import (
20
+ init_telemetry,
21
+ is_enabled,
22
+ record_message_parts,
23
+ record_usage,
24
+ set_span_output,
25
+ set_tool_result,
26
+ shutdown_telemetry,
27
+ start_span,
28
+ trace_llm_call,
29
+ trace_tool_call,
30
+ trace_turn,
31
+ )
32
+
33
+ __all__ = [
34
+ "init_telemetry",
35
+ "is_enabled",
36
+ "record_message_parts",
37
+ "record_usage",
38
+ "set_span_output",
39
+ "set_tool_result",
40
+ "shutdown_telemetry",
41
+ "start_span",
42
+ "trace_llm_call",
43
+ "trace_tool_call",
44
+ "trace_turn",
45
+ ]