flowjet 2.0.2__tar.gz → 2.0.4__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.

Potentially problematic release.


This version of flowjet might be problematic. Click here for more details.

Files changed (169) hide show
  1. {flowjet-2.0.2 → flowjet-2.0.4}/CHANGELOG.md +18 -0
  2. {flowjet-2.0.2 → flowjet-2.0.4}/PKG-INFO +2 -2
  3. {flowjet-2.0.2 → flowjet-2.0.4}/pyproject.toml +2 -2
  4. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/agent.py +23 -70
  5. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/cli.py +135 -4
  6. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/progress.py +34 -0
  7. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/bridges/nano/mapping.py +193 -2
  8. flowjet-2.0.4/src/flowjet/core/home.py +127 -0
  9. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/__main__.py +9 -0
  10. {flowjet-2.0.2 → flowjet-2.0.4}/.gitignore +0 -0
  11. {flowjet-2.0.2 → flowjet-2.0.4}/LICENSE +0 -0
  12. {flowjet-2.0.2 → flowjet-2.0.4}/README.md +0 -0
  13. {flowjet-2.0.2 → flowjet-2.0.4}/config/nano.yml +0 -0
  14. {flowjet-2.0.2 → flowjet-2.0.4}/deploy/README.md +0 -0
  15. {flowjet-2.0.2 → flowjet-2.0.4}/deploy/nano.yml +0 -0
  16. {flowjet-2.0.2 → flowjet-2.0.4}/docs/completion-design.md +0 -0
  17. {flowjet-2.0.2 → flowjet-2.0.4}/docs/drafts/README.md +0 -0
  18. {flowjet-2.0.2 → flowjet-2.0.4}/docs/drafts/api-spec.md +0 -0
  19. {flowjet-2.0.2 → flowjet-2.0.4}/docs/impl/IG-001-openai-compatible-server.md +0 -0
  20. {flowjet-2.0.2 → flowjet-2.0.4}/docs/impl/IG-002-isolated-thread-pool-runtime.md +0 -0
  21. {flowjet-2.0.2 → flowjet-2.0.4}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
  22. {flowjet-2.0.2 → flowjet-2.0.4}/docs/impl/README.md +0 -0
  23. {flowjet-2.0.2 → flowjet-2.0.4}/docs/impl/TEST-COVERAGE-MATRIX.md +0 -0
  24. {flowjet-2.0.2 → flowjet-2.0.4}/docs/naming.md +0 -0
  25. {flowjet-2.0.2 → flowjet-2.0.4}/docs/server.md +0 -0
  26. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
  27. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
  28. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
  29. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/RFC-004-unified-package.md +0 -0
  30. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/rfc-history.md +0 -0
  31. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/rfc-index.md +0 -0
  32. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/rfc-namings.md +0 -0
  33. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/templates/architecture-design.md +0 -0
  34. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/templates/conceptual-design.md +0 -0
  35. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/templates/impl-interface-design.md +0 -0
  36. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/templates/rfc-standard.md +0 -0
  37. {flowjet-2.0.2 → flowjet-2.0.4}/docs/specs/templates/rfc-template.md +0 -0
  38. {flowjet-2.0.2 → flowjet-2.0.4}/docs/upgrade.md +0 -0
  39. {flowjet-2.0.2 → flowjet-2.0.4}/examples/README.md +0 -0
  40. {flowjet-2.0.2 → flowjet-2.0.4}/nano.yml +0 -0
  41. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/__init__.py +0 -0
  42. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/__main__.py +0 -0
  43. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/README.md +0 -0
  44. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
  45. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
  46. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
  47. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
  48. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
  49. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
  50. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
  51. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
  52. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
  53. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
  54. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
  55. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
  56. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
  57. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
  58. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
  59. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
  60. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
  61. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
  62. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
  63. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
  64. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
  65. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
  66. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
  67. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
  68. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
  69. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
  70. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
  71. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
  72. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
  73. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
  74. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
  75. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
  76. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
  77. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
  78. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
  79. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
  80. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
  81. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
  82. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
  83. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
  84. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
  85. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
  86. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
  87. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
  88. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
  89. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
  90. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
  91. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
  92. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
  93. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
  94. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
  95. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
  96. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
  97. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
  98. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
  99. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
  100. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
  101. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
  102. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
  103. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
  104. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
  105. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
  106. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
  107. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
  108. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
  109. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
  110. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
  111. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
  112. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
  113. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
  114. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
  115. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
  116. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/__init__.py +0 -0
  117. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/__main__.py +0 -0
  118. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/completion/__init__.py +0 -0
  119. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/completion/context.py +0 -0
  120. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/completion/engine.py +0 -0
  121. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/doctor_cmd.py +0 -0
  122. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
  123. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/serve_cmd.py +0 -0
  124. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/setup_cmd.py +0 -0
  125. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/shell/__init__.py +0 -0
  126. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/shell/_fj.zsh +0 -0
  127. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/shell/fj.bash +0 -0
  128. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/stream.py +0 -0
  129. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/threads.py +0 -0
  130. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/cli/tool_stream.py +0 -0
  131. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/__init__.py +0 -0
  132. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backend.py +0 -0
  133. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/__init__.py +0 -0
  134. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/direct.py +0 -0
  135. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/fake.py +0 -0
  136. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/__init__.py +0 -0
  137. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/adapter.py +0 -0
  138. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/admission.py +0 -0
  139. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/backend.py +0 -0
  140. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/errors.py +0 -0
  141. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/mode_gate.py +0 -0
  142. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/pool.py +0 -0
  143. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/request.py +0 -0
  144. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/backends/isolation/workspace.py +0 -0
  145. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/bootstrap.py +0 -0
  146. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/bridges/nano/__init__.py +0 -0
  147. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/bridges/nano/adapter.py +0 -0
  148. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/bridges/nano/backend.py +0 -0
  149. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/events.py +0 -0
  150. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/identity.py +0 -0
  151. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/modes.py +0 -0
  152. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/protocol.py +0 -0
  153. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/tool_results.py +0 -0
  154. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/core/tool_stream.py +0 -0
  155. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/__init__.py +0 -0
  156. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/acp/__init__.py +0 -0
  157. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/acp/agent.py +0 -0
  158. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/acp/routes.py +0 -0
  159. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/config.py +0 -0
  160. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/http/__init__.py +0 -0
  161. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/http/app.py +0 -0
  162. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/http/auth.py +0 -0
  163. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/openai_compat/__init__.py +0 -0
  164. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/openai_compat/errors.py +0 -0
  165. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/openai_compat/projection.py +0 -0
  166. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/openai_compat/routes.py +0 -0
  167. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/openai_compat/schemas.py +0 -0
  168. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/openai_compat/service.py +0 -0
  169. {flowjet-2.0.2 → flowjet-2.0.4}/src/flowjet/server/openai_compat/store.py +0 -0
