fruxon 2.0.2__tar.gz → 2.2.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.2.0}/HISTORY.md +60 -0
  2. {fruxon-2.0.2 → fruxon-2.2.0}/PKG-INFO +9 -2
  3. {fruxon-2.0.2 → fruxon-2.2.0}/README.md +8 -1
  4. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/__init__.py +30 -0
  5. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/_version.py +2 -2
  6. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/ledger.py +97 -0
  7. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/__init__.py +1 -0
  8. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_shared.py +86 -0
  9. fruxon-2.2.0/src/fruxon/cli/agents_voice_calls.py +379 -0
  10. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/auth.py +82 -14
  11. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/examples.py +15 -0
  12. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_notices.py +4 -17
  13. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/llm_providers.py +261 -0
  14. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers.py +14 -1
  15. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_ledger.py +388 -9
  16. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_shape.py +207 -4
  17. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/exceptions.py +15 -0
  18. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/fruxon.py +478 -20
  19. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/models.py +495 -5
  20. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-build-agent/SKILL.md +6 -0
  21. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +17 -5
  22. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-meet/SKILL.md +2 -0
  23. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/ui.py +45 -2
  24. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_checks_ledger.py +97 -0
  25. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli.py +148 -0
  26. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_client.py +118 -0
  27. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_llm_provider_configs_cli.py +255 -0
  28. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers.py +64 -0
  29. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_ledger.py +645 -2
  30. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_shape.py +190 -0
  31. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_ui.py +175 -0
  32. fruxon-2.2.0/tests/test_voice_calls.py +288 -0
  33. {fruxon-2.0.2 → fruxon-2.2.0}/.gitignore +0 -0
  34. {fruxon-2.0.2 → fruxon-2.2.0}/LICENSE +0 -0
  35. {fruxon-2.0.2 → fruxon-2.2.0}/pyproject.toml +0 -0
  36. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/__main__.py +0 -0
  37. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/_ssl.py +0 -0
  38. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/__init__.py +0 -0
  39. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/agent.py +0 -0
  40. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/references.py +0 -0
  41. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_crash.py +0 -0
  42. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_schema.py +0 -0
  43. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_stream.py +0 -0
  44. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents.py +0 -0
  45. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_approvals.py +0 -0
  46. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_budget.py +0 -0
  47. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_channels.py +0 -0
  48. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_draft.py +0 -0
  49. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_evaluations.py +0 -0
  50. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_executions.py +0 -0
  51. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_improvements.py +0 -0
  52. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_lifecycle.py +0 -0
  53. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_memory.py +0 -0
  54. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_reply_style.py +0 -0
  55. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_revisions.py +0 -0
  56. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_sandbox.py +0 -0
  57. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_sandbox_test.py +0 -0
  58. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_slots.py +0 -0
  59. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_system_blocks.py +0 -0
  60. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_tests.py +0 -0
  61. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_topics.py +0 -0
  62. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/applications.py +0 -0
  63. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/applications_entry_points.py +0 -0
  64. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/assets.py +0 -0
  65. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/capabilities.py +0 -0
  66. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/completion.py +0 -0
  67. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/config.py +0 -0
  68. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/consult_pins.py +0 -0
  69. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/conversation_imports.py +0 -0
  70. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/dash.py +0 -0
  71. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/describe.py +0 -0
  72. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/doctor.py +0 -0
  73. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/environments.py +0 -0
  74. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/escalations.py +0 -0
  75. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/guides.py +0 -0
  76. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/integrations.py +0 -0
  77. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/keys.py +0 -0
  78. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_bases.py +0 -0
  79. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_documents.py +0 -0
  80. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_sources.py +0 -0
  81. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/messages.py +0 -0
  82. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/metrics.py +0 -0
  83. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/participants.py +0 -0
  84. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/pipelines.py +0 -0
  85. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/secrets.py +0 -0
  86. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/skills.py +0 -0
  87. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/storage.py +0 -0
  88. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/tools.py +0 -0
  89. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_budget.py +0 -0
  90. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_check.py +0 -0
  91. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
  92. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_questions.py +0 -0
  93. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/workspaces.py +0 -0
  94. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/credentials.py +0 -0
  95. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/device_auth.py +0 -0
  96. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/doctor.py +0 -0
  97. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/flow_validation.py +0 -0
  98. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/output.py +0 -0
  99. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/params.py +0 -0
  100. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/sandbox_scenario.py +0 -0
  101. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/__init__.py +0 -0
  102. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
  103. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
  104. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
  105. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/telemetry.py +0 -0
  106. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/__init__.py +0 -0
  107. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/app.py +0 -0
  108. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/demo.py +0 -0
  109. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/links.py +0 -0
  110. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/__init__.py +0 -0
  111. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/agent.py +0 -0
  112. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/base.py +0 -0
  113. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/home.py +0 -0
  114. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/ledger.py +0 -0
  115. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/loader.py +0 -0
  116. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/pickers.py +0 -0
  117. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/trace.py +0 -0
  118. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/source.py +0 -0
  119. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/theme.py +0 -0
  120. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/views.py +0 -0
  121. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/widgets.py +0 -0
  122. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/update_check.py +0 -0
  123. {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/validation.py +0 -0
  124. {fruxon-2.0.2 → fruxon-2.2.0}/tests/.test_durations.json +0 -0
  125. {fruxon-2.0.2 → fruxon-2.2.0}/tests/__init__.py +0 -0
  126. {fruxon-2.0.2 → fruxon-2.2.0}/tests/conftest.py +0 -0
  127. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_actor.py +0 -0
  128. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agent_lifecycle.py +0 -0
  129. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agent_revisions_list.py +0 -0
  130. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agent_slots.py +0 -0
  131. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agents_check_cli.py +0 -0
  132. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agents_revisions_cli.py +0 -0
  133. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agents_sandbox.py +0 -0
  134. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_applications.py +0 -0
  135. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_approvals.py +0 -0
  136. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_assets.py +0 -0
  137. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_budgets.py +0 -0
  138. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_capabilities.py +0 -0
  139. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_channels.py +0 -0
  140. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_checks_agent.py +0 -0
  141. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli_base_url_routing.py +0 -0
  142. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli_crash.py +0 -0
  143. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli_root_flags.py +0 -0
  144. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_connect_nudge.py +0 -0
  145. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_consult.py +0 -0
  146. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_conversation_imports.py +0 -0
  147. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_credentials.py +0 -0
  148. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_app.py +0 -0
  149. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_cli.py +0 -0
  150. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_client.py +0 -0
  151. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_links.py +0 -0
  152. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_source.py +0 -0
  153. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_describe.py +0 -0
  154. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_device_auth.py +0 -0
  155. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_docs_commands_exist.py +0 -0
  156. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_doctor.py +0 -0
  157. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_bootstrap.py +0 -0
  158. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_evaluate_cli.py +0 -0
  159. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_pull_shape.py +0 -0
  160. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_run_replay.py +0 -0
  161. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_validate_cli.py +0 -0
  162. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_drafts.py +0 -0
  163. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_entry_points.py +0 -0
  164. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_environments.py +0 -0
  165. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_escalations.py +0 -0
  166. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_evaluations.py +0 -0
  167. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_execution_records.py +0 -0
  168. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_flow_validation.py +0 -0
  169. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_fruxon.py +0 -0
  170. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_guides.py +0 -0
  171. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_improvements.py +0 -0
  172. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integration_configs_write_cli.py +0 -0
  173. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integration_triggers_cli.py +0 -0
  174. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integrations_authorize_cli.py +0 -0
  175. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integrations_connect_cli.py +0 -0
  176. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_kb_trial_cli.py +0 -0
  177. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_bases.py +0 -0
  178. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_documents.py +0 -0
  179. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_notices.py +0 -0
  180. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_sources.py +0 -0
  181. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_llm_providers_connect_cli.py +0 -0
  182. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_managed_by.py +0 -0
  183. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_memory.py +0 -0
  184. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_messages.py +0 -0
  185. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_metrics.py +0 -0
  186. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_online_llm_config_refs.py +0 -0
  187. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_online_prefetch_refs.py +0 -0
  188. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_online_secret_refs.py +0 -0
  189. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_output.py +0 -0
  190. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_params.py +0 -0
  191. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_participants.py +0 -0
  192. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_participants_write.py +0 -0
  193. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_pipelines.py +0 -0
  194. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_reply_style.py +0 -0
  195. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_sandbox_scenario.py +0 -0
  196. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_schema.py +0 -0
  197. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_schema_fetch.py +0 -0
  198. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_secrets.py +0 -0
  199. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_skills.py +0 -0
  200. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_ssl.py +0 -0
  201. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_storage.py +0 -0
  202. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_system_blocks.py +0 -0
  203. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_system_blocks_cli.py +0 -0
  204. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_telemetry.py +0 -0
  205. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_test_chats.py +0 -0
  206. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_topics.py +0 -0
  207. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_trigger_budgets.py +0 -0
  208. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_check_cli.py +0 -0
  209. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_ledger_batch_views.py +0 -0
  210. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_ledger_stage_tests.py +0 -0
  211. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_questions.py +0 -0
  212. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_write.py +0 -0
  213. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_tui_boundary.py +0 -0
  214. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_update_check.py +0 -0
  215. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_validation.py +0 -0
  216. {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_workspaces_cli.py +0 -0
@@ -1,5 +1,65 @@
1
1
  # History
2
2
 
3
+ ## Unreleased — whoami and the 403 hint read the role cap off the key
4
+
5
+ - On a server with fruxon-backend #1571, `fruxon whoami` and the 403
6
+ `insufficient_scope` hint take your role and the scopes in effect from
7
+ `/tokens/current` (`ownerRole`, `effectiveScopes`) instead of also reading
8
+ your membership, which needs `workspace:read`. A key without that scope now
9
+ gets its role named instead of "couldn't be read". Older servers keep the
10
+ membership read.
11
+ - **`fruxon whoami`** lists a service-account key's grant (`N scopes from its
12
+ grant`, and `effectiveScopes` under `current_key` in `-o json`) instead of
13
+ calling it "dead weight" for carrying no scopes of its own.
14
+
15
+ ## Unreleased — say when your workspace role, not the key, refuses a call
16
+
17
+ - A 403 `insufficient_scope` for a scope the key already carries no longer
18
+ says to re-run `fruxon login --preset …`. Since fruxon-backend `f5aaf1250`
19
+ the server caps a personal key to its owner's current workspace role, so a
20
+ Viewer's key minted with `agents:write` is refused it, and a new key would be
21
+ refused the same way. The CLI now asks the refused key what it carries
22
+ (`/tokens/current`, then the owner's membership) and, when the key has the
23
+ scope, says the role is the limit and to ask a workspace Admin to raise it or
24
+ make the change. A key that lacks the scope, a service-account key, or a
25
+ lookup that fails keeps the preset hint.
26
+ - **`fruxon whoami`** labels its scope list as the key's own and adds a line
27
+ with your role in the workspace and how many of those scopes it leaves in
28
+ effect. `-o json` adds `ownerType`, `ownerRole` and `effectiveScopes` under
29
+ `current_key` (`null` when the role couldn't be read).
30
+ - `InsufficientScopeError.client` is the client whose request was refused, and
31
+ `FruxonClient.get_user(user)` reads one workspace member, including `role`
32
+ and `effectiveScopes`.
33
+
34
+ ## Unreleased — read voice calls back
35
+
36
+ - **`fruxon agents voice-calls list <agent>`** lists the agent's Talk calls
37
+ from the Test lab, newest first. Each row has the status, language, why the
38
+ call ended, its turns and its length. Output is `table`, `json` or `id`.
39
+ - **`fruxon agents voice-calls get <agent> <call-id>`** shows one call turn by
40
+ turn: what the caller said, the whole reply, and what was heard of it. When
41
+ the caller cut in, `delivery` is `INTERRUPTED` and `spokenText` is the part
42
+ they heard. Each turn also carries `generation` and the run behind its reply
43
+ (`executionRecordId`, for `agents executions trace`). Output is `text`,
44
+ `table` or `json`.
45
+ - Client methods `list_voice_calls` and `get_voice_call` return the new
46
+ `VoiceCall` and `VoiceCallTurn`. Both need fruxon-backend #1537; an older
47
+ server answers 404.
48
+
49
+ ## Unreleased — one completion on an LLM config, with no agent
50
+
51
+ - **`fruxon llm-providers configs complete <provider> <config> --model M`**
52
+ sends a system prompt and a short conversation to a model through the
53
+ config's published revision and prints the reply, with no agent, session or
54
+ execution record (scope `llm_providers:test`; fruxon-backend `:complete`).
55
+ It answers "does this model follow that instruction?" — pass a platform
56
+ system block's text as `--system` and read the reply — without a test turn
57
+ that writes to a live agent. `--system` (text, `@file`, `-`; repeatable),
58
+ `-m [user:|assistant:]TEXT` (repeatable) or `--messages chat.json`,
59
+ `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply
60
+ goes to stdout, model, finish reason, tokens and latency to stderr; `-o json`
61
+ passes the server's result through. A Fruxon-hosted provider is refused.
62
+
3
63
  ## Unreleased — `fruxon messages list --reason EMAIL_THREAD_UNAVAILABLE`
4
64
 
5
65
  - `--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.2.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,8 @@ 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 wake` | End an item's wait at a WAIT stage now, as if its due time had come: an event the stage waits for that already arrived takes `onEvent` with what it carries, otherwise the `onDue` exit applies (on to the next or named stage, or FAIL / DEAD); the history records `OPERATOR_WAKE`. 409 for an item waiting on a question (answer it or `cancel-batch`). `ignore` stops a wait without continuing. `items --waiting-for TIME --order-by wakeAt` lists what waits on time, soonest first; the Detail column says when each wakes and the footer flags any 15 minutes overdue. `items --admitted-via API\|AGENT` lists what was started by hand (`admit`, or an agent's `start_work_item`). |
176
+ | `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
177
  | `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
178
  | `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
179
  | `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. |
@@ -179,7 +183,7 @@ Integrations + tools:
179
183
  | `fruxon triggers ledger walk/walk-steps` | `walk --item <id> --yes` chains stage tests: the item's stage (or `--from`), then each stage it advances into, on the data it advanced with, until a real pass would stop too. Each ASK stage it reaches takes `--reply <stage>=<text>` or `--answer <stage>=<value>`; `--agent-revision <agent>=<N>` runs every stage of that agent on an undeployed revision. `stopped.code` is `COMPLETED`, `REACHED_UNTIL` (`--until`), `NOT_ADVANCED` (a `WAIT`, too, and an ADVANCE a pass would undo: a duplicate, or fields the next stage requires), `REPLY_NEEDED` (an ASK stage nobody answered), `STAGE_NOT_TESTABLE`, `MAX_STEPS` (default 10), `TIMEOUT` or `CANCELLED`. Every step is a paid run, so it confirms first and needs `--yes` in agent mode; it exits 0 whenever it ran. `walk-steps <trigger> <walk>` reads a walk back in step order. |
180
184
  | `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items and effects, bumps the cursor version — and confirms the exact item count first. The items' transitions stay, as the ledger's history. Destructive; the effects' external targets are not touched. |
181
185
  | `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
182
- | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`. |
186
+ | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`, and a WAIT stage's `wait` (`minutes` or `untilPath`, then `onDue`; an `event` with `match`, `capture` and `onEvent` to end it early — the invoice example and the save rules are there). It also warns before a save that changes a stage's kind, or a WAIT stage's event correlation, under CLAIMED or WAITING items, which the server refuses, and prints the save's own warnings — such as a WAIT matching its event on a value nothing keeps unique. |
183
187
  | `fruxon triggers revisions list/get/restore` | A trigger's audit history — every mutation writes a snapshot, and `changeReason` separates a config edit from a binding reconcile. `get -o json | jq .workShape` is the shape as it stood, which is how you undo an edit by hand. `restore` replays one and is **not** a universal undo: it runs the same validation a save does, so a snapshot that drops a stage still holding live items is refused. |
184
188
  | `fruxon secrets list/get/grants` | Discover tenant secrets an agent can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
185
189
  | `fruxon participants list/get/create/update/delete/enable/disable` | Manage agent-network participants (people / groups / agents the network routes to). `list` takes `--query` (matches names **and channel addresses and account ids**, so a phone number or handle resolves to an id, and searches both directory tiers by default), `--kind`, `--tier`, `--agent`, `--environment-id`, and pages up to `--limit` / `--all`. `create`/`update` take `--file` (+`--schema`); `enable`/`disable` toggle routing; `delete` confirms + refuses without `--yes` in agent mode. |
@@ -340,6 +344,9 @@ spend = client.get_ledger_spend(trigger_id) # this calendar week, by agent and
340
344
  print(spend.cost, [(a.agent_id, a.cost) for a in spend.agents])
341
345
  for b in client.list_ledger_batches(trigger_id, open_only=True):
342
346
  print(b.delivery, b.question) # UNDELIVERABLE ⇒ nobody was asked
347
+ for i in client.list_ledger_items(trigger_id, waiting_for="TIME", order_by="wakeAt"):
348
+ print(i.item_key, i.wake_at) # parked at a WAIT stage, soonest first
349
+ client.wake_ledger_item(trigger_id, item_id, reason="indexed early") # its onDue exit applies now
343
350
  ```
344
351
 
345
352
  The client picks up `FRUXON_TOKEN`, `FRUXON_WORKSPACE`, and `FRUXON_BASE_URL` only if you read them yourself — the constructor takes explicit values. The CLI resolves them automatically (command flags → root flags → env → stored config).
@@ -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,8 @@ 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 wake` | End an item's wait at a WAIT stage now, as if its due time had come: an event the stage waits for that already arrived takes `onEvent` with what it carries, otherwise the `onDue` exit applies (on to the next or named stage, or FAIL / DEAD); the history records `OPERATOR_WAKE`. 409 for an item waiting on a question (answer it or `cancel-batch`). `ignore` stops a wait without continuing. `items --waiting-for TIME --order-by wakeAt` lists what waits on time, soonest first; the Detail column says when each wakes and the footer flags any 15 minutes overdue. `items --admitted-via API\|AGENT` lists what was started by hand (`admit`, or an agent's `start_work_item`). |
141
+ | `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
142
  | `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
