fruxon 2.1.0__tar.gz → 2.2.1__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.1.0 → fruxon-2.2.1}/HISTORY.md +50 -0
  2. {fruxon-2.1.0 → fruxon-2.2.1}/PKG-INFO +6 -2
  3. {fruxon-2.1.0 → fruxon-2.2.1}/README.md +5 -1
  4. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/__init__.py +24 -0
  5. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/_version.py +2 -2
  6. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/checks/ledger.py +97 -0
  7. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/_shared.py +86 -0
  8. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_memory.py +30 -7
  9. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/auth.py +82 -14
  10. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/examples.py +8 -2
  11. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/knowledge_notices.py +4 -17
  12. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/triggers.py +14 -1
  13. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/triggers_ledger.py +244 -6
  14. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/triggers_shape.py +207 -4
  15. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/exceptions.py +15 -0
  16. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/fruxon.py +329 -30
  17. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/models.py +406 -5
  18. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +10 -5
  19. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/ui.py +45 -2
  20. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_checks_ledger.py +97 -0
  21. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_cli.py +148 -0
  22. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_client.py +109 -3
  23. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers.py +64 -0
  24. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers_ledger.py +498 -2
  25. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers_shape.py +190 -0
  26. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_ui.py +175 -0
  27. {fruxon-2.1.0 → fruxon-2.2.1}/.gitignore +0 -0
  28. {fruxon-2.1.0 → fruxon-2.2.1}/LICENSE +0 -0
  29. {fruxon-2.1.0 → fruxon-2.2.1}/pyproject.toml +0 -0
  30. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/__main__.py +0 -0
  31. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/_ssl.py +0 -0
  32. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/checks/__init__.py +0 -0
  33. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/checks/agent.py +0 -0
  34. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/checks/references.py +0 -0
  35. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/__init__.py +0 -0
  36. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/_crash.py +0 -0
  37. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/_schema.py +0 -0
  38. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/_stream.py +0 -0
  39. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents.py +0 -0
  40. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_approvals.py +0 -0
  41. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_budget.py +0 -0
  42. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_channels.py +0 -0
  43. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_draft.py +0 -0
  44. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_evaluations.py +0 -0
  45. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_executions.py +0 -0
  46. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_improvements.py +0 -0
  47. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_lifecycle.py +0 -0
  48. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_reply_style.py +0 -0
  49. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_revisions.py +0 -0
  50. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_sandbox.py +0 -0
  51. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_sandbox_test.py +0 -0
  52. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_slots.py +0 -0
  53. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_system_blocks.py +0 -0
  54. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_tests.py +0 -0
  55. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_topics.py +0 -0
  56. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/agents_voice_calls.py +0 -0
  57. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/applications.py +0 -0
  58. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/applications_entry_points.py +0 -0
  59. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/assets.py +0 -0
  60. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/capabilities.py +0 -0
  61. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/completion.py +0 -0
  62. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/config.py +0 -0
  63. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/consult_pins.py +0 -0
  64. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/conversation_imports.py +0 -0
  65. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/dash.py +0 -0
  66. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/describe.py +0 -0
  67. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/doctor.py +0 -0
  68. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/environments.py +0 -0
  69. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/escalations.py +0 -0
  70. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/guides.py +0 -0
  71. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/integrations.py +0 -0
  72. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/keys.py +0 -0
  73. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/knowledge_bases.py +0 -0
  74. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/knowledge_documents.py +0 -0
  75. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/knowledge_sources.py +0 -0
  76. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/llm_providers.py +0 -0
  77. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/messages.py +0 -0
  78. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/metrics.py +0 -0
  79. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/participants.py +0 -0
  80. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/pipelines.py +0 -0
  81. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/secrets.py +0 -0
  82. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/skills.py +0 -0
  83. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/storage.py +0 -0
  84. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/tools.py +0 -0
  85. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/triggers_budget.py +0 -0
  86. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/triggers_check.py +0 -0
  87. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
  88. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/triggers_questions.py +0 -0
  89. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/cli/workspaces.py +0 -0
  90. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/credentials.py +0 -0
  91. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/device_auth.py +0 -0
  92. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/doctor.py +0 -0
  93. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/flow_validation.py +0 -0
  94. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/output.py +0 -0
  95. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/params.py +0 -0
  96. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/sandbox_scenario.py +0 -0
  97. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/skills/__init__.py +0 -0
  98. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
  99. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/skills/fruxon-build-agent/SKILL.md +0 -0
  100. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
  101. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/skills/fruxon-meet/SKILL.md +0 -0
  102. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
  103. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/telemetry.py +0 -0
  104. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/__init__.py +0 -0
  105. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/app.py +0 -0
  106. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/demo.py +0 -0
  107. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/links.py +0 -0
  108. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/__init__.py +0 -0
  109. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/agent.py +0 -0
  110. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/base.py +0 -0
  111. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/home.py +0 -0
  112. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/ledger.py +0 -0
  113. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/loader.py +0 -0
  114. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/pickers.py +0 -0
  115. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/screens/trace.py +0 -0
  116. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/source.py +0 -0
  117. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/theme.py +0 -0
  118. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/views.py +0 -0
  119. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/tui/widgets.py +0 -0
  120. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/update_check.py +0 -0
  121. {fruxon-2.1.0 → fruxon-2.2.1}/src/fruxon/validation.py +0 -0
  122. {fruxon-2.1.0 → fruxon-2.2.1}/tests/.test_durations.json +0 -0
  123. {fruxon-2.1.0 → fruxon-2.2.1}/tests/__init__.py +0 -0
  124. {fruxon-2.1.0 → fruxon-2.2.1}/tests/conftest.py +0 -0
  125. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_actor.py +0 -0
  126. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_agent_lifecycle.py +0 -0
  127. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_agent_revisions_list.py +0 -0
  128. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_agent_slots.py +0 -0
  129. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_agents_check_cli.py +0 -0
  130. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_agents_revisions_cli.py +0 -0
  131. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_agents_sandbox.py +0 -0
  132. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_applications.py +0 -0
  133. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_approvals.py +0 -0
  134. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_assets.py +0 -0
  135. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_budgets.py +0 -0
  136. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_capabilities.py +0 -0
  137. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_channels.py +0 -0
  138. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_checks_agent.py +0 -0
  139. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_cli_base_url_routing.py +0 -0
  140. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_cli_crash.py +0 -0
  141. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_cli_root_flags.py +0 -0
  142. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_connect_nudge.py +0 -0
  143. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_consult.py +0 -0
  144. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_conversation_imports.py +0 -0
  145. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_credentials.py +0 -0
  146. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_dash_app.py +0 -0
  147. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_dash_cli.py +0 -0
  148. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_dash_client.py +0 -0
  149. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_dash_links.py +0 -0
  150. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_dash_source.py +0 -0
  151. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_describe.py +0 -0
  152. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_device_auth.py +0 -0
  153. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_docs_commands_exist.py +0 -0
  154. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_doctor.py +0 -0
  155. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_draft_bootstrap.py +0 -0
  156. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_draft_evaluate_cli.py +0 -0
  157. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_draft_pull_shape.py +0 -0
  158. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_draft_run_replay.py +0 -0
  159. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_draft_validate_cli.py +0 -0
  160. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_drafts.py +0 -0
  161. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_entry_points.py +0 -0
  162. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_environments.py +0 -0
  163. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_escalations.py +0 -0
  164. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_evaluations.py +0 -0
  165. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_execution_records.py +0 -0
  166. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_flow_validation.py +0 -0
  167. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_fruxon.py +0 -0
  168. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_guides.py +0 -0
  169. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_improvements.py +0 -0
  170. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_integration_configs_write_cli.py +0 -0
  171. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_integration_triggers_cli.py +0 -0
  172. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_integrations_authorize_cli.py +0 -0
  173. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_integrations_connect_cli.py +0 -0
  174. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_kb_trial_cli.py +0 -0
  175. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_knowledge_bases.py +0 -0
  176. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_knowledge_documents.py +0 -0
  177. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_knowledge_notices.py +0 -0
  178. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_knowledge_sources.py +0 -0
  179. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_llm_provider_configs_cli.py +0 -0
  180. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_llm_providers_connect_cli.py +0 -0
  181. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_managed_by.py +0 -0
  182. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_memory.py +0 -0
  183. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_messages.py +0 -0
  184. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_metrics.py +0 -0
  185. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_online_llm_config_refs.py +0 -0
  186. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_online_prefetch_refs.py +0 -0
  187. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_online_secret_refs.py +0 -0
  188. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_output.py +0 -0
  189. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_params.py +0 -0
  190. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_participants.py +0 -0
  191. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_participants_write.py +0 -0
  192. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_pipelines.py +0 -0
  193. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_reply_style.py +0 -0
  194. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_sandbox_scenario.py +0 -0
  195. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_schema.py +0 -0
  196. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_schema_fetch.py +0 -0
  197. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_secrets.py +0 -0
  198. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_skills.py +0 -0
  199. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_ssl.py +0 -0
  200. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_storage.py +0 -0
  201. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_system_blocks.py +0 -0
  202. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_system_blocks_cli.py +0 -0
  203. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_telemetry.py +0 -0
  204. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_test_chats.py +0 -0
  205. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_topics.py +0 -0
  206. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_trigger_budgets.py +0 -0
  207. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers_check_cli.py +0 -0
  208. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers_ledger_batch_views.py +0 -0
  209. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers_ledger_stage_tests.py +0 -0
  210. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers_questions.py +0 -0
  211. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_triggers_write.py +0 -0
  212. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_tui_boundary.py +0 -0
  213. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_update_check.py +0 -0
  214. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_validation.py +0 -0
  215. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_voice_calls.py +0 -0
  216. {fruxon-2.1.0 → fruxon-2.2.1}/tests/test_workspaces_cli.py +0 -0
