fruxon 2.0.2__tar.gz → 2.1.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 (216) hide show
  1. {fruxon-2.0.2 → fruxon-2.1.0}/HISTORY.md +29 -0
  2. {fruxon-2.0.2 → fruxon-2.1.0}/PKG-INFO +4 -1
  3. {fruxon-2.0.2 → fruxon-2.1.0}/README.md +3 -0
  4. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/__init__.py +6 -0
  5. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/_version.py +2 -2
  6. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/__init__.py +1 -0
  7. fruxon-2.1.0/src/fruxon/cli/agents_voice_calls.py +379 -0
  8. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/examples.py +15 -0
  9. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/llm_providers.py +261 -0
  10. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_ledger.py +144 -3
  11. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/fruxon.py +191 -0
  12. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/models.py +89 -0
  13. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-build-agent/SKILL.md +6 -0
  14. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +7 -0
  15. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-meet/SKILL.md +2 -0
  16. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_client.py +47 -0
  17. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_llm_provider_configs_cli.py +255 -0
  18. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_ledger.py +147 -0
  19. fruxon-2.1.0/tests/test_voice_calls.py +288 -0
  20. {fruxon-2.0.2 → fruxon-2.1.0}/.gitignore +0 -0
  21. {fruxon-2.0.2 → fruxon-2.1.0}/LICENSE +0 -0
  22. {fruxon-2.0.2 → fruxon-2.1.0}/pyproject.toml +0 -0
  23. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/__main__.py +0 -0
  24. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/_ssl.py +0 -0
  25. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/__init__.py +0 -0
  26. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/agent.py +0 -0
  27. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/ledger.py +0 -0
  28. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/references.py +0 -0
  29. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_crash.py +0 -0
  30. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_schema.py +0 -0
  31. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_shared.py +0 -0
  32. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_stream.py +0 -0
  33. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents.py +0 -0
  34. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_approvals.py +0 -0
  35. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_budget.py +0 -0
  36. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_channels.py +0 -0
  37. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_draft.py +0 -0
  38. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_evaluations.py +0 -0
  39. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_executions.py +0 -0
  40. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_improvements.py +0 -0
  41. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_lifecycle.py +0 -0
  42. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_memory.py +0 -0
  43. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_reply_style.py +0 -0
  44. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_revisions.py +0 -0
  45. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_sandbox.py +0 -0
  46. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_sandbox_test.py +0 -0
  47. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_slots.py +0 -0
  48. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_system_blocks.py +0 -0
  49. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_tests.py +0 -0
  50. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_topics.py +0 -0
  51. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/applications.py +0 -0
  52. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/applications_entry_points.py +0 -0
  53. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/assets.py +0 -0
  54. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/auth.py +0 -0
  55. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/capabilities.py +0 -0
  56. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/completion.py +0 -0
  57. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/config.py +0 -0
  58. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/consult_pins.py +0 -0
  59. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/conversation_imports.py +0 -0
  60. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/dash.py +0 -0
  61. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/describe.py +0 -0
  62. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/doctor.py +0 -0
  63. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/environments.py +0 -0
  64. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/escalations.py +0 -0
  65. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/guides.py +0 -0
  66. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/integrations.py +0 -0
  67. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/keys.py +0 -0
  68. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_bases.py +0 -0
  69. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_documents.py +0 -0
  70. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_notices.py +0 -0
  71. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_sources.py +0 -0
  72. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/messages.py +0 -0
  73. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/metrics.py +0 -0
  74. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/participants.py +0 -0
  75. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/pipelines.py +0 -0
  76. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/secrets.py +0 -0
  77. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/skills.py +0 -0
  78. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/storage.py +0 -0
  79. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/tools.py +0 -0
  80. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers.py +0 -0
  81. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_budget.py +0 -0
  82. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_check.py +0 -0
  83. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
  84. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_questions.py +0 -0
  85. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_shape.py +0 -0
  86. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/workspaces.py +0 -0
  87. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/credentials.py +0 -0
  88. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/device_auth.py +0 -0
  89. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/doctor.py +0 -0
  90. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/exceptions.py +0 -0
  91. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/flow_validation.py +0 -0
  92. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/output.py +0 -0
  93. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/params.py +0 -0
  94. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/sandbox_scenario.py +0 -0
  95. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/__init__.py +0 -0
  96. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
  97. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
  98. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
  99. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/telemetry.py +0 -0
  100. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/__init__.py +0 -0
  101. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/app.py +0 -0
  102. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/demo.py +0 -0
  103. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/links.py +0 -0
  104. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/__init__.py +0 -0
  105. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/agent.py +0 -0
  106. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/base.py +0 -0
  107. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/home.py +0 -0
  108. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/ledger.py +0 -0
  109. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/loader.py +0 -0
  110. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/pickers.py +0 -0
  111. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/trace.py +0 -0
  112. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/source.py +0 -0
  113. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/theme.py +0 -0
  114. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/views.py +0 -0
  115. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/widgets.py +0 -0
  116. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/ui.py +0 -0
  117. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/update_check.py +0 -0
  118. {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/validation.py +0 -0
  119. {fruxon-2.0.2 → fruxon-2.1.0}/tests/.test_durations.json +0 -0
  120. {fruxon-2.0.2 → fruxon-2.1.0}/tests/__init__.py +0 -0
  121. {fruxon-2.0.2 → fruxon-2.1.0}/tests/conftest.py +0 -0
  122. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_actor.py +0 -0
  123. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agent_lifecycle.py +0 -0
  124. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agent_revisions_list.py +0 -0
  125. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agent_slots.py +0 -0
  126. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agents_check_cli.py +0 -0
  127. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agents_revisions_cli.py +0 -0
  128. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agents_sandbox.py +0 -0
  129. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_applications.py +0 -0
  130. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_approvals.py +0 -0
  131. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_assets.py +0 -0
  132. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_budgets.py +0 -0
  133. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_capabilities.py +0 -0
  134. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_channels.py +0 -0
  135. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_checks_agent.py +0 -0
  136. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_checks_ledger.py +0 -0
  137. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli.py +0 -0
  138. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli_base_url_routing.py +0 -0
  139. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli_crash.py +0 -0
  140. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli_root_flags.py +0 -0
  141. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_connect_nudge.py +0 -0
  142. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_consult.py +0 -0
  143. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_conversation_imports.py +0 -0
  144. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_credentials.py +0 -0
  145. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_app.py +0 -0
  146. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_cli.py +0 -0
  147. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_client.py +0 -0
  148. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_links.py +0 -0
  149. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_source.py +0 -0
  150. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_describe.py +0 -0
  151. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_device_auth.py +0 -0
  152. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_docs_commands_exist.py +0 -0
  153. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_doctor.py +0 -0
  154. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_bootstrap.py +0 -0
  155. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_evaluate_cli.py +0 -0
  156. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_pull_shape.py +0 -0
  157. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_run_replay.py +0 -0
  158. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_validate_cli.py +0 -0
  159. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_drafts.py +0 -0
  160. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_entry_points.py +0 -0
  161. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_environments.py +0 -0
  162. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_escalations.py +0 -0
  163. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_evaluations.py +0 -0
  164. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_execution_records.py +0 -0
  165. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_flow_validation.py +0 -0
  166. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_fruxon.py +0 -0
  167. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_guides.py +0 -0
  168. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_improvements.py +0 -0
  169. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integration_configs_write_cli.py +0 -0
  170. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integration_triggers_cli.py +0 -0
  171. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integrations_authorize_cli.py +0 -0
  172. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integrations_connect_cli.py +0 -0
  173. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_kb_trial_cli.py +0 -0
  174. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_bases.py +0 -0
  175. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_documents.py +0 -0
  176. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_notices.py +0 -0
  177. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_sources.py +0 -0
  178. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_llm_providers_connect_cli.py +0 -0
  179. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_managed_by.py +0 -0
  180. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_memory.py +0 -0
  181. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_messages.py +0 -0
  182. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_metrics.py +0 -0
  183. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_online_llm_config_refs.py +0 -0
  184. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_online_prefetch_refs.py +0 -0
  185. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_online_secret_refs.py +0 -0
  186. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_output.py +0 -0
  187. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_params.py +0 -0
  188. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_participants.py +0 -0
  189. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_participants_write.py +0 -0
  190. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_pipelines.py +0 -0
  191. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_reply_style.py +0 -0
  192. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_sandbox_scenario.py +0 -0
  193. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_schema.py +0 -0
  194. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_schema_fetch.py +0 -0
  195. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_secrets.py +0 -0
  196. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_skills.py +0 -0
  197. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_ssl.py +0 -0
  198. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_storage.py +0 -0
  199. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_system_blocks.py +0 -0
  200. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_system_blocks_cli.py +0 -0
  201. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_telemetry.py +0 -0
  202. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_test_chats.py +0 -0
  203. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_topics.py +0 -0
  204. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_trigger_budgets.py +0 -0
  205. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers.py +0 -0
  206. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_check_cli.py +0 -0
  207. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_ledger_batch_views.py +0 -0
  208. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_ledger_stage_tests.py +0 -0
  209. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_questions.py +0 -0
  210. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_shape.py +0 -0
  211. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_write.py +0 -0
  212. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_tui_boundary.py +0 -0
  213. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_ui.py +0 -0
  214. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_update_check.py +0 -0
  215. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_validation.py +0 -0
  216. {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_workspaces_cli.py +0 -0
@@ -1,5 +1,34 @@
1
1
  # History
2
2
 
3
+ ## Unreleased — read voice calls back
4
+
5
+ - **`fruxon agents voice-calls list <agent>`** lists the agent's Talk calls
6
+ from the Test lab, newest first. Each row has the status, language, why the
7
+ call ended, its turns and its length. Output is `table`, `json` or `id`.
8
+ - **`fruxon agents voice-calls get <agent> <call-id>`** shows one call turn by
9
+ turn: what the caller said, the whole reply, and what was heard of it. When
10
+ the caller cut in, `delivery` is `INTERRUPTED` and `spokenText` is the part
11
+ they heard. Each turn also carries `generation` and the run behind its reply
12
+ (`executionRecordId`, for `agents executions trace`). Output is `text`,
13
+ `table` or `json`.
14
+ - Client methods `list_voice_calls` and `get_voice_call` return the new
15
+ `VoiceCall` and `VoiceCallTurn`. Both need fruxon-backend #1537; an older
16
+ server answers 404.
17
+
18
+ ## Unreleased — one completion on an LLM config, with no agent
19
+
20
+ - **`fruxon llm-providers configs complete <provider> <config> --model M`**
21
+ sends a system prompt and a short conversation to a model through the
22
+ config's published revision and prints the reply, with no agent, session or
23
+ execution record (scope `llm_providers:test`; fruxon-backend `:complete`).
24
+ It answers "does this model follow that instruction?" — pass a platform
25
+ system block's text as `--system` and read the reply — without a test turn
26
+ that writes to a live agent. `--system` (text, `@file`, `-`; repeatable),
27
+ `-m [user:|assistant:]TEXT` (repeatable) or `--messages chat.json`,
28
+ `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply
29
+ goes to stdout, model, finish reason, tokens and latency to stderr; `-o json`
30
+ passes the server's result through. A Fruxon-hosted provider is refused.
31
+
3
32
  ## Unreleased — `fruxon messages list --reason EMAIL_THREAD_UNAVAILABLE`
4
33
 
5
34
  - `--reason` takes the server's new `EMAIL_THREAD_UNAVAILABLE` (fruxon-backend
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fruxon
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
5
5
  Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
6
6
  Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
@@ -104,6 +104,7 @@ Execution:
104
104
  | `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
105
105
  | `fruxon messages list` | The channel ledger — including inbound an agent was never shown. A **withheld** message (a human owns the chat in the source console, or admission refused the sender) never becomes a conversation turn, so it is in no transcript and the thread reads as a customer being ignored. `--delivery WITHHELD_PENDING` lists the ones nothing has ever acted on; nothing drains them. |
106
106
  | `fruxon agents sandbox open/turn/fire/resolve-input/stream/answer/close <session>` | Drive an agent-network sandbox end-to-end without real channels — send turns as participants, test-fire triggers, and watch the run (`stream`, NDJSON in agent mode). Sandbox **captures the reply and hard-blocks every real send**; `--await` makes consult/approval gates suspend so they can be resolved (`answer` for consults, `approvals respond` for approvals). `--draft` runs your unpublished draft instead of the deployed revision (`sandbox draft <session>` says which); `--confirm-writes` holds every create/update/delete tool call for approval, because sandbox does **not** block integration writes; a write that did not run (rejected, expired, or one the run could not pause for) shows on `stream` as `refused`. |
107
+ | `fruxon agents voice-calls list/get <agent>` | Read back the Test lab's Talk calls. `list` gives calls newest first (status, end reason, turns, length). `get <agent> <call-id>` gives the call turn by turn: the caller's transcript, the reply, what was heard of it (`delivery`; `spokenText` when the caller cut in), and each reply's `executionRecordId` for `agents executions trace`. Read-only; needs `executions:read`. |
107
108
  | `fruxon agents sandbox fork <agent> <message-id>` | Fork a real conversation at a transcript row (an `id` from `agents topics messages`) into a sandbox session already holding the history the agent had when it produced that reply. `--send` re-sends the customer's message against it — with `--draft` to test an edit before publishing — and holds writes for approval unless `--no-confirm-writes`. |
108
109
  | `fruxon agents sandbox test <file-or-dir>` | Run scenario-based e2e tests of an agent's network behavior. A scenario (YAML/JSON) declares `steps` (turns / trigger fires), `responders` (scripted consult/approval answers), and `expect` assertions (reply / routing / tools called or refused / no-leak / outcome); the runner drives a sandbox session and returns one verdict — exit 0 (pass) / 1 (fail) / 12 (bad file), with `--junit` for CI. An optional `setup` block provisions real resources first — an `asset` (uploaded + ingestion-waited), `participant`, or `agent` — templated as `${asset.catalog.id}` and **torn down afterwards even on failure** (`--keep` to inspect). Assertions are deterministic by default; `reply_judge: {metric, min_score}` adds an opt-in **semantic** check that scores the reply against a tenant eval metric. The thing an agent runs to prove the agent it built works. |
109
110
 
@@ -151,6 +152,7 @@ Integrations + tools:
151
152
  | `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
152
153
  | `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
153
154
  | `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
155
+ | `fruxon llm-providers configs complete <provider> <config> --model M` | One completion on a saved config's published revision, with no agent, session or execution — to see whether a model follows a prompt or a system block's instruction. `--system` (text, `@file`, `-`; repeatable), `-m [user:\|assistant:]TEXT` (repeatable) or `--messages chat.json`, `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply goes to stdout, the model, finish reason, tokens and latency to stderr (`llm_providers:test`). Fruxon-hosted providers are refused. |
154
156
  | `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
155
157
  | `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
156
158
  | `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
@@ -170,6 +172,7 @@ Integrations + tools:
170
172
  | `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
171
173
  | `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
172
174
  | `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
175
+ | `fruxon triggers ledger admit` | Start one item by hand on an automation whose work shape declares `onDemand`. `--set path=value` / `path:=<json>` (dotted paths nest) or `--data-file`; the server checks the declared inputs and refuses credential-named keys. The key — `--key`, or composed from the shape's `keyPaths` — is the whole idempotency: the same key again returns the existing item **unchanged** (`created: false`), whatever its status. Needs `triggers:fire`. |
173
176
  | `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
174
177
  | `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
175
178
  | `fruxon triggers ledger exclusions` | Every reason items were set aside, with how many each holds — the list `--ignore-reason` is picked from, and usually the fastest way to see a prompt excluding a whole class of work it should not. `funnel` stops at `1,204 IGNORED`, which is a status and not something anyone can act on; an exclusion is a judgement a stage made under an instruction, so what you hold after revising it is a *reason*. Each count is exactly what a reason-scoped re-drive resolves, so the diagnosis and the fix use the same string. Reasons print whole and are never truncated — the filter matches the entire string, case-sensitively, so a shortened one selects nothing. A `(none recorded)` row predates the reason being kept on the item: those items are real, no reason filter can reach them, and their way back is `--id`. At most 200 distinct reasons come back. |
@@ -69,6 +69,7 @@ Execution:
69
69
  | `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
70
70
  | `fruxon messages list` | The channel ledger — including inbound an agent was never shown. A **withheld** message (a human owns the chat in the source console, or admission refused the sender) never becomes a conversation turn, so it is in no transcript and the thread reads as a customer being ignored. `--delivery WITHHELD_PENDING` lists the ones nothing has ever acted on; nothing drains them. |
71
71
  | `fruxon agents sandbox open/turn/fire/resolve-input/stream/answer/close <session>` | Drive an agent-network sandbox end-to-end without real channels — send turns as participants, test-fire triggers, and watch the run (`stream`, NDJSON in agent mode). Sandbox **captures the reply and hard-blocks every real send**; `--await` makes consult/approval gates suspend so they can be resolved (`answer` for consults, `approvals respond` for approvals). `--draft` runs your unpublished draft instead of the deployed revision (`sandbox draft <session>` says which); `--confirm-writes` holds every create/update/delete tool call for approval, because sandbox does **not** block integration writes; a write that did not run (rejected, expired, or one the run could not pause for) shows on `stream` as `refused`. |
72
+ | `fruxon agents voice-calls list/get <agent>` | Read back the Test lab's Talk calls. `list` gives calls newest first (status, end reason, turns, length). `get <agent> <call-id>` gives the call turn by turn: the caller's transcript, the reply, what was heard of it (`delivery`; `spokenText` when the caller cut in), and each reply's `executionRecordId` for `agents executions trace`. Read-only; needs `executions:read`. |
72
73
  | `fruxon agents sandbox fork <agent> <message-id>` | Fork a real conversation at a transcript row (an `id` from `agents topics messages`) into a sandbox session already holding the history the agent had when it produced that reply. `--send` re-sends the customer's message against it — with `--draft` to test an edit before publishing — and holds writes for approval unless `--no-confirm-writes`. |
73
74
  | `fruxon agents sandbox test <file-or-dir>` | Run scenario-based e2e tests of an agent's network behavior. A scenario (YAML/JSON) declares `steps` (turns / trigger fires), `responders` (scripted consult/approval answers), and `expect` assertions (reply / routing / tools called or refused / no-leak / outcome); the runner drives a sandbox session and returns one verdict — exit 0 (pass) / 1 (fail) / 12 (bad file), with `--junit` for CI. An optional `setup` block provisions real resources first — an `asset` (uploaded + ingestion-waited), `participant`, or `agent` — templated as `${asset.catalog.id}` and **torn down afterwards even on failure** (`--keep` to inspect). Assertions are deterministic by default; `reply_judge: {metric, min_score}` adds an opt-in **semantic** check that scores the reply against a tenant eval metric. The thing an agent runs to prove the agent it built works. |
74
75
 
@@ -116,6 +117,7 @@ Integrations + tools:
116
117
  | `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
117
118
  | `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
118
119
  | `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
120
+ | `fruxon llm-providers configs complete <provider> <config> --model M` | One completion on a saved config's published revision, with no agent, session or execution — to see whether a model follows a prompt or a system block's instruction. `--system` (text, `@file`, `-`; repeatable), `-m [user:\|assistant:]TEXT` (repeatable) or `--messages chat.json`, `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply goes to stdout, the model, finish reason, tokens and latency to stderr (`llm_providers:test`). Fruxon-hosted providers are refused. |
119
121
  | `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
120
122
  | `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
121
123
  | `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
@@ -135,6 +137,7 @@ Integrations + tools:
135
137
  | `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
136
138
  | `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
137
139
  | `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
140
+ | `fruxon triggers ledger admit` | Start one item by hand on an automation whose work shape declares `onDemand`. `--set path=value` / `path:=<json>` (dotted paths nest) or `--data-file`; the server checks the declared inputs and refuses credential-named keys. The key — `--key`, or composed from the shape's `keyPaths` — is the whole idempotency: the same key again returns the existing item **unchanged** (`created: false`), whatever its status. Needs `triggers:fire`. |
138
141
  | `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
139
142
  | `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
140
143
  | `fruxon triggers ledger exclusions` | Every reason items were set aside, with how many each holds — the list `--ignore-reason` is picked from, and usually the fastest way to see a prompt excluding a whole class of work it should not. `funnel` stops at `1,204 IGNORED`, which is a status and not something anyone can act on; an exclusion is a judgement a stage made under an instruction, so what you hold after revising it is a *reason*. Each count is exactly what a reason-scoped re-drive resolves, so the diagnosis and the fix use the same string. Reasons print whole and are never truncated — the filter matches the entire string, case-sensitively, so a shortened one selects nothing. A `(none recorded)` row predates the reason being kept on the item: those items are real, no reason filter can reach them, and their way back is `--id`. At most 200 distinct reasons come back. |
@@ -80,6 +80,7 @@ from fruxon.models import (
80
80
  LedgerFunnelCell,
81
81
  LedgerGatheringGroup,
82
82
  LedgerItem,
83
+ LedgerItemAdmission,
83
84
  LedgerItemPoint,
84
85
  LedgerItemTimeseries,
85
86
  LedgerItemTransition,
@@ -123,6 +124,8 @@ from fruxon.models import (
123
124
  TriggerParameterMapping,
124
125
  TriggerRevision,
125
126
  UnhandledEscalation,
127
+ VoiceCall,
128
+ VoiceCallTurn,
126
129
  WorkShapeDoor,
127
130
  WorkShapeDoorConversation,
128
131
  WorkShapeDoorField,
@@ -212,6 +215,7 @@ __all__ = [
212
215
  "LedgerFunnelCell",
213
216
  "LedgerGatheringGroup",
214
217
  "LedgerItem",
218
+ "LedgerItemAdmission",
215
219
  "LedgerItemTransition",
216
220
  "LedgerPass",
217
221
  "LedgerPurgeResult",
@@ -250,6 +254,8 @@ __all__ = [
250
254
  "TriggerRevision",
251
255
  "UnhandledEscalation",
252
256
  "ValidationError",
257
+ "VoiceCall",
258
+ "VoiceCallTurn",
253
259
  "WorkShapeDoor",
254
260
  "WorkShapeDoorConversation",
255
261
  "WorkShapeDoorField",
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.0.2'
22
- __version_tuple__ = version_tuple = (2, 0, 2)
21
+ __version__ = version = '2.1.0'
22
+ __version_tuple__ = version_tuple = (2, 1, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -710,6 +710,7 @@ from fruxon.cli import agents_slots as _agents_slots # noqa: E402, F401
710
710
  from fruxon.cli import agents_system_blocks as _agents_system_blocks # noqa: E402, F401
711
711
  from fruxon.cli import agents_tests as _agents_tests # noqa: E402, F401
712
712
  from fruxon.cli import agents_topics as _agents_topics # noqa: E402, F401
713
+ from fruxon.cli import agents_voice_calls as _agents_voice_calls # noqa: E402, F401
713
714
  from fruxon.cli import applications as _applications # noqa: E402, F401
714
715
  from fruxon.cli import applications_entry_points as _applications_entry_points # noqa: E402, F401
715
716
  from fruxon.cli import assets as _assets # noqa: E402, F401
@@ -0,0 +1,379 @@
1
+ """``fruxon agents voice-calls`` — read an agent's voice (Talk) calls back.
2
+
3
+ A voice call is a Talk press in the studio Test lab: the caller speaks, the
4
+ voice worker transcribes, the agent answers, and the worker speaks the reply.
5
+ Each turn records two things a text transcript cannot: whether the reply ran
6
+ to the end (``generation``) and how much of it the caller actually heard
7
+ (``delivery``, with ``spokenText`` when they cut in). This is the read surface
8
+ over them — "what did the caller say, what did the agent answer, what was
9
+ heard of it" — without reading the cell's database.
10
+
11
+ Agent-nested like executions (the backend's
12
+ ``agents/{agent}/voiceSessions``, ``executions:read``), and every turn names
13
+ its run, so ``fruxon agents executions trace <agent> <execution-id>`` follows
14
+ on from any reply.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from typing import Annotated
21
+
22
+ import typer
23
+ from rich.markup import escape as markup_escape
24
+
25
+ from fruxon.cli._shared import build_client, format_ts
26
+ from fruxon.cli.agents import _hint_for_get_error, agents_app
27
+ from fruxon.exceptions import FruxonError, NotFoundError
28
+ from fruxon.fruxon import FruxonClient, VoiceCall, VoiceCallTurn
29
+ from fruxon.ui import (
30
+ EXIT_VALIDATION,
31
+ fail,
32
+ fail_from_api_error,
33
+ format_duration,
34
+ print_banner,
35
+ resolve_output_format,
36
+ say_info,
37
+ say_ok,
38
+ stderr,
39
+ )
40
+
41
+ voice_calls_app = typer.Typer(
42
+ help="Read an agent's voice (Talk) calls back: what was said, the reply, what was heard (list / get).",
43
+ no_args_is_help=True,
44
+ )
45
+ agents_app.add_typer(voice_calls_app, name="voice-calls")
46
+
47
+ # How much of a transcript or reply a table cell shows; `get -o text` / `-o json` show all of it.
48
+ _CELL_CHARS = 60
49
+
50
+
51
+ @voice_calls_app.command("list")
52
+ def voice_calls_list(
53
+ agent: Annotated[str, typer.Argument(help="Agent whose voice calls to list.")],
54
+ limit: Annotated[int, typer.Option("--limit", "-n", help="Max calls to return (newest first).")] = 20,
55
+ workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
56
+ base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
57
+ output: Annotated[
58
+ str | None,
59
+ typer.Option("--output", "-o", help="Output format: table (default), json, id. Agent mode defaults to json."),
60
+ ] = None,
61
+ ):
62
+ """List an agent's voice calls, newest first.
63
+
64
+ One row per Talk press: when, language, status, why it ended, how many
65
+ turns, how long. Read one call's turns with
66
+ ``fruxon agents voice-calls get <agent> <voice-session-id>``.
67
+
68
+ A call with no turns and ``START_TIMEOUT`` never reached a voice worker;
69
+ ``ENDED`` with zero turns and ``USER_HANGUP`` is a caller who hung up
70
+ before saying anything the worker heard.
71
+
72
+ Examples:
73
+ fruxon agents voice-calls list my-agent
74
+ fruxon agents voice-calls list my-agent -n 5 --output json
75
+ fruxon agents voice-calls list my-agent -o id | head -1
76
+ """
77
+ output = resolve_output_format(output, human_default="table", agent_default="json")
78
+ if output not in {"table", "json", "id"}:
79
+ fail(
80
+ f"Unknown output format: [bold]{output}[/bold]",
81
+ hint="Valid formats: table, json, id.",
82
+ code=EXIT_VALIDATION,
83
+ )
84
+ if limit <= 0:
85
+ fail("--limit must be greater than zero.", code=EXIT_VALIDATION)
86
+
87
+ client = build_client(workspace, base_url)
88
+ try:
89
+ calls = client.list_voice_calls(agent, limit=limit)
90
+ except FruxonError as e:
91
+ fail_from_api_error(e, hint=_hint_for_get_error(e, agent, client))
92
+
93
+ if output == "id":
94
+ for c in calls:
95
+ print(c.id)
96
+ return
97
+ if output == "json":
98
+ print(json.dumps([_call_to_dict(c) for c in calls], indent=2, ensure_ascii=False))
99
+ return
100
+
101
+ if not calls:
102
+ say_info(
103
+ f"[bold]{agent}[/bold] has no voice calls.",
104
+ hint="Calls are made with the Talk button in the agent's Test lab.",
105
+ )
106
+ return
107
+ if stderr.is_terminal:
108
+ print_banner(subtitle=f"voice calls · {agent} · {len(calls)}")
109
+ stderr.print()
110
+ stderr.print(_build_calls_table(calls))
111
+ stderr.print()
112
+ say_ok(
113
+ f"[bold]{len(calls)}[/bold] call(s). Read one: "
114
+ f"[dim]fruxon agents voice-calls get {agent} <voice-session-id>[/dim]"
115
+ )
116
+ if len(calls) >= limit:
117
+ say_info(f"Showing the newest {limit}. Raise [bold]-n[/bold] to see older calls.")
118
+
119
+
120
+ @voice_calls_app.command("get")
121
+ def voice_calls_get(
122
+ agent: Annotated[str, typer.Argument(help="Agent the call was made to.")],
123
+ voice_session: Annotated[str, typer.Argument(help="The call's id (from `voice-calls list`).")],
124
+ workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
125
+ base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
126
+ output: Annotated[
127
+ str | None,
128
+ typer.Option(
129
+ "--output",
130
+ "-o",
131
+ help="Output format: text (default; the whole transcript), table (one row a turn), json.",
132
+ ),
133
+ ] = None,
134
+ ):
135
+ """Show one voice call turn by turn: what the caller said, the reply, what was heard.
136
+
137
+ **What was heard is not always the reply.** ``delivery`` is
138
+ ``INTERRUPTED`` when the caller cut in; ``spokenText`` is then the part
139
+ they heard, and the rest of ``replyText`` never reached them.
140
+ ``generation`` is separate: ``CANCELLED`` is a reply stopped by that
141
+ interruption or by the caller's next turn, ``PENDING`` a turn the caller
142
+ talked over before it ran.
143
+
144
+ Every turn names its run in ``executionRecordId``; follow it with
145
+ ``fruxon agents executions trace <agent> <execution-id>`` to see the
146
+ tools, the prompt and the model call behind a reply. Times are epoch ms;
147
+ ``userStartedAt`` / ``userEndedAt`` are the voice worker's clock.
148
+
149
+ Examples:
150
+ fruxon agents voice-calls get my-agent 0199f3c2-…
151
+ fruxon agents voice-calls get my-agent 0199f3c2-… --output table
152
+ fruxon agents voice-calls get my-agent 0199f3c2-… -o json | jq '.turns[] | select(.delivery == "INTERRUPTED")'
153
+ """
154
+ output = resolve_output_format(output, human_default="text", agent_default="json")
155
+ if output not in {"text", "table", "json"}:
156
+ fail(
157
+ f"Unknown output format: [bold]{output}[/bold]",
158
+ hint="Valid formats: text, table, json.",
159
+ code=EXIT_VALIDATION,
160
+ )
161
+
162
+ client = build_client(workspace, base_url)
163
+ try:
164
+ call = client.get_voice_call(agent, voice_session)
165
+ except FruxonError as e:
166
+ fail_from_api_error(e, hint=_hint_for_call_error(e, agent, voice_session, client))
167
+
168
+ if output == "json":
169
+ print(json.dumps(_call_to_dict(call, with_turns=True), indent=2, ensure_ascii=False))
170
+ return
171
+
172
+ if stderr.is_terminal:
173
+ print_banner(subtitle=f"voice call · {agent} · {len(call.turns)} turn(s)")
174
+ stderr.print()
175
+ _render_call_header(call)
176
+ stderr.print()
177
+ if not call.turns:
178
+ say_info("No turns: the worker never heard the caller say anything.")
179
+ return
180
+ if output == "table":
181
+ stderr.print(_build_turns_table(call))
182
+ else:
183
+ for index, turn in enumerate(call.turns, start=1):
184
+ _render_turn(call, index, turn)
185
+ stderr.print()
186
+ if any(t.execution_record_id for t in call.turns):
187
+ say_ok(f"Trace a reply: [dim]fruxon agents executions trace {agent} <execution-id>[/dim]")
188
+
189
+
190
+ # ─────────────────────────────────────────────────────────────────────────────
191
+ # helpers
192
+ # ─────────────────────────────────────────────────────────────────────────────
193
+
194
+
195
+ def _hint_for_call_error(error: FruxonError, agent: str, voice_session: str, client: FruxonClient) -> str | None:
196
+ """A 404 on ``get`` is usually the call id, not the agent; say which."""
197
+ if not isinstance(error, NotFoundError):
198
+ return _hint_for_get_error(error, agent, client)
199
+ try:
200
+ client.get_agent(agent)
201
+ except FruxonError:
202
+ return _hint_for_get_error(error, agent, client)
203
+ return (
204
+ f"[bold]{agent}[/bold] has no voice call [bold]{voice_session}[/bold]. "
205
+ f"List its calls with [bold]fruxon agents voice-calls list {agent}[/bold]."
206
+ )
207
+
208
+
209
+ def _call_length_ms(call: VoiceCall) -> int | None:
210
+ if call.call_duration_ms is not None:
211
+ return call.call_duration_ms
212
+ if call.started_at is not None and call.ended_at is not None:
213
+ return call.ended_at - call.started_at
214
+ return None
215
+
216
+
217
+ def _offset(call: VoiceCall, turn: VoiceCallTurn) -> str:
218
+ """When the turn began, from the start of the call: the caller's first word if known."""
219
+ origin = call.started_at or call.created_at
220
+ at = turn.user_started_at or turn.created_at
221
+ if not origin or not at:
222
+ return ""
223
+ return f"+{format_duration(max(at - origin, 0))}"
224
+
225
+
226
+ _DELIVERY_COLOR = {"PLAYED": "green", "INTERRUPTED": "yellow", "UNKNOWN": "red"}
227
+
228
+
229
+ def _delivery_word(turn: VoiceCallTurn) -> str:
230
+ delivery = turn.delivery or "?"
231
+ color = _DELIVERY_COLOR.get(delivery)
232
+ return f"[{color}]{delivery}[/{color}]" if color else delivery
233
+
234
+
235
+ def _delivery_label(turn: VoiceCallTurn) -> str:
236
+ delivery = turn.delivery or "?"
237
+ label = _delivery_word(turn)
238
+ if turn.delivered_duration_ms is not None and delivery in {"PLAYED", "INTERRUPTED"}:
239
+ label += f" [dim]({format_duration(turn.delivered_duration_ms)} played)[/dim]"
240
+ if turn.generation and turn.generation != "DONE":
241
+ label += f" · generation {turn.generation}"
242
+ return label
243
+
244
+
245
+ def _render_call_header(call: VoiceCall) -> None:
246
+ status = call.status or "?"
247
+ reason = f" · {call.end_reason}" if call.end_reason else ""
248
+ stderr.print(f" [bold]{call.id}[/bold] {status} · {call.language or '?'}{reason}")
249
+ length = _call_length_ms(call)
250
+ parts = [f"started {format_ts(call.started_at)}" if call.started_at else f"pressed {format_ts(call.created_at)}"]
251
+ if length is not None:
252
+ parts.append(format_duration(length))
253
+ parts.append(f"{call.turn_count or len(call.turns)} turn(s)")
254
+ stderr.print(f" [dim]{' · '.join(parts)}[/dim]")
255
+ stderr.print(f" [dim]Test lab session {call.fruxon_session_id}[/dim]")
256
+ speech = []
257
+ if call.stt_provider:
258
+ heard = f" ({call.stt_seconds:.1f}s)" if call.stt_seconds is not None else ""
259
+ speech.append(f"speech-to-text {call.stt_provider}{heard}")
260
+ if call.tts_provider:
261
+ chars = f" ({call.tts_characters} chars)" if call.tts_characters is not None else ""
262
+ speech.append(f"voice {call.tts_provider}{chars}")
263
+ if speech:
264
+ stderr.print(f" [dim]{' · '.join(speech)}[/dim]")
265
+
266
+
267
+ def _render_turn(call: VoiceCall, index: int, turn: VoiceCallTurn) -> None:
268
+ stderr.print(f" [bold]#{index}[/bold] [dim]{_offset(call, turn)}[/dim] {_delivery_label(turn)}")
269
+ caller = markup_escape(turn.transcript) if turn.transcript else "[dim]— (no transcript)[/dim]"
270
+ stderr.print(f" [bold]Caller[/bold] {caller}")
271
+ reply = markup_escape(turn.reply_text) if turn.reply_text else "[dim]— (no reply)[/dim]"
272
+ stderr.print(f" [bold]Agent[/bold] {reply}")
273
+ if turn.delivery == "INTERRUPTED":
274
+ heard = markup_escape(turn.spoken_text) if turn.spoken_text else "[dim]nothing[/dim]"
275
+ stderr.print(f" [bold]Heard[/bold] {heard}")
276
+ if turn.execution_record_id:
277
+ stderr.print(f" [dim]execution {turn.execution_record_id}[/dim]")
278
+ stderr.print()
279
+
280
+
281
+ def _cell(text: str) -> str:
282
+ flat = " ".join(text.split())
283
+ return markup_escape(flat if len(flat) <= _CELL_CHARS else flat[: _CELL_CHARS - 1] + "…")
284
+
285
+
286
+ def _build_calls_table(calls: list[VoiceCall]):
287
+ from rich.table import Table
288
+
289
+ table = Table(show_header=True, header_style="bold", show_edge=False, pad_edge=False, box=None, padding=(0, 1))
290
+ table.add_column("ID", style="bold", no_wrap=True)
291
+ table.add_column("Pressed", no_wrap=True, style="dim")
292
+ table.add_column("Lang", no_wrap=True)
293
+ table.add_column("Status", no_wrap=True)
294
+ table.add_column("Ended", no_wrap=True)
295
+ table.add_column("Turns", justify="right")
296
+ table.add_column("Length", justify="right", no_wrap=True)
297
+ for c in calls:
298
+ length = _call_length_ms(c)
299
+ table.add_row(
300
+ c.id,
301
+ format_ts(c.created_at),
302
+ c.language or "[dim]—[/dim]",
303
+ c.status or "[dim]—[/dim]",
304
+ c.end_reason or "[dim]—[/dim]",
305
+ str(c.turn_count),
306
+ format_duration(length) if length is not None else "[dim]—[/dim]",
307
+ )
308
+ return table
309
+
310
+
311
+ def _build_turns_table(call: VoiceCall):
312
+ from rich.table import Table
313
+
314
+ table = Table(show_header=True, header_style="bold", show_edge=False, pad_edge=False, box=None, padding=(0, 1))
315
+ table.add_column("#", justify="right", style="bold")
316
+ table.add_column("At", no_wrap=True, style="dim")
317
+ table.add_column("Caller", overflow="fold", ratio=1, min_width=16)
318
+ table.add_column("Heard", overflow="fold", ratio=1, min_width=16)
319
+ table.add_column("Delivery", no_wrap=True)
320
+ table.add_column("Execution", no_wrap=True, style="dim")
321
+ for index, t in enumerate(call.turns, start=1):
322
+ # What the caller heard: all of the reply, or the part before they cut in.
323
+ heard = (t.spoken_text or "") if t.delivery == "INTERRUPTED" else t.reply_text
324
+ table.add_row(
325
+ str(index),
326
+ _offset(call, t),
327
+ _cell(t.transcript) if t.transcript else "[dim]—[/dim]",
328
+ _cell(heard) if heard else "[dim]—[/dim]",
329
+ _delivery_word(t),
330
+ t.execution_record_id or "[dim]—[/dim]",
331
+ )
332
+ return table
333
+
334
+
335
+ def _turn_to_dict(t: VoiceCallTurn) -> dict:
336
+ return {
337
+ "id": t.id,
338
+ "turnId": t.turn_id,
339
+ "language": t.language,
340
+ "transcript": t.transcript,
341
+ "replyText": t.reply_text,
342
+ "generation": t.generation,
343
+ "delivery": t.delivery,
344
+ "spokenText": t.spoken_text,
345
+ "deliveredDurationMs": t.delivered_duration_ms,
346
+ "executionRecordId": t.execution_record_id or None,
347
+ "createdAt": t.created_at,
348
+ "userStartedAt": t.user_started_at,
349
+ "userEndedAt": t.user_ended_at,
350
+ "generationEndedAt": t.generation_ended_at,
351
+ "cancelRequestedAt": t.cancel_requested_at,
352
+ "deliveryReportedAt": t.delivery_reported_at,
353
+ }
354
+
355
+
356
+ def _call_to_dict(c: VoiceCall, *, with_turns: bool = False) -> dict:
357
+ body = {
358
+ "id": c.id,
359
+ "agentId": c.agent_id,
360
+ "fruxonSessionId": c.fruxon_session_id,
361
+ "status": c.status,
362
+ "language": c.language,
363
+ "createdAt": c.created_at,
364
+ "startedAt": c.started_at,
365
+ "endedAt": c.ended_at,
366
+ "endReason": c.end_reason or None,
367
+ "callDurationMs": c.call_duration_ms,
368
+ "turnCount": c.turn_count,
369
+ "createdByUserId": c.created_by_user_id or None,
370
+ }
371
+ if with_turns:
372
+ body |= {
373
+ "sttProvider": c.stt_provider or None,
374
+ "ttsProvider": c.tts_provider or None,
375
+ "sttSeconds": c.stt_seconds,
376
+ "ttsCharacters": c.tts_characters,
377
+ "turns": [_turn_to_dict(t) for t in c.turns],
378
+ }
379
+ return body
@@ -99,6 +99,7 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
99
99
  ),
100
100
  ("fruxon agents tests list my-agent", "List your saved test-chat sessions for an agent"),
101
101
  ("fruxon agents executions list my-agent", "List past executions — find a record id to trace"),
102
+ ("fruxon agents voice-calls list my-agent", "Talk calls made to the agent — find one to read back"),
102
103
  ("fruxon agents budget list my-agent", "Every cost cap — PRODUCTION + TEST, daily / weekly / monthly"),
103
104
  (
104
105
  "fruxon agents budget set my-agent --origin TEST --period DAILY --amount 20",
@@ -373,6 +374,15 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
373
374
  ),
374
375
  ("fruxon agents inbox my-agent", "What the agent is focused on (per participant)"),
375
376
  ],
377
+ "voice-calls": [
378
+ ("fruxon agents voice-calls list my-agent", "The agent's Talk calls, newest first — status, turns, length"),
379
+ ("fruxon agents voice-calls get my-agent 0199f3c2-…", "One call turn by turn: said, replied, heard"),
380
+ ("fruxon agents voice-calls get my-agent 0199f3c2-… -o table", "…one row a turn, with each turn's run"),
381
+ (
382
+ "fruxon agents voice-calls get my-agent 0199f3c2-… -o json | jq '.turns[].executionRecordId'",
383
+ "The runs behind the replies — feed one to `agents executions trace`",
384
+ ),
385
+ ],
376
386
  "memory": [
377
387
  ("fruxon agents memory list my-agent --subject user_42", "What the agent remembers about one person"),
378
388
  ("fruxon agents memory list my-agent --query 'dark mode'", "Full-text over its memories"),
@@ -734,6 +744,11 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
734
744
  ("fruxon llm-providers configs get vertex_ai 4950…", "Draft (masked), published revision, health"),
735
745
  ("fruxon llm-providers configs test vertex_ai 4950…", "Probe every surface sharing the credential"),
736
746
  ("fruxon llm-providers configs test vertex_ai_maas 4950…", "…or just that one surface"),
747
+ (
748
+ "fruxon llm-providers configs complete google 4950… --model gemini-3.1-flash-lite "
749
+ "--system @voice.txt -m 'hi'",
750
+ "One completion on the config, no agent: does the model follow this system prompt?",
751
+ ),
737
752
  ("fruxon llm-providers configs publish vertex_ai 4950…", "Ship the draft; every agent on it switches"),
738
753
  (
739
754
  "fruxon llm-providers connect openai",