143
  | `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
144
  | `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. |
@@ -144,7 +148,7 @@ Integrations + tools:
144
148
  | `fruxon triggers ledger walk/walk-steps` | `walk --item <id> --yes` chains stage tests: the item's stage (or `--from`), then each stage it advances into, on the data it advanced with, until a real pass would stop too. Each ASK stage it reaches takes `--reply <stage>=<text>` or `--answer <stage>=<value>`; `--agent-revision <agent>=<N>` runs every stage of that agent on an undeployed revision. `stopped.code` is `COMPLETED`, `REACHED_UNTIL` (`--until`), `NOT_ADVANCED` (a `WAIT`, too, and an ADVANCE a pass would undo: a duplicate, or fields the next stage requires), `REPLY_NEEDED` (an ASK stage nobody answered), `STAGE_NOT_TESTABLE`, `MAX_STEPS` (default 10), `TIMEOUT` or `CANCELLED`. Every step is a paid run, so it confirms first and needs `--yes` in agent mode; it exits 0 whenever it ran. `walk-steps <trigger> <walk>` reads a walk back in step order. |
145
149
  | `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items and effects, bumps the cursor version — and confirms the exact item count first. The items' transitions stay, as the ledger's history. Destructive; the effects' external targets are not touched. |
146
150
  | `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
147
- | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`. |
151
+ | `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`, and a WAIT stage's `wait` (`minutes` or `untilPath`, then `onDue`; an `event` with `match`, `capture` and `onEvent` to end it early — the invoice example and the save rules are there). It also warns before a save that changes a stage's kind, or a WAIT stage's event correlation, under CLAIMED or WAITING items, which the server refuses, and prints the save's own warnings — such as a WAIT matching its event on a value nothing keeps unique. |
148
152
  | `fruxon triggers revisions list/get/restore` | A trigger's audit history — every mutation writes a snapshot, and `changeReason` separates a config edit from a binding reconcile. `get -o json | jq .workShape` is the shape as it stood, which is how you undo an edit by hand. `restore` replays one and is **not** a universal undo: it runs the same validation a save does, so a snapshot that drops a stage still holding live items is refused. |
149
153
  | `fruxon secrets list/get/grants` | Discover tenant secrets an agent can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
150
154
  | `fruxon participants list/get/create/update/delete/enable/disable` | Manage agent-network participants (people / groups / agents the network routes to). `list` takes `--query` (matches names **and channel addresses and account ids**, so a phone number or handle resolves to an id, and searches both directory tiers by default), `--kind`, `--tier`, `--agent`, `--environment-id`, and pages up to `--limit` / `--all`. `create`/`update` take `--file` (+`--schema`); `enable`/`disable` toggle routing; `delete` confirms + refuses without `--yes` in agent mode. |
@@ -305,6 +309,9 @@ spend = client.get_ledger_spend(trigger_id) # this calendar week, by agent and
305
309
  print(spend.cost, [(a.agent_id, a.cost) for a in spend.agents])
306
310
  for b in client.list_ledger_batches(trigger_id, open_only=True):
307
311
  print(b.delivery, b.question) # UNDELIVERABLE ⇒ nobody was asked
312
+ for i in client.list_ledger_items(trigger_id, waiting_for="TIME", order_by="wakeAt"):
313
+ print(i.item_key, i.wake_at) # parked at a WAIT stage, soonest first
314
+ client.wake_ledger_item(trigger_id, item_id, reason="indexed early") # its onDue exit applies now
308
315
  ```
309
316
 
310
317
  The client picks up `FRUXON_TOKEN`, `FRUXON_WORKSPACE`, and `FRUXON_BASE_URL` only if you read them yourself — the constructor takes explicit values. The CLI resolves them automatically (command flags → root flags → env → stored config).
@@ -80,7 +80,15 @@ from fruxon.models import (
80
80
  LedgerFunnelCell,
81
81
  LedgerGatheringGroup,
82
82
  LedgerItem,
83
+ LedgerItemAdmission,
84
+ LedgerItemAttempt,
85
+ LedgerItemAttemptAsk,
86
+ LedgerItemAttemptBatch,
87
+ LedgerItemAttemptOutcome,
88
+ LedgerItemAttemptWait,
83
89
  LedgerItemPoint,
90
+ LedgerItemRuns,
91
+ LedgerItemRunTotals,
84
92
  LedgerItemTimeseries,
85
93
  LedgerItemTransition,
86
94
  LedgerPass,
@@ -97,6 +105,11 @@ from fruxon.models import (
97
105
  LedgerStageTestSimulatedCall,
98
106
  LedgerStageTestSink,
99
107
  LedgerStageTestVerdict,
108
+ LedgerWaitCapture,
109
+ LedgerWaitConfig,
110
+ LedgerWaitEventConfig,
111
+ LedgerWaitExit,
112
+ LedgerWaitMatch,
100
113
  ManagedBy,
101
114
  ManagedResourceLink,
102
115
  Memory,
@@ -123,6 +136,8 @@ from fruxon.models import (
123
136
  TriggerParameterMapping,
124
137
  TriggerRevision,
125
138
  UnhandledEscalation,
139
+ VoiceCall,
140
+ VoiceCallTurn,
126
141
  WorkShapeDoor,
127
142
  WorkShapeDoorConversation,
128
143
  WorkShapeDoorField,
@@ -212,6 +227,14 @@ __all__ = [
212
227
  "LedgerFunnelCell",
213
228
  "LedgerGatheringGroup",
214
229
  "LedgerItem",
230
+ "LedgerItemAdmission",
231
+ "LedgerItemAttempt",
232
+ "LedgerItemAttemptAsk",
233
+ "LedgerItemAttemptBatch",
234
+ "LedgerItemAttemptOutcome",
235
+ "LedgerItemAttemptWait",
236
+ "LedgerItemRuns",
237
+ "LedgerItemRunTotals",
215
238
  "LedgerItemTransition",
216
239
  "LedgerPass",
217
240
  "LedgerPurgeResult",
@@ -221,6 +244,11 @@ __all__ = [
221
244
  "LedgerStageTestSimulatedCall",
222
245
  "LedgerStageTestSink",
223
246
  "LedgerStageTestVerdict",
247
+ "LedgerWaitCapture",
248
+ "LedgerWaitConfig",
249
+ "LedgerWaitEventConfig",
250
+ "LedgerWaitExit",
251
+ "LedgerWaitMatch",
224
252
  "LedgerSkippedItem",
225
253
  "ManagedBy",
226
254
  "Memory",
@@ -250,6 +278,8 @@ __all__ = [
250
278
  "TriggerRevision",
251
279
  "UnhandledEscalation",
252
280
  "ValidationError",
281
+ "VoiceCall",
282
+ "VoiceCallTurn",
253
283
  "WorkShapeDoor",
254
284
  "WorkShapeDoorConversation",
255
285
  "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.2.0'
22
+ __version_tuple__ = version_tuple = (2, 2, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -21,6 +21,7 @@ from datetime import datetime
21
21
 
22
22
  from fruxon.checks import CheckReport, CheckResult, shape_get
23
23
  from fruxon.exceptions import FruxonError, NotFoundError
24
+ from fruxon.models import LedgerWaitConfig, LedgerWaitEventConfig, LedgerWaitExit
24
25
 
25
26
  DAY_MS = 86_400_000
26
27
  _HOUR_MS = 3_600_000
@@ -137,6 +138,7 @@ def check_ledger(client, trigger: str, *, detail=None, now_ms: int | None = None
137
138
  cells: dict[str, dict[str, int]] = {}
138
139
  for cell in funnel or []:
139
140
  cells.setdefault(cell.stage_id, {})[cell.status.upper()] = cell.count
141
+ waiting_cells = {c.stage_id: c for c in funnel or [] if c.status.upper() == "WAITING"}
140
142
  today = int(datetime.fromtimestamp(now / 1000).replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
141
143
  series, _ = _try(
142
144
  client.get_ledger_item_timeseries, trigger, interval="DAY", since=today * 1000 - 6 * DAY_MS, until=now
@@ -207,6 +209,9 @@ def check_ledger(client, trigger: str, *, detail=None, now_ms: int | None = None
207
209
  if kind == "ASK":
208
210
  rows.append(_ask_row(trigger, stage, stage_id, asker, slots_of, batches, batches_ok, now))
209
211
  continue
212
+ if kind == "WAIT":
213
+ rows.append(_wait_row(trigger, stage, stage_id, waiting_cells.get(stage_id), now))
214
+ continue
210
215
  agent = str(shape_get(stage, "agentId") or "")
211
216
  if not agent:
212
217
  add(stage_id, "skip", "no agent — items pass through", explain="", fix="trigger-edit")
@@ -432,10 +437,102 @@ def _ask_row(
432
437
  )
433
438
 
434
439
 
440
+ def _wait_row(trigger: str, stage: dict, stage_id: str, cell, now: int) -> CheckResult:
441
+ """A WAIT stage: nothing runs there, so the one thing that can be wrong is a
442
+ wait left well past its due time — the sweeper that ends them is behind."""
443
+ wait = LedgerWaitConfig.from_dict(shape_get(stage, "wait")) or LedgerWaitConfig()
444
+ what = f"waits {describe_wait(wait)}"
445
+ overdue = getattr(cell, "overdue", 0) if cell is not None else 0
446
+ if overdue:
447
+ return CheckResult(
448
+ stage_id,
449
+ "warn",
450
+ f"{what} · {overdue} overdue",
451
+ hint=f"fruxon triggers ledger items {trigger} --waiting-for TIME --order-by wakeAt",
452
+ explain="A wait ends within a minute or two of its due time. Items more than 15 minutes past it mean "
453
+ "the wait sweeper is down or behind; `fruxon triggers ledger wake` ends one by hand.",
454
+ fix=f"work:WAITING:{stage_id}",
455
+ )
456
+ waiting = cell.count if cell is not None else 0
457
+ next_wake = getattr(cell, "next_wake_at", None) if cell is not None else None
458
+ if waiting:
459
+ what += f" · {waiting:,} waiting" + (f", next wakes {_clock(next_wake, now)}" if next_wake else "")
460
+ return CheckResult(
461
+ stage_id,
462
+ "ok",
463
+ what,
464
+ explain="Nothing runs here: each item waits until its due time"
465
+ + (", or until its event arrives," if wait.event is not None else "")
466
+ + " then moves on.",
467
+ fix=f"work:WAITING:{stage_id}",
468
+ )
469
+
470
+
471
+ def _span(minutes: int) -> str:
472
+ days, rest = divmod(minutes, 1_440)
473
+ hours, mins = divmod(rest, 60)
474
+ return " ".join(f"{n}{unit}" for n, unit in ((days, "d"), (hours, "h"), (mins, "m")) if n) or "0m"
475
+
476
+
477
+ def _exit_text(exit_: LedgerWaitExit) -> str:
478
+ if exit_.action == "ADVANCE_TO":
479
+ return exit_.stage_id or "?"
480
+ if exit_.action in {"FAIL", "DEAD"}:
481
+ return exit_.action
482
+ return "next"
483
+
484
+
485
+ def describe_wait(wait: LedgerWaitConfig) -> str:
486
+ """A WAIT stage in a few words: ``3d``, ``until due_at, then DEAD``, ``1h 30m → paid``, or with an
487
+ event ``until due_at or stripe.payment_intent.succeeded: event → receipt, due → next``."""
488
+ span = _span(wait.minutes) if wait.minutes is not None else f"until {wait.until_path or '?'}"
489
+ if wait.event is not None:
490
+ return (
491
+ f"{span} or {wait.event.event_type or '?'}: event → {_exit_text(wait.effective_on_event)}, "
492
+ f"due → {_exit_text(wait.effective_on_due)}"
493
+ )
494
+ exit_ = wait.effective_on_due
495
+ if exit_.action == "ADVANCE_TO":
496
+ return f"{span} → {exit_.stage_id or '?'}"
497
+ if exit_.action in {"FAIL", "DEAD"}:
498
+ return f"{span}, then {exit_.action}"
499
+ return span
500
+
501
+
502
+ def describe_wait_event(event: LedgerWaitEventConfig) -> list[tuple[str, str]]:
503
+ """``(label, text)`` lines for what a WAIT stage's event is matched on and carries into the item."""
504
+ lines: list[tuple[str, str]] = []
505
+ if event.integration_config_id:
506
+ lines.append(("connection", event.integration_config_id))
507
+ lines.extend(("match", f"{m.event_path} = item {m.item_path}") for m in event.match)
508
+ lines.extend(("capture", f"{c.event_path} → {c.item_path} ({c.type})") for c in event.capture)
509
+ if event.filter:
510
+ lines.append(("only when", _filter_text(event.filter)))
511
+ return lines
512
+
513
+
514
+ def _filter_text(flt: dict) -> str:
515
+ """An event filter as one line: its conditions, joined the way its ``match`` combines them."""
516
+ conditions = shape_get(flt, "conditions")
517
+ parts = []
518
+ for c in conditions if isinstance(conditions, list) else []:
519
+ if not isinstance(c, dict):
520
+ continue
521
+ values = shape_get(c, "values")
522
+ value = f"[{', '.join(str(v) for v in values)}]" if isinstance(values, list) else shape_get(c, "value")
523
+ parts.append(" ".join(str(p) for p in (shape_get(c, "path"), shape_get(c, "operator"), value) if p is not None))
524
+ joiner = " or " if str(shape_get(flt, "match") or "ALL").upper() == "ANY" else " and "
525
+ return joiner.join(parts) or "—"
526
+
527
+
435
528
  def describe_stage(stage: dict) -> tuple[str, str, str, str]:
436
529
  """``(name, kind, who, what it does)`` for the console's shape table."""
437
530
  stage_id = str(shape_get(stage, "id") or "?")
438
531
  kind = str(shape_get(stage, "kind") or "").upper()
532
+ if kind == "WAIT":
533
+ wait = LedgerWaitConfig.from_dict(shape_get(stage, "wait")) or LedgerWaitConfig()
534
+ who = wait.event.event_type if wait.event is not None and wait.event.event_type else "clock"
535
+ return stage_id, "wait", who, describe_wait(wait)
439
536
  if kind == "ASK":
440
537
  ask = shape_get(stage, "ask")
441
538
  ask = ask if isinstance(ask, dict) else {}
@@ -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
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import json
14
14
  import sys
15
+ from dataclasses import dataclass
15
16
  from datetime import datetime, timezone
16
17
 
17
18
  import typer
@@ -69,6 +70,24 @@ def format_ts(ms: int | None) -> str:
69
70
  return f"{ms} (raw)"
70
71
 
71
72
 
73
+ def format_relative(ms: int, now: int) -> str:
74
+ """How far ``ms`` is from ``now``, both epoch-ms: ``in 2d 3h`` ahead, ``40m ago`` behind.
75
+
76
+ Two units at most and never less than a minute — a due time is read as
77
+ "soon" or "a while", not to the second.
78
+ """
79
+ delta = abs(ms - now) // 60_000
80
+ days, rest = divmod(delta, 1_440)
81
+ hours, minutes = divmod(rest, 60)
82
+ if days:
83
+ span = f"{days}d {hours}h" if hours else f"{days}d"
84
+ elif hours:
85
+ span = f"{hours}h {minutes}m" if minutes else f"{hours}h"
86
+ else:
87
+ span = f"{max(minutes, 1)}m"
88
+ return f"in {span}" if ms >= now else f"{span} ago"
89
+
90
+
72
91
  def build_dict(required: dict, *, truthy: dict | None = None, keep_zero: dict | None = None) -> dict:
73
92
  """Assemble a wire dict from three buckets, so each call site only
74
93
  has to say which bucket a field belongs in rather than re-deriving
@@ -305,6 +324,73 @@ def build_client(workspace: str | None, base_url: str | None) -> FruxonClient:
305
324
  return FruxonClient(token=token, workspace=workspace, base_url=resolved_base)
306
325
 
307
326
 
327
+ @dataclass(frozen=True)
328
+ class KeyAccess:
329
+ """What the calling key carries, and what the server lets it do in this workspace.
330
+
331
+ ``scopes`` is the key's own list, as minted (``/tokens/current``). A
332
+ personal key acts as its owner, and the server cuts it down on every
333
+ request to what the owner's current workspace role allows
334
+ (fruxon-backend ``RoleScopeMap.CapToRole``), so ``scopes`` can name a
335
+ scope the key is refused. ``role`` and ``effective_scopes`` are that
336
+ cut. A service-account key carries no scopes of its own and no role
337
+ caps it: its ``effective_scopes`` is the account's grant and ``role``
338
+ is ``None``. Both are ``None`` when the server didn't say: the owner's
339
+ membership could not be read, or a service-account key on a server
340
+ older than fruxon-backend #1571.
341
+ """
342
+
343
+ key: dict
344
+ scopes: list[str]
345
+ personal: bool
346
+ role: str | None
347
+ effective_scopes: list[str] | None
348
+
349
+
350
+ def read_key_access(client: FruxonClient) -> KeyAccess | None:
351
+ """Ask the server what ``client``'s key carries and what it may do in this workspace.
352
+
353
+ A server with fruxon-backend #1571 says it all on ``/tokens/current``
354
+ (``effectiveScopes``, and ``ownerRole`` for a personal key). On an
355
+ older one a personal key's cut is worked out from its owner's
356
+ membership (``GET /users/{ownerId}``), a read that needs
357
+ ``workspace:read``.
358
+
359
+ Best-effort: ``None`` when ``/tokens/current`` can't answer (signed in
360
+ by federation, network down, …), and a :class:`KeyAccess` without a
361
+ role when only the membership read fails.
362
+ """
363
+ try:
364
+ key = client.get_current_token()
365
+ except Exception:
366
+ return None
367
+ if not key:
368
+ return None
369
+ scopes = [str(s) for s in key.get("scopes") or []]
370
+ personal = key.get("ownerType") == "USER"
371
+ role = effective = None
372
+ owner = key.get("ownerId")
373
+ reported, owner_role = key.get("effectiveScopes"), key.get("ownerRole")
374
+ if isinstance(reported, list) and (owner_role or not personal):
375
+ # The server's own answer: what this request was authorised with
376
+ # (CapToRole's cut, or a service account's grant). One call, with
377
+ # no workspace:read, which a narrow key may not carry.
378
+ effective = [str(s) for s in reported]
379
+ role = str(owner_role) if owner_role else None
380
+ elif personal and owner:
381
+ try:
382
+ member = client.get_user(str(owner))
383
+ except Exception:
384
+ member = {}
385
+ ceiling = member.get("effectiveScopes")
386
+ if member.get("role") and isinstance(ceiling, list):
387
+ role = str(member["role"])
388
+ allowed = {str(s) for s in ceiling}
389
+ # CapToRole's order: the key's own, keeping what the role carries.
390
+ effective = [s for s in dict.fromkeys(scopes) if s in allowed]
391
+ return KeyAccess(key=key, scopes=scopes, personal=personal, role=role, effective_scopes=effective)
392
+
393
+
308
394
  def load_json_body(file: str, *, what: str) -> dict:
309
395
  """Read and parse a ``--file`` JSON request body into a dict.
310
396