@@ -1,5 +1,55 @@
1
1
  # History
2
2
 
3
+ ## Unreleased — `chat_user` is deprecated: the server ignores it
4
+
5
+ - `execute`, `stream` and `stream_text` no longer send `chat_user` (as
6
+ `chatUser`). The execution endpoints have no field for it, so the server
7
+ dropped it unread: it did not identify the end user to the agent, gave no
8
+ per-user memory and filled no `{{sender.*}}` values. The argument is still
9
+ accepted, so existing calls keep working, but passing it emits a
10
+ `DeprecationWarning`. To give the agent details about the user, pass them in
11
+ `parameters`.
12
+
13
+ ## Unreleased — `agents memory` names a person by participant id or address
14
+
15
+ - `fruxon agents memory forget-subject` and `list --subject` document the
16
+ subject the server takes: the person's participant id, or one of their
17
+ addresses as `provider:identifier` (e.g. `telegram:123456789`). The old
18
+ example, `user_42`, matched nothing. With fruxon-backend's fix either form
19
+ reaches every memory about the person — those filed under the participant
20
+ id since memory follows the person, and older ones filed under an address.
21
+
22
+ ## Unreleased — whoami and the 403 hint read the role cap off the key
23
+
24
+ - On a server with fruxon-backend #1571, `fruxon whoami` and the 403
25
+ `insufficient_scope` hint take your role and the scopes in effect from
26
+ `/tokens/current` (`ownerRole`, `effectiveScopes`) instead of also reading
27
+ your membership, which needs `workspace:read`. A key without that scope now
28
+ gets its role named instead of "couldn't be read". Older servers keep the
29
+ membership read.
30
+ - **`fruxon whoami`** lists a service-account key's grant (`N scopes from its
31
+ grant`, and `effectiveScopes` under `current_key` in `-o json`) instead of
32
+ calling it "dead weight" for carrying no scopes of its own.
33
+
34
+ ## Unreleased — say when your workspace role, not the key, refuses a call
35
+
36
+ - A 403 `insufficient_scope` for a scope the key already carries no longer
37
+ says to re-run `fruxon login --preset …`. Since fruxon-backend `f5aaf1250`
38
+ the server caps a personal key to its owner's current workspace role, so a
39
+ Viewer's key minted with `agents:write` is refused it, and a new key would be
40
+ refused the same way. The CLI now asks the refused key what it carries
41
+ (`/tokens/current`, then the owner's membership) and, when the key has the
42
+ scope, says the role is the limit and to ask a workspace Admin to raise it or
43
+ make the change. A key that lacks the scope, a service-account key, or a
44
+ lookup that fails keeps the preset hint.
45
+ - **`fruxon whoami`** labels its scope list as the key's own and adds a line
46
+ with your role in the workspace and how many of those scopes it leaves in
47
+ effect. `-o json` adds `ownerType`, `ownerRole` and `effectiveScopes` under
48
+ `current_key` (`null` when the role couldn't be read).
49
+ - `InsufficientScopeError.client` is the client whose request was refused, and
50
+ `FruxonClient.get_user(user)` reads one workspace member, including `role`
51
+ and `effectiveScopes`.
52
+
3
53
  ## Unreleased — read voice calls back
4
54
 
5
55
  - **`fruxon agents voice-calls list <agent>`** lists the agent's Talk calls
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fruxon
3
- Version: 2.1.0
3
+ Version: 2.2.1
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
@@ -172,6 +172,7 @@ Integrations + tools:
172
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. |
173
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. |
174
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`). |
175
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`. |
176
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. |
177
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). |
@@ -182,7 +183,7 @@ Integrations + tools:
182
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. |
183
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. |
184
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. |
185
- | `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. |
186
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. |
187
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. |
188
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. |
@@ -343,6 +344,9 @@ spend = client.get_ledger_spend(trigger_id) # this calendar week, by agent and
343
344
  print(spend.cost, [(a.agent_id, a.cost) for a in spend.agents])