@@ -10,6 +10,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [2.0.4] — 2026-09-19
14
+
15
+ ### Changed
16
+
17
+ - Bumped `soothe` lower bound to `1.0.13` (was `1.0.12`). soothe-nano stays
18
+ pinned at `>=1.2.28,<1.3.0`.
19
+
20
+ ## [2.0.3] — 2026-09-18
21
+
22
+ ### Added
23
+
24
+ - **Plan-step and middleware-phase progress in the CLI.** The nano bridge now
25
+ maps the host (soothe) loop's todo list and middleware phase updates into
26
+ step/tool progress events, so the terminal shows `Step 2/3 · Create a2.txt`
27
+ instead of a bare `Working` line. Late-parsing tool args refresh the status
28
+ line once, and `write_todos` bookkeeping is suppressed as a tool call (it
29
+ already drives the step line).
30
+
13
31
  ## [2.0.2] — 2026-09-18
14
32
 
15
33
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flowjet
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by soothe-nano
5
5
  Project-URL: Homepage, https://github.com/caesar0301/flowjet
6
6
  Project-URL: Repository, https://github.com/caesar0301/flowjet
@@ -23,7 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Python: <3.15,>=3.11
25
25
  Requires-Dist: soothe-nano<1.3.0,>=1.2.28
26
- Requires-Dist: soothe<2.0.0,>=1.0.12
26
+ Requires-Dist: soothe<2.0.0,>=1.0.13
27
27
  Provides-Extra: dev
28
28
  Requires-Dist: httpx>=0.27.0; extra == 'dev'
29
29
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flowjet"
7
- version = "2.0.2"
7
+ version = "2.0.4"
8
8
  description = "FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by soothe-nano"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -31,7 +31,7 @@ classifiers = [
31
31
  dependencies = [
32
32
  # Default CLI runtime (FLOWJET_BACKEND=soothe). soothe is a host layer over
33
33
  # soothe-nano, so nano stays pinned explicitly: soothe only caps it at <2.
34
- "soothe>=1.0.12,<2.0.0",
34
+ "soothe>=1.0.13,<2.0.0",
35
35
  "soothe-nano>=1.2.28,<1.3.0",
36
36
  ]
37
37
 
@@ -8,14 +8,12 @@ Nano bootstrap, config loading and the builtin-skill root now live in
8
8
  from __future__ import annotations
9
9
 
10
10
  import logging
11
- import os
12
- import sys
13
- from logging.handlers import RotatingFileHandler
14
11
  from pathlib import Path
15
12
  from typing import Any
16
13
 
17
14
  from soothe_nano.config import SootheConfig
18
15
 
16
+ from flowjet.cli.cli import _CompactConsoleFormatter
19
17
  from flowjet.core.bootstrap import (
20
18
  BUILTIN_SKILLS_DIR,
21
19
  FJ_CORE_SKILL_NAMES,
@@ -31,6 +29,27 @@ from flowjet.core.modes import CLI_PROFILE, apply_profile
31
29
 
32
30
  logger = logging.getLogger(__name__)
33
31
 
32
+ # ---------------------------------------------------------------------------
33
+ # CLI logging (delegated to ``flowjet.cli.cli`` at call time)
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def configure_cli_logging(*, verbose: bool = False) -> None:
38
+ """Quiet the console for one-shot CLI use (see ``flowjet.cli.cli``).
39
+
40
+ Delegated dynamically so monkeypatching ``flowjet.cli.cli.configure_cli_logging``
41
+ (as tests do via ``monkeypatch.setattr(cli, ...)``) is honored here too.
42
+ """
43
+ from flowjet.cli import cli as _cli
44
+
45
+ _cli.configure_cli_logging(verbose=verbose)
46
+
47
+
48
+ def silence_after_plugins(*, verbose: bool = False) -> None:
49
+ """Re-apply quieting after plugin imports (belt-and-suspenders)."""
50
+ configure_cli_logging(verbose=verbose)
51
+
52
+
34
53
  # ---------------------------------------------------------------------------
35
54
  # Builtin skills (re-exported; owned by flowjet.core.bootstrap)
36
55
  # ---------------------------------------------------------------------------
@@ -57,73 +76,6 @@ def apply_fj_defaults(config: SootheConfig) -> SootheConfig:
57
76
  return apply_profile(config, CLI_PROFILE)
58
77
 
59
78
 
60
- # ---------------------------------------------------------------------------
61
- # CLI logging
62
- # ---------------------------------------------------------------------------
63
-
64
- _BROWSER_USE_SETUP_LOGGING = "BROWSER_USE_SETUP_LOGGING"
65
- _FJ_CONSOLE_HANDLER = "fj-console"
66
-
67
-
68
- class _CompactConsoleFormatter(logging.Formatter):
69
- """One-line console records without exception tracebacks."""
70
-
71
- def formatException(self, ei: object) -> str: # noqa: N802 - logging API
72
- return ""
73
-
74
- def format(self, record: logging.LogRecord) -> str:
75
- # logger.exception sets exc_info; drop it so format() stays one line.
76
- record.exc_info = None
77
- record.exc_text = None
78
- return super().format(record)
79
-
80
-
81
- def configure_cli_logging(*, verbose: bool = False) -> None:
82
- """Quiet the console for one-shot CLI use.
83
-
84
- - Opt out of ``browser_use`` import-time root logger setup when unset.
85
- - Remove existing root stream handlers (stderr/stdout) so init INFO
86
- lines do not interleave with the agent answer.
87
- - Prevent ``lastResort`` traceback dumps from soothe tool failures.
88
- - When ``verbose``, show WARNING+ as single-line messages on stderr.
89
- """
90
- os.environ.setdefault(_BROWSER_USE_SETUP_LOGGING, "false")
91
- _remove_root_console_handlers()
92
- root = logging.getLogger()
93
- if verbose:
94
- handler = logging.StreamHandler(sys.stderr)
95
- handler.set_name(_FJ_CONSOLE_HANDLER)
96
- handler.setLevel(logging.WARNING)
97
- handler.setFormatter(_CompactConsoleFormatter("%(message)s"))
98
- root.addHandler(handler)
99
- elif not root.handlers:
100
- null = logging.NullHandler()
101
- null.set_name(_FJ_CONSOLE_HANDLER)
102
- root.addHandler(null)
103
- if root.level < logging.WARNING:
104
- root.setLevel(logging.WARNING)
105
-
106
-
107
- def silence_after_plugins(*, verbose: bool = False) -> None:
108
- """Re-apply quieting after plugin imports (belt-and-suspenders)."""
109
- configure_cli_logging(verbose=verbose)
110
-
111
-
112
- def _remove_root_console_handlers() -> None:
113
- root = logging.getLogger()
114
- for handler in list(root.handlers):
115
- if isinstance(handler, RotatingFileHandler):
116
- continue
117
- if isinstance(handler, logging.FileHandler):
118
- continue
119
- if isinstance(handler, logging.StreamHandler) or isinstance(handler, logging.NullHandler):
120
- root.removeHandler(handler)
121
- try:
122
- handler.close()
123
- except Exception: # pragma: no cover - defensive
124
- pass
125
-
126
-
127
79
  # ---------------------------------------------------------------------------
128
80
  # Agent build
129
81
  # ---------------------------------------------------------------------------
@@ -165,6 +117,7 @@ async def build_agent(
165
117
  __all__ = [
166
118
  "BUILTIN_SKILLS_DIR",
167
119
  "FJ_CORE_SKILL_NAMES",
120
+ "_CompactConsoleFormatter",
168
121
  "apply_fj_defaults",
169
122
  "build_agent",
170
123
  "configure_cli_logging",
@@ -8,18 +8,113 @@ from __future__ import annotations
8
8
  import argparse
9
9
  import asyncio
10
10
  import contextlib
11
+ import logging
12
+ import os
11
13
  import sys
12
14
  from collections.abc import Coroutine
15
+ from logging.handlers import RotatingFileHandler
13
16
  from pathlib import Path
14
17
  from typing import Any
15
18
 
16
19
  from flowjet.cli import __version__
17
- from flowjet.cli.agent import configure_cli_logging
18
20
 
19
21
  FORMAL_CLI = "flowjet"
20
22
  CLI_ALIASES = frozenset({"fj", "fjf", FORMAL_CLI})
21
23
 
22
24
 
25
+ # ---------------------------------------------------------------------------
26
+ # CLI logging — stdlib only, kept here so ``fj -V`` / ``fj -h`` never pay the
27
+ # heavy ``soothe_nano`` / ``flowjet.core.bootstrap`` import cost.
28
+ # ---------------------------------------------------------------------------
29
+
30
+ _BROWSER_USE_SETUP_LOGGING = "BROWSER_USE_SETUP_LOGGING"
31
+ _FJ_CONSOLE_HANDLER = "fj-console"
32
+
33
+
34
+ class _CompactConsoleFormatter(logging.Formatter):
35
+ """One-line console records without exception tracebacks."""
36
+
37
+ def formatException(self, ei: object) -> str: # noqa: N802 - logging API
38
+ return ""
39
+
40
+ def format(self, record: logging.LogRecord) -> str:
41
+ # logger.exception sets exc_info; drop it so format() stays one line.
42
+ record.exc_info = None
43
+ record.exc_text = None
44
+ return super().format(record)
45
+
46
+
47
+ _FILE_LOGGING_INSTALLED = False
48
+
49
+
50
+ def configure_cli_logging(*, verbose: bool = False) -> None:
51
+ """Quiet the console for one-shot CLI use and enable file logging by default.
52
+
53
+ - Opt out of ``browser_use`` import-time root logger setup when unset.
54
+ - Remove existing root stream handlers (stderr/stdout) so init INFO
55
+ lines do not interleave with the agent answer.
56
+ - Prevent ``lastResort`` traceback dumps from soothe tool failures.
57
+ - When ``verbose``, show WARNING+ as single-line messages on stderr.
58
+ - Attach a rotating file handler at ``<FLOWJET_HOME>/logs/cli.log`` so
59
+ every CLI run is recorded by default (idempotent across re-invocations
60
+ within the same process, e.g. tests).
61
+ """
62
+ os.environ.setdefault(_BROWSER_USE_SETUP_LOGGING, "false")
63
+ _remove_root_console_handlers()
64
+ root = logging.getLogger()
65
+ if verbose:
66
+ handler = logging.StreamHandler(sys.stderr)
67
+ handler.set_name(_FJ_CONSOLE_HANDLER)
68
+ handler.setLevel(logging.WARNING)
69
+ handler.setFormatter(_CompactConsoleFormatter("%(message)s"))
70
+ root.addHandler(handler)
71
+ elif not root.handlers:
72
+ null = logging.NullHandler()
73
+ null.set_name(_FJ_CONSOLE_HANDLER)
74
+ root.addHandler(null)
75
+ if root.level < logging.WARNING:
76
+ root.setLevel(logging.WARNING)
77
+
78
+ _ensure_cli_file_logging()
79
+
80
+
81
+ def _ensure_cli_file_logging() -> None:
82
+ """Attach the rotating file handler for CLI runs (idempotent)."""
83
+ global _FILE_LOGGING_INSTALLED
84
+ if _FILE_LOGGING_INSTALLED:
85
+ return
86
+ try:
87
+ from flowjet.core.home import cli_log_path, ensure_flowjet_home
88
+
89
+ ensure_flowjet_home()
90
+ from soothe_nano.logging.setup import setup_logging
91
+
92
+ setup_logging(log_file=str(cli_log_path()), extra_logger_names=("flowjet",))
93
+ _FILE_LOGGING_INSTALLED = True
94
+ except Exception: # pragma: no cover - logging must never break the CLI
95
+ pass
96
+
97
+
98
+ def silence_after_plugins(*, verbose: bool = False) -> None:
99
+ """Re-apply quieting after plugin imports (belt-and-suspenders)."""
100
+ configure_cli_logging(verbose=verbose)
101
+
102
+
103
+ def _remove_root_console_handlers() -> None:
104
+ root = logging.getLogger()
105
+ for handler in list(root.handlers):
106
+ if isinstance(handler, RotatingFileHandler):
107
+ continue
108
+ if isinstance(handler, logging.FileHandler):
109
+ continue
110
+ if isinstance(handler, logging.StreamHandler) or isinstance(handler, logging.NullHandler):
111
+ root.removeHandler(handler)
112
+ try:
113
+ handler.close()
114
+ except Exception: # pragma: no cover - defensive
115
+ pass
116
+
117
+
23
118
  def resolve_cli_prog(argv0: str | None = None) -> str:
24
119
  """Return the invoked command name for help / version strings.
25
120
 
@@ -217,6 +312,13 @@ class _HelpFormatter(argparse.RawDescriptionHelpFormatter):
217
312
  def __init__(self, prog: str) -> None:
218
313
  super().__init__(prog, max_help_position=26, width=92)
219
314
 
315
+ def _format_action(self, action: argparse.Action) -> str: # type: ignore[override]
316
+ # Resolve lazy help strings only when help is actually rendered, so
317
+ # ``-V`` never pays for the import behind ``_default_config_help``.
318
+ if isinstance(action.help, _LazyHelp):
319
+ action.help = str(action.help)
320
+ return super()._format_action(action)
321
+
220
322
 
221
323
  def _default_config_help() -> str:
222
324
  from flowjet.cli.agent import default_config_path
@@ -224,6 +326,26 @@ def _default_config_help() -> str:
224
326
  return f"Alternate nano.yml (default: {default_config_path()})"
225
327
 
226
328
 
329
+ class _LazyHelp:
330
+ """Resolve a help-string callable only when argparse reads it.
331
+
332
+ ``-V`` / ``-h``-without-our-flag paths never touch the help text, so the
333
+ heavy import behind ``_default_config_help`` stays deferred until a flag
334
+ that actually shows help (``-h``) or errors is invoked.
335
+ """
336
+
337
+ __slots__ = ("_factory", "_cached")
338
+
339
+ def __init__(self, factory: Any) -> None:
340
+ self._factory = factory
341
+ self._cached: str | None = None
342
+
343
+ def __str__(self) -> str:
344
+ if self._cached is None:
345
+ self._cached = self._factory()
346
+ return self._cached
347
+
348
+
227
349
  def _build_parser(prog: str | None = None) -> argparse.ArgumentParser:
228
350
  cli_prog = prog or resolve_cli_prog()
229
351
  parser = argparse.ArgumentParser(
@@ -297,7 +419,7 @@ def _build_parser(prog: str | None = None) -> argparse.ArgumentParser:
297
419
  "-c",
298
420
  "--config",
299
421
  metavar="PATH",
300
- help=_default_config_help(),
422
+ help=_LazyHelp(_default_config_help),
301
423
  )
302
424
  paths.add_argument(
303
425
  "-w",
@@ -324,7 +446,7 @@ def _build_setup_parser(prog: str | None = None) -> argparse.ArgumentParser:
324
446
  "-c",
325
447
  "--config",
326
448
  metavar="PATH",
327
- help=_default_config_help(),
449
+ help=_LazyHelp(_default_config_help),
328
450
  )
329
451
  return parser
330
452
 
@@ -565,8 +687,17 @@ def main_follow(argv: list[str] | None = None) -> int:
565
687
 
566
688
  def main(argv: list[str] | None = None) -> int:
567
689
  try:
568
- configure_cli_logging()
690
+ # Align SOOTHE_HOME with FLOWJET_HOME before any soothe import so
691
+ # config, data, and logs all land under ~/.flowjet. Must run first:
692
+ # soothe_sdk.paths binds SOOTHE_HOME at import time.
693
+ from flowjet.core.home import ensure_flowjet_home
694
+
695
+ ensure_flowjet_home()
696
+ # Parse first so ``-V`` / ``-h`` / ``--help`` exit before we touch the
697
+ # logging stack (and the heavy ``flowjet.cli.agent`` import it used to
698
+ # drag in). Version/help must stay instant.
569
699
  args = parse_args(argv)
700
+ configure_cli_logging()
570
701
  # Re-apply after parse so ``-v`` can enable compact console warnings.
571
702
  if getattr(args, "verbose", False):
572
703
  configure_cli_logging(verbose=True)
@@ -67,6 +67,14 @@ _ACTION_LABELS: dict[str, str] = {
67
67
  "queued": "Queued",
68
68
  }
69
69
 
70
+ # Host (soothe) loop phases surfaced as graph updates by the nano bridge.
71
+ _STEP_PHASE_LABELS: dict[str, str] = {
72
+ "planned": "Planning steps",
73
+ "decomposed": "Decomposing task",
74
+ "evaluated": "Evaluating step",
75
+ "fanned_out": "Coordinating agents",
76
+ }
77
+
70
78
  # Verb + color for well-known tools (fallback: Running / yellow)
71
79
  _TOOL_VERBS: dict[str, tuple[str, str]] = {
72
80
  "read_file": ("Reading", "yellow"),
@@ -693,6 +701,32 @@ def friendly_progress(data: dict[str, Any]) -> tuple[str, str] | None:
693
701
  label = f"{label} · {detail}"
694
702
  return _fit(label), color
695
703
 
704
+ if domain == "step":
705
+ # Plan steps come from the agent's todo list (one event per change).
706
+ if "steps" in data or "todo" in data:
707
+ step = data.get("step")
708
+ steps = data.get("steps")
709
+ todo = _compact(data.get("todo"))
710
+ head = f"Step {step}/{steps}" if step and steps else "Step"
711
+ if todo:
712
+ return _fit(f"{head} · {todo}"), "blue"
713
+ if data.get("status") == "completed":
714
+ return _fit(f"Steps complete · {step}/{steps}"), "green"
715
+ return _fit(head), "blue"
716
+ label = _STEP_PHASE_LABELS.get(action) or verb
717
+ return _fit(label), "blue"
718
+
719
+ if event_type == "soothe.ask.requested":
720
+ question = _compact(data.get("question") or data.get("message"))
721
+ if question:
722
+ q_limit = _detail_budget(prefix="Preparing question · ", fraction=0.55, floor=16)
723
+ return _fit(f"Preparing question · {_truncate_middle(question, q_limit)}"), "magenta"
724
+ return _fit("Preparing question"), "magenta"
725
+
726
+ if event_type == "soothe.intake.routed":
727
+ name = _compact(data.get("subagent") or data.get("subagent_type") or "specialist")
728
+ return _fit(f"Routing · {name}"), "magenta"
729
+
696
730
  if domain == "skill" or "skill" in short:
697
731
  color = "blue"
698
732
  name = data.get("skill") or data.get("name") or "skill"
@@ -33,6 +33,161 @@ _SKIP_CUSTOM_TYPES = frozenset(
33
33
  }
34
34
  )
35
35
 
36
+ # Host (soothe) middleware surfaces as ``<Middleware>.<phase>`` graph updates.
37
+ # The CLI renders these through ``friendly_progress``, so the synthetic payloads
38
+ # below stay in the same ``soothe.*`` vocabulary as nano's custom events.
39
+ _HOST_PHASE_EVENTS: dict[str, str] = {
40
+ "GoalStepGuardMiddleware": "soothe.step.planned",
41
+ "DecomposeTaskMiddleware": "soothe.step.decomposed",
42
+ "EvalStepMiddleware": "soothe.step.evaluated",
43
+ "WestWorldMiddleware": "soothe.step.fanned_out",
44
+ "AskUserPromptMiddleware": "soothe.ask.requested",
45
+ "IntakeOnlyTaskGuardMiddleware": "soothe.intake.routed",
46
+ }
47
+
48
+ # Plan bookkeeping: the todo list already drives the step line.
49
+ _QUIET_TOOLS = frozenset({"write_todos", "todo_write", "write_todo_list"})
50
+
51
+ # Fires on every turn with no user-visible content.
52
+ _SKIP_UPDATE_NODES = frozenset(
53
+ {
54
+ "PatchToolCallsMiddleware",
55
+ "ProgressiveToolMiddleware",
56
+ "WorkspaceContextMiddleware",
57
+ "TodoListMiddleware",
58
+ "model",
59
+ "tools",
60
+ }
61
+ )
62
+
63
+
64
+ def todo_step_payload(todos: list[Any]) -> dict[str, Any] | None:
65
+ """Turn a soothe todo list into a step-progress payload.
66
+
67
+ The agent writes its plan as todos (``write_todos``); each ``in_progress``
68
+ item is the step the user is waiting on, which is far more informative than
69
+ a generic "Working" line.
70
+ """
71
+ items = [item for item in todos if isinstance(item, dict)]
72
+ if not items:
73
+ return None
74
+ total = len(items)
75
+ done = sum(1 for item in items if str(item.get("status")) == "completed")
76
+ current = next(
77
+ (item for item in items if str(item.get("status")) == "in_progress"),
78
+ None,
79
+ )
80
+ if current is None:
81
+ if done == 0:
82
+ return None
83
+ return {
84
+ "type": "soothe.step.progress",
85
+ "step": done,
86
+ "steps": total,
87
+ "done": done,
88
+ "status": "completed",
89
+ "todo": "",
90
+ }
91
+ return {
92
+ "type": "soothe.step.progress",
93
+ "step": items.index(current) + 1,
94
+ "steps": total,
95
+ "done": done,
96
+ "status": "in_progress",
97
+ "todo": str(current.get("content") or ""),
98
+ }
99
+
100
+
101
+ def _skill_names(payload: dict[str, Any]) -> list[str]:
102
+ """Best-effort skill names from a ``SkillActivationMiddleware`` payload."""
103
+ raw = payload.get("skill_activation")
104
+ if isinstance(raw, dict):
105
+ raw = raw.get("skills") or raw.get("activated") or raw.get("names") or []
106
+ if not isinstance(raw, (list, tuple)):
107
+ return []
108
+ names: list[str] = []
109
+ for item in raw:
110
+ if isinstance(item, str) and item.strip():
111
+ names.append(item.strip())
112
+ elif isinstance(item, dict):
113
+ name = item.get("name") or item.get("skill")
114
+ if isinstance(name, str) and name.strip():
115
+ names.append(name.strip())
116
+ return names
117
+
118
+
119
+ def update_progress(data: dict[str, Any]) -> tuple[str, dict[str, Any]] | None:
120
+ """Map one soothe ``updates`` chunk to ``(dedupe_key, event_payload)``.
121
+
122
+ Returns ``None`` for the plumbing updates that carry no user-visible step.
123
+ """
124
+ todos = data.get("todos")
125
+ if isinstance(todos, list):
126
+ payload = todo_step_payload(todos)
127
+ if payload is not None:
128
+ key = f"todo:{payload['step']}/{payload['steps']}:{payload['todo']}:{payload['done']}"
129
+ return key, payload
130
+
131
+ for node, node_payload in data.items():
132
+ name = node.split(".")[0]
133
+ # The todo list rides along inside the ``tools`` node update.
134
+ if isinstance(node_payload, dict) and isinstance(node_payload.get("todos"), list):
135
+ payload = todo_step_payload(node_payload["todos"])
136
+ if payload is not None:
137
+ key = (
138
+ f"todo:{payload['step']}/{payload['steps']}:{payload['todo']}:{payload['done']}"
139
+ )
140
+ return key, payload
141
+ if name in _SKIP_UPDATE_NODES:
142
+ continue
143
+ event = _HOST_PHASE_EVENTS.get(name)
144
+ if event is not None:
145
+ phase = node.split(".", 1)[1] if "." in node else ""
146
+ return node, {"type": event, "middleware": name, "phase": phase}
147
+ if name == "SkillActivationMiddleware" and isinstance(node_payload, dict):
148
+ names = _skill_names(node_payload)
149
+ if names:
150
+ return node, {"type": "soothe.skill.activated", "skill": ", ".join(names)}
151
+ return None
152
+
153
+
154
+ # Arg keys worth echoing in a one-line tool summary (first match wins).
155
+ _TOOL_SUMMARY_KEYS = (
156
+ "file_path",
157
+ "path",
158
+ "target_file",
159
+ "command",
160
+ "cmd",
161
+ "script",
162
+ "query",
163
+ "pattern",
164
+ "url",
165
+ "skill",
166
+ )
167
+
168
+
169
+ def tool_call_summary(name: str, args: dict[str, Any]) -> str:
170
+ """``write_file a.txt``-style summary for renderers without tool metadata."""
171
+ for key in _TOOL_SUMMARY_KEYS:
172
+ value = args.get(key)
173
+ if isinstance(value, (str, int, float)) and str(value).strip():
174
+ text = " ".join(str(value).split())
175
+ return f"{name} {text[:60]}"
176
+ return str(name)
177
+
178
+
179
+ def step_message(payload: dict[str, Any]) -> str:
180
+ """Human-readable one-liner for a step payload (used by non-CLI renderers)."""
181
+ step = payload.get("step")
182
+ steps = payload.get("steps")
183
+ todo = str(payload.get("todo") or "").strip()
184
+ head = f"{step}/{steps}" if step and steps else "step"
185
+ if todo:
186
+ return f"Step {head} · {todo}"
187
+ if payload.get("status") == "completed":
188
+ return f"Steps complete · {head}"
189
+ return f"Step {head}"
190
+
36
191
 
37
192
  def resolve_interaction_mode(metadata: dict[str, Any] | None) -> str:
38
193
  """Return ``agent`` or ``ask`` from request metadata (default ``agent``)."""
@@ -114,6 +269,8 @@ async def iter_nano_runtime_events(
114
269
  open_args: dict[str, dict[str, Any]] = {}
115
270
  tool_args = ToolCallArgAccumulator()
116
271
  emitted_calls: set[str] = set()
272
+ refreshed_calls: set[str] = set()
273
+ last_step_key = ""
117
274
 
118
275
  try:
119
276
  async for chunk in agent.astream(
@@ -132,8 +289,23 @@ async def iter_nano_runtime_events(
132
289
  yield mapped
133
290
  continue
134
291
 
135
- if mode == "updates" and isinstance(data, dict) and "__interrupt__" in data:
136
- yield InterruptWaiting(message="Waiting for input…")
292
+ if mode == "updates" and isinstance(data, dict):
293
+ if "__interrupt__" in data:
294
+ yield InterruptWaiting(message="Waiting for input…")
295
+ continue
296
+ # Plan steps (todos) and host middleware phases are the only
297
+ # signal soothe gives us about *what* it is doing between tool
298
+ # calls; surface each change once.
299
+ step = update_progress(data)
300
+ if step is not None:
301
+ key, payload = step
302
+ if key != last_step_key:
303
+ last_step_key = key
304
+ yield Progress(
305
+ stage="Step",
306
+ message=step_message(payload),
307
+ raw=payload,
308
+ )
137
309
  continue
138
310
 
139
311
  if mode != "messages":
@@ -153,6 +325,22 @@ async def iter_nano_runtime_events(
153
325
  if tc_id in emitted_calls:
154
326
  # Later updates refresh args; the call already started.
155
327
  open_args[tc_id] = args
328
+ # Tool args stream as partial JSON, so ToolStarted
329
+ # usually carries none — refresh the status line
330
+ # once they parse (Writing b1.txt, not Writing).
331
+ # Once per call: partial JSON grows on every token,
332
+ # and re-emitting it would flood the event stream.
333
+ if args and name not in _QUIET_TOOLS and tc_id not in refreshed_calls:
334
+ refreshed_calls.add(tc_id)
335
+ yield Progress(
336
+ stage="Tool",
337
+ message=tool_call_summary(str(name), args),
338
+ raw={
339
+ "type": "soothe.tool.started",
340
+ "tool": name,
341
+ "args": args,
342
+ },
343
+ )
156
344
  continue
157
345
  emitted_calls.add(tc_id)
158
346
  open_tools[tc_id] = str(name)
@@ -197,6 +385,9 @@ async def iter_nano_runtime_events(
197
385
  err_detail = tool_result_error_detail(message_obj.content)
198
386
  ok = status != "error" and err_detail is None
199
387
  detail = simplify_tool_error(err_detail) if err_detail else None
388
+ if str(name) in _QUIET_TOOLS and ok:
389
+ # Plan bookkeeping already surfaced as a step event.
390
+ continue
200
391
  yield ToolCompleted(
201
392
  tool=str(name),
202
393
  ok=ok,