flowjet 2.0.1__tar.gz → 2.0.2__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.1 → flowjet-2.0.2}/CHANGELOG.md +18 -0
  2. {flowjet-2.0.1 → flowjet-2.0.2}/PKG-INFO +16 -1
  3. {flowjet-2.0.1 → flowjet-2.0.2}/README.md +14 -0
  4. {flowjet-2.0.1 → flowjet-2.0.2}/pyproject.toml +4 -1
  5. flowjet-2.0.2/src/flowjet/core/backend.py +107 -0
  6. flowjet-2.0.2/src/flowjet/core/bootstrap.py +310 -0
  7. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/adapter.py +7 -6
  8. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/identity.py +66 -0
  9. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/modes.py +24 -7
  10. flowjet-2.0.1/src/flowjet/core/bootstrap.py +0 -183
  11. {flowjet-2.0.1 → flowjet-2.0.2}/.gitignore +0 -0
  12. {flowjet-2.0.1 → flowjet-2.0.2}/LICENSE +0 -0
  13. {flowjet-2.0.1 → flowjet-2.0.2}/config/nano.yml +0 -0
  14. {flowjet-2.0.1 → flowjet-2.0.2}/deploy/README.md +0 -0
  15. {flowjet-2.0.1 → flowjet-2.0.2}/deploy/nano.yml +0 -0
  16. {flowjet-2.0.1 → flowjet-2.0.2}/docs/completion-design.md +0 -0
  17. {flowjet-2.0.1 → flowjet-2.0.2}/docs/drafts/README.md +0 -0
  18. {flowjet-2.0.1 → flowjet-2.0.2}/docs/drafts/api-spec.md +0 -0
  19. {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/IG-001-openai-compatible-server.md +0 -0
  20. {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/IG-002-isolated-thread-pool-runtime.md +0 -0
  21. {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
  22. {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/README.md +0 -0
  23. {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/TEST-COVERAGE-MATRIX.md +0 -0
  24. {flowjet-2.0.1 → flowjet-2.0.2}/docs/naming.md +0 -0
  25. {flowjet-2.0.1 → flowjet-2.0.2}/docs/server.md +0 -0
  26. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
  27. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
  28. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
  29. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-004-unified-package.md +0 -0
  30. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/rfc-history.md +0 -0
  31. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/rfc-index.md +0 -0
  32. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/rfc-namings.md +0 -0
  33. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/architecture-design.md +0 -0
  34. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/conceptual-design.md +0 -0
  35. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/impl-interface-design.md +0 -0
  36. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/rfc-standard.md +0 -0
  37. {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/rfc-template.md +0 -0
  38. {flowjet-2.0.1 → flowjet-2.0.2}/docs/upgrade.md +0 -0
  39. {flowjet-2.0.1 → flowjet-2.0.2}/examples/README.md +0 -0
  40. {flowjet-2.0.1 → flowjet-2.0.2}/nano.yml +0 -0
  41. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/__init__.py +0 -0
  42. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/__main__.py +0 -0
  43. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/README.md +0 -0
  44. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
  45. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
  46. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
  47. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
  48. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
  49. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
  50. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
  51. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
  52. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
  53. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
  54. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
  55. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
  56. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
  57. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
  58. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
  59. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
  60. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
  61. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
  62. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
  63. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
  64. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
  65. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
  66. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
  67. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
  68. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
  69. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
  70. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
  71. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
  72. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
  73. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
  74. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
  75. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
  76. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
  77. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
  78. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
  79. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
  80. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
  81. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
  82. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
  83. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
  84. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
  85. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
  86. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
  87. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
  88. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
  89. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
  90. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
  91. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
  92. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
  93. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
  94. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
  95. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
  96. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
  97. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
  98. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
  99. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
  100. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
  101. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
  102. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
  103. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
  104. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
  105. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
  106. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
  107. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
  108. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
  109. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
  110. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
  111. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
  112. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
  113. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
  114. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
  115. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
  116. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/__init__.py +0 -0
  117. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/__main__.py +0 -0
  118. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/agent.py +0 -0
  119. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/cli.py +0 -0
  120. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/completion/__init__.py +0 -0
  121. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/completion/context.py +0 -0
  122. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/completion/engine.py +0 -0
  123. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/doctor_cmd.py +0 -0
  124. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
  125. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/progress.py +0 -0
  126. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/serve_cmd.py +0 -0
  127. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/setup_cmd.py +0 -0
  128. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/shell/__init__.py +0 -0
  129. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/shell/_fj.zsh +0 -0
  130. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/shell/fj.bash +0 -0
  131. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/stream.py +0 -0
  132. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/threads.py +0 -0
  133. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/tool_stream.py +0 -0
  134. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/__init__.py +0 -0
  135. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/__init__.py +0 -0
  136. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/direct.py +0 -0
  137. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/fake.py +0 -0
  138. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/__init__.py +0 -0
  139. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/adapter.py +0 -0
  140. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/admission.py +0 -0
  141. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/backend.py +0 -0
  142. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/errors.py +0 -0
  143. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/mode_gate.py +0 -0
  144. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/pool.py +0 -0
  145. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/request.py +0 -0
  146. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/workspace.py +0 -0
  147. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/__init__.py +0 -0
  148. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/backend.py +0 -0
  149. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/mapping.py +0 -0
  150. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/events.py +0 -0
  151. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/protocol.py +0 -0
  152. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/tool_results.py +0 -0
  153. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/tool_stream.py +0 -0
  154. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/__init__.py +0 -0
  155. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/__main__.py +0 -0
  156. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/acp/__init__.py +0 -0
  157. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/acp/agent.py +0 -0
  158. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/acp/routes.py +0 -0
  159. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/config.py +0 -0
  160. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/http/__init__.py +0 -0
  161. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/http/app.py +0 -0
  162. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/http/auth.py +0 -0
  163. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/__init__.py +0 -0
  164. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/errors.py +0 -0
  165. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/projection.py +0 -0
  166. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/routes.py +0 -0
  167. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/schemas.py +0 -0
  168. {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/service.py +0 -0
  169. {flowjet-2.0.1 → flowjet-2.0.2}/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.2] — 2026-09-18
14
+
15
+ ### Added
16
+
17
+ - **Duo backends — `soothe` or `soothe-nano`.** `FLOWJET_BACKEND` (CLI, default
18
+ `soothe`) and `FLOWJET_SERVER_BACKEND` (HTTP/ACP, default `nano`) select which
19
+ runtime builds the agent. `soothe` is a host layer over soothe-nano, so the
20
+ engine stays installed either way; the two surfaces switch independently
21
+ because the server needs a dual-mode agent (AGENT + ASK graphs per request),
22
+ which soothe does not expose — with `FLOWJET_SERVER_BACKEND=soothe` the server
23
+ builds one host agent per mode and routes on `interaction_mode`.
24
+
25
+ ### Changed
26
+
27
+ - The CLI now builds its agent through the `soothe` host runtime by default
28
+ (host middleware, `request_plan_mode` / `ask_user` tools, configurable
29
+ persona). Set `FLOWJET_BACKEND=nano` for the previous behaviour.
30
+
13
31
  ## [2.0.1] — 2026-09-17
14
32
 
15
33
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flowjet
3
- Version: 2.0.1
3
+ Version: 2.0.2
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,6 +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
27
  Provides-Extra: dev
27
28
  Requires-Dist: httpx>=0.27.0; extra == 'dev'
28
29
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -78,6 +79,20 @@ fj summarize README.md
78
79
 
79
80
  Not sure your machine is ready? `fj doctor` (add `--deep`, `--live-llm`).
80
81
 
82
+ ### Backends
83
+
84
+ Two runtimes can build the agent; they are switched independently per surface:
85
+
86
+ | Env var | Surface | Default | Values |
87
+ |---------|---------|---------|--------|
88
+ | `FLOWJET_BACKEND` | CLI (`fj`) | `soothe` | `soothe`, `nano` |
89
+ | `FLOWJET_SERVER_BACKEND` | `flowjet-server` / `fj serve` | `nano` | `soothe`, `nano` |
90
+
91
+ `soothe` is the host runtime (extra middleware and tools over the same
92
+ soothe-nano engine). The server defaults to `nano` because it needs a dual-mode
93
+ agent — AGENT and ASK graphs selected per request — which `soothe` does not
94
+ expose yet; on `soothe` the server builds one host agent per mode instead.
95
+
81
96
  ---
82
97
 
83
98
  ## CLI
@@ -35,6 +35,20 @@ fj summarize README.md
35
35
 
36
36
  Not sure your machine is ready? `fj doctor` (add `--deep`, `--live-llm`).
37
37
 
38
+ ### Backends
39
+
40
+ Two runtimes can build the agent; they are switched independently per surface:
41
+
42
+ | Env var | Surface | Default | Values |
43
+ |---------|---------|---------|--------|
44
+ | `FLOWJET_BACKEND` | CLI (`fj`) | `soothe` | `soothe`, `nano` |
45
+ | `FLOWJET_SERVER_BACKEND` | `flowjet-server` / `fj serve` | `nano` | `soothe`, `nano` |
46
+
47
+ `soothe` is the host runtime (extra middleware and tools over the same
48
+ soothe-nano engine). The server defaults to `nano` because it needs a dual-mode
49
+ agent — AGENT and ASK graphs selected per request — which `soothe` does not
50
+ expose yet; on `soothe` the server builds one host agent per mode instead.
51
+
38
52
  ---
39
53
 
40
54
  ## CLI
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flowjet"
7
- version = "2.0.1"
7
+ version = "2.0.2"
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" }
@@ -29,6 +29,9 @@ classifiers = [
29
29
  # lives in the `server` extra, and `flowjet.cli` never imports it at module
30
30
  # scope (see tests/unit/test_cli_no_server_deps.py).
31
31
  dependencies = [
32
+ # Default CLI runtime (FLOWJET_BACKEND=soothe). soothe is a host layer over
33
+ # soothe-nano, so nano stays pinned explicitly: soothe only caps it at <2.
34
+ "soothe>=1.0.12,<2.0.0",
32
35
  "soothe-nano>=1.2.28,<1.3.0",
33
36
  ]
34
37
 
@@ -0,0 +1,107 @@
1
+ """Backend selection — which runtime builds FlowJet's agent.
2
+
3
+ FlowJet can build its agent two ways:
4
+
5
+ ``soothe``
6
+ The host runtime (``soothe.coreagent.create_soothe_agent``). ``soothe`` is a
7
+ layer **on top of** soothe-nano, not a replacement for it: it injects host
8
+ middleware (goal/step guard, decompose, eval, ask-user), host tools
9
+ (``request_plan_mode``, ``ask_user``) and a configurable persona over the
10
+ same nano agent graph.
11
+
12
+ ``nano``
13
+ soothe-nano directly (``create_nano_agent`` for the CLI,
14
+ ``create_dual_mode_nano_agent`` for the server).
15
+
16
+ The two surfaces switch **independently**, because they do not share the same
17
+ constraints: the CLI builds one agent per invocation, while the server needs a
18
+ dual-mode agent (AGENT + ASK graphs selected per request) — an API soothe does
19
+ not expose today. Hence the defaults differ:
20
+
21
+ * ``FLOWJET_BACKEND`` (CLI) — default ``soothe``.
22
+ * ``FLOWJET_SERVER_BACKEND`` (HTTP/ACP) — default ``nano``.
23
+
24
+ Both accept ``soothe`` or ``nano`` (case-insensitive); anything else is a hard
25
+ error rather than a silent fallback, so a typo can never quietly run the wrong
26
+ runtime.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import os
32
+ from enum import StrEnum
33
+
34
+ from flowjet.core.modes import Surface
35
+
36
+ CLI_BACKEND_ENV = "FLOWJET_BACKEND"
37
+ SERVER_BACKEND_ENV = "FLOWJET_SERVER_BACKEND"
38
+
39
+
40
+ class Backend(StrEnum):
41
+ """Runtime that builds the agent."""
42
+
43
+ SOOTHE = "soothe"
44
+ NANO = "nano"
45
+
46
+
47
+ #: CLI builds one agent per invocation; the host runtime is the default there.
48
+ DEFAULT_CLI_BACKEND = Backend.SOOTHE
49
+
50
+ #: The server needs per-request mode switching (dual-mode agent), which soothe
51
+ #: does not expose; it stays on nano until host dual-mode exists.
52
+ DEFAULT_SERVER_BACKEND = Backend.NANO
53
+
54
+ _BACKENDS: dict[str, Backend] = {b.value: b for b in Backend}
55
+
56
+
57
+ def parse_backend(value: str | None, *, default: Backend, env_name: str) -> Backend:
58
+ """Resolve one backend value (env string → :class:`Backend`).
59
+
60
+ ``None`` and empty/whitespace values fall back to ``default``; an unknown
61
+ value raises ``ValueError`` naming the offending variable.
62
+ """
63
+ if value is None:
64
+ return default
65
+ token = value.strip().lower()
66
+ if not token:
67
+ return default
68
+ try:
69
+ return _BACKENDS[token]
70
+ except KeyError:
71
+ raise ValueError(
72
+ f"{env_name}={value!r} is not a valid backend "
73
+ f"(expected one of: {', '.join(sorted(_BACKENDS))})"
74
+ ) from None
75
+
76
+
77
+ def resolve_backend(surface: Surface | str) -> Backend:
78
+ """Return the backend configured for ``surface`` (read from the env)."""
79
+ resolved = Surface(str(surface))
80
+ if resolved is Surface.SERVER:
81
+ return parse_backend(
82
+ os.environ.get(SERVER_BACKEND_ENV),
83
+ default=DEFAULT_SERVER_BACKEND,
84
+ env_name=SERVER_BACKEND_ENV,
85
+ )
86
+ return parse_backend(
87
+ os.environ.get(CLI_BACKEND_ENV),
88
+ default=DEFAULT_CLI_BACKEND,
89
+ env_name=CLI_BACKEND_ENV,
90
+ )
91
+
92
+
93
+ def backend_for_profile(profile: object) -> Backend:
94
+ """Return the backend for a :class:`~flowjet.core.modes.SurfaceProfile`."""
95
+ return resolve_backend(getattr(profile, "surface", Surface.CLI))
96
+
97
+
98
+ __all__ = [
99
+ "CLI_BACKEND_ENV",
100
+ "DEFAULT_CLI_BACKEND",
101
+ "DEFAULT_SERVER_BACKEND",
102
+ "SERVER_BACKEND_ENV",
103
+ "Backend",
104
+ "backend_for_profile",
105
+ "parse_backend",
106
+ "resolve_backend",
107
+ ]
@@ -0,0 +1,310 @@
1
+ """Single bootstrap shared by the CLI and the server.
2
+
3
+ Both surfaces used to build their agent separately (``fj_ai.agent.build_agent``
4
+ and ``flowjet_server.bridges.nano.adapter.create_nano_agent_instance``), with
5
+ different constructors and contradictory security defaults. This module is the
6
+ one place where a ``SootheConfig`` is loaded, profiled and turned into an agent.
7
+
8
+ Which runtime builds that agent is decided by :mod:`flowjet.core.backend`
9
+ (``soothe`` host runtime or ``soothe-nano``), per surface.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ from collections.abc import AsyncIterator
17
+ from contextlib import asynccontextmanager
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from soothe_nano.config import SOOTHE_HOME, SootheConfig
22
+ from soothe_nano.resolve import resolve_checkpointer
23
+
24
+ from flowjet.core.backend import Backend, resolve_backend
25
+ from flowjet.core.modes import (
26
+ CLI_PROFILE,
27
+ InteractionMode,
28
+ Surface,
29
+ SurfaceProfile,
30
+ require_mode,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Builtin skills
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _REGISTERED = False
40
+
41
+ # Skills belong to the distribution, not to either surface: both the CLI and
42
+ # the server expose the same FlowJet skill set.
43
+ BUILTIN_SKILLS_DIR = Path(__file__).resolve().parents[1] / "builtin_skills"
44
+
45
+ # FlowJet additions on top of soothe-nano's DEFAULT_CORE_SKILL_NAMES.
46
+ FJ_CORE_SKILL_NAMES: tuple[str, ...] = ()
47
+
48
+
49
+ def default_core_skill_names() -> list[str]:
50
+ """Nano default core skills plus FlowJet workflow skills."""
51
+ from soothe_nano.skills.registry import DEFAULT_CORE_SKILL_NAMES
52
+
53
+ return sorted(DEFAULT_CORE_SKILL_NAMES | {n.lower() for n in FJ_CORE_SKILL_NAMES})
54
+
55
+
56
+ def register_flowjet_builtin_skills() -> None:
57
+ """Register ``flowjet/builtin_skills`` as a soothe-nano builtin skill root.
58
+
59
+ Idempotent — safe to call from either surface and from pool worker threads.
60
+ """
61
+ global _REGISTERED
62
+ if _REGISTERED:
63
+ return
64
+ if not BUILTIN_SKILLS_DIR.is_dir():
65
+ return
66
+ from soothe_nano.skills import register_builtin_skill_root
67
+
68
+ register_builtin_skill_root(BUILTIN_SKILLS_DIR, source="builtin")
69
+ _REGISTERED = True
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Config
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ def default_config_path() -> Path:
78
+ """Return ``~/.soothe/config/nano.yml`` (respects ``SOOTHE_HOME``)."""
79
+ return SOOTHE_HOME / "config" / "nano.yml"
80
+
81
+
82
+ def load_config(
83
+ config_path: str | Path | None = None,
84
+ *,
85
+ backend: Backend | None = None,
86
+ ) -> Any:
87
+ """Load a ``SootheConfig`` for ``backend``, or bootstrap from env when missing.
88
+
89
+ The config class follows the backend: ``soothe`` loads the *host*
90
+ ``soothe.config.SootheConfig`` (a superset of nano's — the host agent reads
91
+ host-owned fields such as ``agent.loop`` / ``agent.clarification`` that nano's
92
+ config does not define), ``nano`` loads ``soothe_nano.config.SootheConfig``.
93
+
94
+ Resolution order:
95
+ 1. Explicit ``config_path``
96
+ 2. ``SOOTHE_HOME / config / nano.yml`` (default ``~/.soothe/config/nano.yml``)
97
+ 3. Zero-config ``SootheConfig()`` from ``OPENAI_API_KEY`` / ``ANTHROPIC_API_KEY``
98
+
99
+ On the ``soothe`` backend, a sibling ``soothe.yml`` (host-owned keys) is
100
+ composed over ``nano.yml`` when present.
101
+ """
102
+ backend = backend or resolve_backend(Surface.CLI)
103
+ path = Path(config_path).expanduser() if config_path else default_config_path()
104
+ if path.is_file():
105
+ return _load_config_file(path, backend)
106
+ if config_path is not None:
107
+ raise FileNotFoundError(f"Config not found: {path}")
108
+ return default_config(backend)
109
+
110
+
111
+ def default_config(backend: Backend) -> Any:
112
+ """Zero-config ``SootheConfig`` for ``backend`` (env-derived providers)."""
113
+ if backend is Backend.SOOTHE:
114
+ from soothe.config import SootheConfig as HostSootheConfig
115
+
116
+ return HostSootheConfig()
117
+ return SootheConfig()
118
+
119
+
120
+ def _load_config_file(path: Path, backend: Backend) -> Any:
121
+ """Parse ``path`` into the config class owned by ``backend``."""
122
+ if backend is Backend.SOOTHE:
123
+ from soothe.config import SootheConfig as HostSootheConfig
124
+
125
+ host_path = path.with_name("soothe.yml")
126
+ if host_path.is_file():
127
+ return HostSootheConfig.from_split_yaml_files(
128
+ nano_path=str(path), soothe_path=str(host_path)
129
+ )
130
+ return HostSootheConfig.from_yaml_file(str(path))
131
+ return SootheConfig.from_yaml_file(str(path))
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Workspace / checkpointer
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ def ensure_workspace(workspace: Path | None = None) -> Path:
140
+ """Use ``workspace`` or cwd as ``SOOTHE_WORKSPACE`` for file/shell tools."""
141
+ root = (workspace or Path.cwd()).resolve()
142
+ os.environ.setdefault("SOOTHE_WORKSPACE", str(root))
143
+ try:
144
+ from soothe_nano.workspace import FrameworkFilesystem
145
+
146
+ FrameworkFilesystem.set_current_workspace(root)
147
+ except Exception: # pragma: no cover - optional API surface
148
+ logger.debug("Could not set FrameworkFilesystem workspace", exc_info=True)
149
+ return root
150
+
151
+
152
+ @asynccontextmanager
153
+ async def open_sqlite_checkpointer(config: Any) -> AsyncIterator[Any | None]:
154
+ """Yield an ``AsyncSqliteSaver`` using nano's default sqlite data path.
155
+
156
+ Path: ``$SOOTHE_DATA_DIR/soothe_checkpoints.db`` (default ``~/.soothe/data/``).
157
+ """
158
+ sqlite_cfg = config.model_copy(
159
+ update={
160
+ "agent": config.agent.model_copy(
161
+ update={
162
+ "protocols": config.agent.protocols.model_copy(
163
+ update={
164
+ "durability": config.agent.protocols.durability.model_copy(
165
+ update={"backend": "sqlite", "checkpointer": "sqlite"}
166
+ )
167
+ }
168
+ )
169
+ }
170
+ )
171
+ }
172
+ )
173
+ result = resolve_checkpointer(sqlite_cfg)
174
+ db_path: str | None = None
175
+ if isinstance(result, tuple):
176
+ _placeholder, pool_or_path = result
177
+ if isinstance(pool_or_path, str):
178
+ db_path = pool_or_path
179
+
180
+ if not db_path:
181
+ logger.warning("SQLite checkpointer path unresolved; running without persistence")
182
+ yield None
183
+ return
184
+
185
+ import aiosqlite
186
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
187
+ from soothe_sdk.utils.serde import create_soothe_serde
188
+
189
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
190
+ conn = await aiosqlite.connect(db_path)
191
+ checkpointer = AsyncSqliteSaver(conn, serde=create_soothe_serde())
192
+ await checkpointer.setup()
193
+ logger.debug("SQLite checkpointer ready at %s", db_path)
194
+ try:
195
+ yield checkpointer
196
+ finally:
197
+ await conn.close()
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Agent construction
202
+ # ---------------------------------------------------------------------------
203
+
204
+
205
+ def create_agent(
206
+ config: Any,
207
+ profile: SurfaceProfile = CLI_PROFILE,
208
+ *,
209
+ interaction_mode: InteractionMode | str | None = None,
210
+ backend: Backend | None = None,
211
+ ) -> Any:
212
+ """Build an agent for ``profile``'s surface on the selected backend.
213
+
214
+ The backend comes from ``FLOWJET_BACKEND`` (CLI) / ``FLOWJET_SERVER_BACKEND``
215
+ (server) unless ``backend`` is passed explicitly — see
216
+ :mod:`flowjet.core.backend`.
217
+
218
+ The server uses a dual-mode agent and supplies the interaction mode per
219
+ request through the LangGraph ``configurable`` dict; the CLI resolves the
220
+ mode up front and passes it to the single-mode constructor.
221
+ """
222
+ from flowjet.core.modes import apply_profile
223
+
224
+ mode = require_mode(interaction_mode, profile)
225
+ resolved = backend or resolve_backend(profile.surface)
226
+ cfg = apply_profile(config, profile, backend=resolved)
227
+
228
+ if resolved is Backend.SOOTHE:
229
+ return create_soothe_agent(cfg, profile, mode)
230
+
231
+ if profile.surface is Surface.SERVER:
232
+ from soothe_nano import create_dual_mode_nano_agent
233
+
234
+ return create_dual_mode_nano_agent(cfg)
235
+
236
+ from soothe_nano import create_nano_agent
237
+
238
+ return create_nano_agent(cfg, interaction_mode=mode)
239
+
240
+
241
+ def soothe_agent_factory() -> Any:
242
+ """Return ``soothe.coreagent.create_soothe_agent`` (imported lazily).
243
+
244
+ Kept as a seam so the host runtime is imported only when it is actually
245
+ selected — the ``nano`` backend never pays for it, and tests patch this
246
+ instead of building a real host agent.
247
+ """
248
+ from soothe.coreagent import create_soothe_agent
249
+
250
+ return create_soothe_agent
251
+
252
+
253
+ def create_soothe_agent(
254
+ config: Any,
255
+ profile: SurfaceProfile = CLI_PROFILE,
256
+ mode: str | None = None,
257
+ ) -> Any:
258
+ """Build a ``soothe`` (host runtime) agent for ``profile``'s surface.
259
+
260
+ ``interrupt_on`` is pinned to ``{}``: the host builder otherwise injects
261
+ HITL interrupts for mutating tools (``edit_file`` / ``write_file`` /
262
+ ``delete`` / ``run_command``), and FlowJet has no resume path — it renders
263
+ ``InterruptWaiting`` and stops, so any write would stall the run.
264
+ """
265
+ factory = soothe_agent_factory()
266
+ if profile.surface is Surface.SERVER:
267
+ return ModeRoutedSootheAgent(config, factory)
268
+ return factory(config, interaction_mode=mode, interrupt_on={})
269
+
270
+
271
+ class ModeRoutedSootheAgent:
272
+ """Route one server request to the host agent built for its mode.
273
+
274
+ soothe has no dual-mode API (nano's ``create_dual_mode_nano_agent`` has no
275
+ host equivalent), so the server surface keeps one host agent per allowed
276
+ mode and picks it from ``configurable["interaction_mode"]`` — an ``ask``
277
+ request must never land on the ``agent`` graph. Agents are built lazily, so
278
+ a worker that only serves ``ask`` never builds the ``agent`` graph.
279
+
280
+ Only ``astream`` is proxied: that is the whole surface the nano bridge
281
+ (``flowjet.core.bridges.nano.mapping``) drives.
282
+ """
283
+
284
+ def __init__(
285
+ self,
286
+ config: Any,
287
+ factory: Any,
288
+ modes: tuple[str, ...] = ("agent", "ask"),
289
+ ) -> None:
290
+ self._config = config
291
+ self._factory = factory
292
+ self._modes = modes
293
+ self._agents: dict[str, Any] = {}
294
+
295
+ def _agent_for(self, mode: str) -> Any:
296
+ resolved = mode if mode in self._modes else "agent"
297
+ agent = self._agents.get(resolved)
298
+ if agent is None:
299
+ agent = self._factory(self._config, interaction_mode=resolved, interrupt_on={})
300
+ self._agents[resolved] = agent
301
+ return agent
302
+
303
+ async def astream(self, payload: Any, **kwargs: Any) -> Any:
304
+ """Proxy ``astream`` to the agent built for the request's mode."""
305
+ config = kwargs.get("config") or {}
306
+ configurable = config.get("configurable") or {} if isinstance(config, dict) else {}
307
+ mode = str(configurable.get("interaction_mode") or "agent")
308
+ agent = self._agent_for(mode)
309
+ async for chunk in agent.astream(payload, **kwargs):
310
+ yield chunk
@@ -22,15 +22,16 @@ def create_nano_agent_instance(config_path: str | Path | None = None) -> Any:
22
22
  :data:`flowjet.core.modes.SERVER_PROFILE`; they are applied by the shared
23
23
  bootstrap rather than re-implemented here.
24
24
  """
25
- from flowjet.core.bootstrap import create_agent, load_config
25
+ from flowjet.core.backend import resolve_backend
26
+ from flowjet.core.bootstrap import create_agent, default_config, load_config
27
+ from flowjet.core.modes import Surface
26
28
 
29
+ backend = resolve_backend(Surface.SERVER)
27
30
  try:
28
- config = load_config(config_path)
31
+ config = load_config(config_path, backend=backend)
29
32
  except FileNotFoundError:
30
- from soothe_nano.config import SootheConfig
31
-
32
- config = SootheConfig()
33
- return create_agent(config, SERVER_PROFILE)
33
+ config = default_config(backend)
34
+ return create_agent(config, SERVER_PROFILE, backend=backend)
34
35
 
35
36
 
36
37
  class NanoAgentAdapter:
@@ -15,12 +15,29 @@ so this is the single place the persona is decided — a custom
15
15
  from __future__ import annotations
16
16
 
17
17
  import logging
18
+ from typing import Any
18
19
 
19
20
  logger = logging.getLogger(__name__)
20
21
 
21
22
  #: Assistant name injected as ``{assistant_name}`` when config does not set one.
22
23
  FLOWJET_ASSISTANT_NAME = "FlowJet"
23
24
 
25
+ #: Persona fields for the ``soothe`` backend, which owns the identity block
26
+ #: through host config (``agent.assistant_identity``) instead of the override
27
+ #: below. Values mirror :data:`FLOWJET_IDENTITY_FRAGMENT`.
28
+ FLOWJET_CREATOR = "Dr. Xiaming Chen"
29
+ FLOWJET_ROLE_DESCRIPTION = "a coding agent"
30
+ FLOWJET_VENDOR_DENYLIST: tuple[str, ...] = (
31
+ "Soothe",
32
+ "soothe-nano",
33
+ "Claude",
34
+ "ChatGPT",
35
+ "Gemini",
36
+ "Anthropic",
37
+ "OpenAI",
38
+ "Google",
39
+ )
40
+
24
41
  #: Replacement for ``soothe_nano``'s ``assistant_identity.xml``. Keeps the
25
42
  #: ``{assistant_name}`` placeholder so an explicit ``agent.name`` still renames
26
43
  #: the assistant; the anti-impersonation line is extended to cover nano itself.
@@ -67,8 +84,57 @@ def install_flowjet_identity() -> bool:
67
84
  return True
68
85
 
69
86
 
87
+ def configure_soothe_identity(config: Any) -> Any:
88
+ """Own the persona through soothe's host config, and return the config.
89
+
90
+ On the ``soothe`` backend two writers would otherwise race for nano's
91
+ ``ASSISTANT_IDENTITY_FRAGMENT``: this module's override and
92
+ ``soothe.identity.persona.apply_identity_fragment_override`` — whichever ran
93
+ last won. Configuring ``agent.assistant_identity`` and letting soothe render
94
+ the fragment makes soothe the single writer, and keeps the FlowJet persona
95
+ (creator, role, vendor denylist) declarative.
96
+
97
+ Returns ``config`` unchanged when it is not a host config (no
98
+ ``agent.assistant_identity`` field) or when soothe is unavailable; the
99
+ :func:`install_flowjet_identity` override then stays in force.
100
+ """
101
+ agent_cfg = getattr(config, "agent", None)
102
+ if agent_cfg is None or "assistant_identity" not in type(agent_cfg).model_fields:
103
+ return config
104
+
105
+ try:
106
+ from soothe.config.models import AssistantIdentity
107
+ from soothe.identity.persona import apply_identity_fragment_override
108
+ except ImportError: # pragma: no cover - host runtime missing/too old
109
+ logger.warning("soothe identity API unavailable; FlowJet keeps the nano fragment override")
110
+ return config
111
+
112
+ # An explicit persona in the user's config wins, same as ``agent.name``.
113
+ if "assistant_identity" not in getattr(agent_cfg, "model_fields_set", frozenset()):
114
+ config = config.model_copy(
115
+ update={
116
+ "agent": agent_cfg.model_copy(
117
+ update={
118
+ "assistant_identity": AssistantIdentity(
119
+ creator=FLOWJET_CREATOR,
120
+ role_description=FLOWJET_ROLE_DESCRIPTION,
121
+ vendor_denylist=list(FLOWJET_VENDOR_DENYLIST),
122
+ )
123
+ }
124
+ )
125
+ }
126
+ )
127
+
128
+ apply_identity_fragment_override(config)
129
+ return config
130
+
131
+
70
132
  __all__ = [
71
133
  "FLOWJET_ASSISTANT_NAME",
134
+ "FLOWJET_CREATOR",
72
135
  "FLOWJET_IDENTITY_FRAGMENT",
136
+ "FLOWJET_ROLE_DESCRIPTION",
137
+ "FLOWJET_VENDOR_DENYLIST",
138
+ "configure_soothe_identity",
73
139
  "install_flowjet_identity",
74
140
  ]
@@ -17,10 +17,7 @@ from __future__ import annotations
17
17
 
18
18
  from dataclasses import dataclass
19
19
  from enum import StrEnum
20
- from typing import TYPE_CHECKING, Any
21
-
22
- if TYPE_CHECKING: # pragma: no cover
23
- from soothe_nano.config import SootheConfig
20
+ from typing import Any
24
21
 
25
22
 
26
23
  class Surface(StrEnum):
@@ -85,19 +82,39 @@ def require_mode(mode: InteractionMode | str | None, profile: SurfaceProfile) ->
85
82
  return resolved.value
86
83
 
87
84
 
88
- def apply_profile(config: SootheConfig, profile: SurfaceProfile) -> SootheConfig:
89
- """Return a copy of ``config`` with ``profile``'s invariants applied."""
85
+ def apply_profile(
86
+ config: Any,
87
+ profile: SurfaceProfile,
88
+ *,
89
+ backend: Any | None = None,
90
+ ) -> Any:
91
+ """Return a copy of ``config`` with ``profile``'s invariants applied.
92
+
93
+ ``backend`` is a :class:`~flowjet.core.backend.Backend` (resolved from the
94
+ environment when omitted). It only decides *how* the persona is owned: on
95
+ ``soothe`` the host config's ``agent.assistant_identity`` becomes the single
96
+ source of truth instead of FlowJet's fragment override.
97
+ """
98
+ from flowjet.core.backend import Backend, resolve_backend
90
99
  from flowjet.core.bootstrap import (
91
100
  default_core_skill_names,
92
101
  register_flowjet_builtin_skills,
93
102
  )
94
- from flowjet.core.identity import FLOWJET_ASSISTANT_NAME, install_flowjet_identity
103
+ from flowjet.core.identity import (
104
+ FLOWJET_ASSISTANT_NAME,
105
+ configure_soothe_identity,
106
+ install_flowjet_identity,
107
+ )
95
108
 
96
109
  if profile.register_builtin_skills:
97
110
  register_flowjet_builtin_skills()
98
111
 
99
112
  install_flowjet_identity()
100
113
 
114
+ resolved = backend or resolve_backend(profile.surface)
115
+ if resolved is Backend.SOOTHE:
116
+ config = configure_soothe_identity(config)
117
+
101
118
  updates: dict[str, Any] = {}
102
119
  agent_updates: dict[str, Any] = {}
103
120