344
345
  for b in client.list_ledger_batches(trigger_id, open_only=True):
345
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
346
350
  ```
347
351
 
348
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).
@@ -137,6 +137,7 @@ Integrations + tools:
137
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. |
138
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. |
139
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`). |
140
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`. |
141
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. |
142
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). |
@@ -147,7 +148,7 @@ Integrations + tools:
147
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. |
148
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. |
149
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. |
150
- | `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. |
151
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. |
152
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. |
153
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. |
@@ -308,6 +309,9 @@ spend = client.get_ledger_spend(trigger_id) # this calendar week, by agent and
308
309
  print(spend.cost, [(a.agent_id, a.cost) for a in spend.agents])
309
310
  for b in client.list_ledger_batches(trigger_id, open_only=True):
310
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
311
315
  ```
312
316
 
313
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).
@@ -81,7 +81,14 @@ from fruxon.models import (
81
81
  LedgerGatheringGroup,
82
82
  LedgerItem,
83
83
  LedgerItemAdmission,
84
+ LedgerItemAttempt,
85
+ LedgerItemAttemptAsk,
86
+ LedgerItemAttemptBatch,
87
+ LedgerItemAttemptOutcome,
88
+ LedgerItemAttemptWait,
84
89
  LedgerItemPoint,
90
+ LedgerItemRuns,
91
+ LedgerItemRunTotals,
85
92
  LedgerItemTimeseries,
86
93
  LedgerItemTransition,
87
94
  LedgerPass,
@@ -98,6 +105,11 @@ from fruxon.models import (
98
105
  LedgerStageTestSimulatedCall,
99
106
  LedgerStageTestSink,
100
107
  LedgerStageTestVerdict,
108
+ LedgerWaitCapture,
109
+ LedgerWaitConfig,
110
+ LedgerWaitEventConfig,
111
+ LedgerWaitExit,
112
+ LedgerWaitMatch,
101
113
  ManagedBy,
102
114
  ManagedResourceLink,
103
115
  Memory,
@@ -216,6 +228,13 @@ __all__ = [
216
228
  "LedgerGatheringGroup",
217
229
  "LedgerItem",
218
230
  "LedgerItemAdmission",
231
+ "LedgerItemAttempt",
232
+ "LedgerItemAttemptAsk",
233
+ "LedgerItemAttemptBatch",
234
+ "LedgerItemAttemptOutcome",
235
+ "LedgerItemAttemptWait",
236
+ "LedgerItemRuns",
237
+ "LedgerItemRunTotals",
219
238
  "LedgerItemTransition",
220
239
  "LedgerPass",
221
240
  "LedgerPurgeResult",
@@ -225,6 +244,11 @@ __all__ = [
225
244
  "LedgerStageTestSimulatedCall",
226
245
  "LedgerStageTestSink",
227
246
  "LedgerStageTestVerdict",
247
+ "LedgerWaitCapture",
248
+ "LedgerWaitConfig",
249
+ "LedgerWaitEventConfig",
250
+ "LedgerWaitExit",
251
+ "LedgerWaitMatch",
228
252
  "LedgerSkippedItem",
229
253
  "ManagedBy",
230
254
  "Memory",
@@ -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.1.0'
22
- __version_tuple__ = version_tuple = (2, 1, 0)
21
+ __version__ = version = '2.2.1'
22
+ __version_tuple__ = version_tuple = (2, 2, 1)
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 {}
@@ -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
 
@@ -51,7 +51,13 @@ def memory_list(
51
51
  agent: Annotated[str, typer.Argument(help="Agent whose memories to list.")],
52
52
  subject: Annotated[
53
53
  str | None,
54
- typer.Option("--subject", help="Only memories about this subject (person / end-user key)."),
54
+ typer.Option(
55
+ "--subject",
56
+ help=(
57
+ "Only memories about this person: their participant id, or a channel address as "
58
+ "provider:identifier (e.g. telegram:123456789)."
59
+ ),
60
+ ),
55
61
  ] = None,
56
62
  query: Annotated[str | None, typer.Option("--query", "-q", help="Full-text filter over title + content.")] = None,
57
63
  scope: Annotated[
@@ -75,10 +81,13 @@ def memory_list(
75
81
  """List what an agent remembers — narrow with --subject / --query / --scope.
76
82
 
77
83
  The filters run server-side, so "what does my agent remember about
78
- user_42" is `--subject user_42`, not a client-side scan.
84
+ the Telegram user 123456789" is `--subject telegram:123456789`, not a
85
+ client-side scan. A subject is the person's participant id or one of
86
+ their addresses as provider:identifier; either matches every memory
87
+ about them.
79
88
 
80
89
  Examples:
81
- fruxon agents memory list my-agent --subject user_42
90
+ fruxon agents memory list my-agent --subject telegram:123456789
82
91
  fruxon agents memory list my-agent --query "dark mode"
83
92
  fruxon agents memory list my-agent --scope AGENT --output id
84
93
  """
@@ -228,7 +237,15 @@ def memory_get(
228
237
  @memory_app.command("forget-subject")
229
238
  def memory_forget_subject(
230
239
  agent: Annotated[str, typer.Argument(help="Agent whose memories to prune.")],
231
- subject: Annotated[str, typer.Argument(help="The subject (person / end-user key) to forget entirely.")],
240
+ subject: Annotated[
241
+ str,
242
+ typer.Argument(
243
+ help=(
244
+ "The person to forget entirely: their participant id, or a channel address as "
245
+ "provider:identifier (e.g. telegram:123456789)."
246
+ )
247
+ ),
248
+ ],
232
249
  yes: Annotated[
233
250
  bool,
234
251
  typer.Option("--yes", "-y", help="Skip the confirmation prompt. REQUIRED under agent mode (CLAUDECODE / CI)."),
@@ -242,11 +259,17 @@ def memory_forget_subject(
242
259
  ):
243
260
  """Forget every memory an agent holds about one subject (GDPR-style).
244
261
 
245
- Irreversible — it deletes all of the subject's memories. Confirms
246
- first, and under agent mode refuses without [bold]--yes[/bold].
262
+ Irreversible — it deletes all of the person's memories, whether they
263
+ are filed under the participant id or under any of the person's
264
+ addresses, and records each deletion in the agent's deletion trail.
265
+ Confirms first, and under agent mode refuses without [bold]--yes[/bold].
266
+
267
+ Name the person by participant id (the `subject` column of
268
+ `agents memory subjects`) or by an address as provider:identifier.
247
269
 
248
270
  Examples:
249
- fruxon agents memory forget-subject my-agent user_42 --yes
271
+ fruxon agents memory forget-subject my-agent telegram:123456789 --yes
272
+ fruxon agents memory forget-subject my-agent 0199f3c2-5b1e-7c4a-9d2e-3f6a8b1c0d4e --yes
250
273
  """
251
274
  output = resolve_output_format(output, human_default="text", agent_default="json")
252
275
  if output not in {"text", "json"}: