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.
- {flowjet-2.0.1 → flowjet-2.0.2}/CHANGELOG.md +18 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/PKG-INFO +16 -1
- {flowjet-2.0.1 → flowjet-2.0.2}/README.md +14 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/pyproject.toml +4 -1
- flowjet-2.0.2/src/flowjet/core/backend.py +107 -0
- flowjet-2.0.2/src/flowjet/core/bootstrap.py +310 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/adapter.py +7 -6
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/identity.py +66 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/modes.py +24 -7
- flowjet-2.0.1/src/flowjet/core/bootstrap.py +0 -183
- {flowjet-2.0.1 → flowjet-2.0.2}/.gitignore +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/LICENSE +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/config/nano.yml +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/deploy/README.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/deploy/nano.yml +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/completion-design.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/drafts/README.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/drafts/api-spec.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/IG-001-openai-compatible-server.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/IG-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/README.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/impl/TEST-COVERAGE-MATRIX.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/naming.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/server.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/RFC-004-unified-package.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/rfc-history.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/rfc-index.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/rfc-namings.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/architecture-design.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/conceptual-design.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/impl-interface-design.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/rfc-standard.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/specs/templates/rfc-template.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/docs/upgrade.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/examples/README.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/nano.yml +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/__main__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/README.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/__main__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/agent.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/cli.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/completion/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/completion/context.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/completion/engine.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/doctor_cmd.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/progress.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/serve_cmd.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/setup_cmd.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/shell/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/shell/_fj.zsh +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/shell/fj.bash +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/stream.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/threads.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/cli/tool_stream.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/direct.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/fake.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/adapter.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/admission.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/backend.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/errors.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/mode_gate.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/pool.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/request.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/backends/isolation/workspace.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/backend.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/bridges/nano/mapping.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/events.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/protocol.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/tool_results.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/core/tool_stream.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/__main__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/acp/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/acp/agent.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/acp/routes.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/config.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/http/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/http/app.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/http/auth.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/__init__.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/errors.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/projection.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/routes.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/schemas.py +0 -0
- {flowjet-2.0.1 → flowjet-2.0.2}/src/flowjet/server/openai_compat/service.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
89
|
-
|
|
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
|
|
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
|
|