flowjet 2.1.3__tar.gz → 2.1.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.
- {flowjet-2.1.3 → flowjet-2.1.4}/CHANGELOG.md +22 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/PKG-INFO +1 -1
- {flowjet-2.1.3 → flowjet-2.1.4}/pyproject.toml +14 -1
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/agent.py +1 -3
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/cli.py +14 -31
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/doctor_cmd.py +3 -1
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/progress.py +1 -1
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/setup_cmd.py +1 -1
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/agent/factory.py +56 -12
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/agent/wiring.py +12 -3
- flowjet-2.1.4/src/flowjet/core/agent/workspace.py +157 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/base.py +90 -6
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/nano/backend.py +2 -0
- flowjet-2.1.4/src/flowjet/core/bridges/nano/interrupts.py +74 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/nano/mapping.py +22 -14
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/soothe/backend.py +2 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/backend.py +1 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/pool.py +1 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/request.py +5 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/workspace.py +7 -5
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/sandbox/resolver.py +1 -1
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/streaming/tool_stream.py +3 -3
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/acp/agent.py +21 -17
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/acp/routes.py +2 -2
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/config.py +8 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/http/app.py +1 -0
- flowjet-2.1.3/src/flowjet/core/agent/workspace.py +0 -89
- {flowjet-2.1.3 → flowjet-2.1.4}/.gitignore +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/LICENSE +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/README.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/config/nano-local.yml +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/config/nano-sandbox-soothe.yml +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/config/nano.yml +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/deploy/README.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/completion-design.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/impl/IG-001-openai-compatible-server.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/impl/IG-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/RFC-004-unified-package.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/RFC-005-arcbox-tool-sandbox.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/rfc-history.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/rfc-index.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/docs/specs/rfc-namings.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/examples/README.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/__main__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/README.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/__main__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/completion/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/completion/context.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/completion/engine.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/serve_cmd.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/shell/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/shell/_fj.zsh +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/shell/fj.bash +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/stream.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/threads.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/cli/tool_stream.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/_warnings.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/agent/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/agent/config.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/agent/skills.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/backend_config.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/nano/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/nano/adapter.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/soothe/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/bridges/soothe/adapter.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/events.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/identity.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/modes.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/protocol.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/direct.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/fake.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/adapter.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/admission.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/errors.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/mode_gate.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/sandbox/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/sandbox/arbox.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/sandbox/settings.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/streaming/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/streaming/presentation.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/core/streaming/tool_results.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/home.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/__main__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/acp/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/http/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/http/auth.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/openai_compat/__init__.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/openai_compat/errors.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/openai_compat/projection.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/openai_compat/routes.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/openai_compat/schemas.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/openai_compat/service.py +0 -0
- {flowjet-2.1.3 → flowjet-2.1.4}/src/flowjet/server/openai_compat/store.py +0 -0
|
@@ -10,6 +10,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [2.1.4] — 2026-10-23
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Resumable `ask_user` pauses via shared thread-state checkpointer.** The
|
|
18
|
+
server now persists agent thread state (LangGraph checkpoint) to a shared
|
|
19
|
+
SQLite database (WAL mode + busy_timeout) so that `ask_user` pauses survive
|
|
20
|
+
pool-worker switches and server restarts. A second turn on the same session
|
|
21
|
+
resumes the paused interrupt and delivers the user's input text as the
|
|
22
|
+
answer — no more re-asking the same question after a worker rotation. The
|
|
23
|
+
`server_checkpointer` setting (`"auto"` by default, `"off"` for legacy
|
|
24
|
+
stateless behavior) controls the policy end-to-end: Settings →
|
|
25
|
+
`ServerRuntimeBuilder` → `build_isolating_nano_backend` →
|
|
26
|
+
`AgentAdapterBase`, which opens the checkpointer once per worker, attaches
|
|
27
|
+
it at build time, and survives taint rebuilds.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **CI now enforces mypy type compliance.** A `make typecheck` (mypy) gate
|
|
32
|
+
was added to the CI pipeline, and type annotations were fixed across the
|
|
33
|
+
CLI, core, and server packages to achieve full compliance.
|
|
34
|
+
|
|
13
35
|
## [2.1.3] — 2026-10-23
|
|
14
36
|
|
|
15
37
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flowjet
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by Soothe
|
|
5
5
|
Project-URL: Homepage, https://github.com/caesar0301/flowjet
|
|
6
6
|
Project-URL: Repository, https://github.com/caesar0301/flowjet
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flowjet"
|
|
7
|
-
version = "2.1.
|
|
7
|
+
version = "2.1.4"
|
|
8
8
|
description = "FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by Soothe"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -102,6 +102,8 @@ python_version = "3.12"
|
|
|
102
102
|
warn_return_any = true
|
|
103
103
|
warn_unused_configs = true
|
|
104
104
|
disallow_untyped_defs = true
|
|
105
|
+
# Skill-local scripts ship their own (looser) tooling; excluded from package lint.
|
|
106
|
+
exclude = "src/flowjet/builtin_skills"
|
|
105
107
|
|
|
106
108
|
# Any-heavy moved code; loosened until annotated.
|
|
107
109
|
[[tool.mypy.overrides]]
|
|
@@ -109,6 +111,17 @@ module = ["flowjet.server.*", "flowjet.core.bridges.*"]
|
|
|
109
111
|
disallow_untyped_defs = false
|
|
110
112
|
warn_return_any = false
|
|
111
113
|
|
|
114
|
+
# Local runtimes and stub-less third-party packages consumed at runtime;
|
|
115
|
+
# typed via runtime contract, not via inline types.
|
|
116
|
+
[[tool.mypy.overrides]]
|
|
117
|
+
module = [
|
|
118
|
+
"soothe_nano.*",
|
|
119
|
+
"soothe.*",
|
|
120
|
+
"fsspec",
|
|
121
|
+
"yaml",
|
|
122
|
+
]
|
|
123
|
+
ignore_missing_imports = true
|
|
124
|
+
|
|
112
125
|
[tool.pytest.ini_options]
|
|
113
126
|
asyncio_mode = "auto"
|
|
114
127
|
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -108,11 +108,9 @@ async def build_agent(
|
|
|
108
108
|
mode = "bypass"
|
|
109
109
|
configure_cli_logging(verbose=verbose)
|
|
110
110
|
ensure_workspace(workspace)
|
|
111
|
-
agent = create_agent(config, CLI_PROFILE, interaction_mode=mode)
|
|
111
|
+
agent = create_agent(config, CLI_PROFILE, interaction_mode=mode, checkpointer=checkpointer)
|
|
112
112
|
# Plugin imports (e.g. browser_use) may still attach root console handlers.
|
|
113
113
|
silence_after_plugins(verbose=verbose)
|
|
114
|
-
if checkpointer is not None:
|
|
115
|
-
agent.graph.checkpointer = checkpointer
|
|
116
114
|
return agent
|
|
117
115
|
|
|
118
116
|
|
|
@@ -16,7 +16,7 @@ import traceback
|
|
|
16
16
|
from collections.abc import Coroutine
|
|
17
17
|
from logging.handlers import RotatingFileHandler
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Any
|
|
19
|
+
from typing import Any, cast
|
|
20
20
|
|
|
21
21
|
from flowjet.cli import __version__
|
|
22
22
|
|
|
@@ -441,7 +441,8 @@ def _build_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
|
441
441
|
"-c",
|
|
442
442
|
"--config",
|
|
443
443
|
metavar="PATH",
|
|
444
|
-
help
|
|
444
|
+
# Lazy help keeps the heavy default-config import off -V / normal runs.
|
|
445
|
+
help=cast("str", _LazyHelp(_default_config_help)),
|
|
445
446
|
)
|
|
446
447
|
paths.add_argument(
|
|
447
448
|
"-w",
|
|
@@ -468,7 +469,8 @@ def _build_setup_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
|
468
469
|
"-c",
|
|
469
470
|
"--config",
|
|
470
471
|
metavar="PATH",
|
|
471
|
-
help
|
|
472
|
+
# Lazy help keeps the heavy default-config import off -V / normal runs.
|
|
473
|
+
help=cast("str", _LazyHelp(_default_config_help)),
|
|
472
474
|
)
|
|
473
475
|
return parser
|
|
474
476
|
|
|
@@ -570,35 +572,16 @@ def run_pin_thread(thread_id: str) -> int:
|
|
|
570
572
|
async def _detect_pending_interrupt(agent: Any, thread_id: str) -> Any:
|
|
571
573
|
"""Return the pending ``ask_user`` interrupt value, or ``None``.
|
|
572
574
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
575
|
+
Thin wrapper over the shared bridge helper
|
|
576
|
+
(:func:`flowjet.core.bridges.nano.interrupts.detect_pending_ask_user`) so
|
|
577
|
+
the CLI and the server surfaces apply the exact same probe. When an
|
|
578
|
+
``ask_user`` interrupt is pending, its ``value`` carries the question
|
|
579
|
+
specs; the run resumes by feeding the user's answer to
|
|
580
|
+
``Command(resume=...)``.
|
|
576
581
|
"""
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
try:
|
|
581
|
-
snapshot = await get_state({"configurable": {"thread_id": thread_id}})
|
|
582
|
-
except Exception as exc:
|
|
583
|
-
# Not fatal — but explain why a pending question may be missed and a
|
|
584
|
-
# new turn started instead of resuming (visible with -v / FJ_LOG_FILE).
|
|
585
|
-
_logger.warning("thread snapshot check failed; starting a new turn: %s", exc)
|
|
586
|
-
return None
|
|
587
|
-
if snapshot is None:
|
|
588
|
-
return None
|
|
589
|
-
interrupts = getattr(snapshot, "interrupts", None) or ()
|
|
590
|
-
if not interrupts:
|
|
591
|
-
# Some snapshots nest interrupts under per-task state.
|
|
592
|
-
tasks = getattr(snapshot, "tasks", None) or ()
|
|
593
|
-
for task in tasks:
|
|
594
|
-
task_interrupts = getattr(task, "interrupts", None) or ()
|
|
595
|
-
interrupts = task_interrupts
|
|
596
|
-
break
|
|
597
|
-
for entry in interrupts:
|
|
598
|
-
value = getattr(entry, "value", None)
|
|
599
|
-
if isinstance(value, dict) and value.get("type") == "ask_user":
|
|
600
|
-
return value
|
|
601
|
-
return None
|
|
582
|
+
from flowjet.core.bridges.nano.interrupts import detect_pending_ask_user
|
|
583
|
+
|
|
584
|
+
return await detect_pending_ask_user(agent, {"configurable": {"thread_id": thread_id}})
|
|
602
585
|
|
|
603
586
|
|
|
604
587
|
async def run_async(args: argparse.Namespace) -> int:
|
|
@@ -153,7 +153,8 @@ async def _run_diagnose(
|
|
|
153
153
|
"soothe-nano diagnose API unavailable; upgrade soothe-nano "
|
|
154
154
|
"(fj doctor requires soothe-nano>=1.0.8)"
|
|
155
155
|
) from exc
|
|
156
|
-
|
|
156
|
+
result: list[dict[str, Any]] = await diagnose(config, deep=deep, live_llm=live_llm)
|
|
157
|
+
return result
|
|
157
158
|
|
|
158
159
|
|
|
159
160
|
def _bin_version(bin_path: str) -> str | None:
|
|
@@ -174,6 +175,7 @@ def _bin_version(bin_path: str) -> str | None:
|
|
|
174
175
|
|
|
175
176
|
def _find_chrome_executable() -> str | None:
|
|
176
177
|
"""Locate a Chrome/Chromium-family browser executable, or None."""
|
|
178
|
+
candidates: list[str]
|
|
177
179
|
if sys.platform == "darwin":
|
|
178
180
|
candidates = [
|
|
179
181
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -310,7 +310,7 @@ def _abbrev_path(path: str) -> str:
|
|
|
310
310
|
try:
|
|
311
311
|
from soothe_nano.utils.formatting import convert_and_abbreviate_path
|
|
312
312
|
|
|
313
|
-
return convert_and_abbreviate_path(path)
|
|
313
|
+
return str(convert_and_abbreviate_path(path))
|
|
314
314
|
except Exception:
|
|
315
315
|
home = os.path.expanduser("~")
|
|
316
316
|
if path.startswith(home + os.sep):
|
|
@@ -241,7 +241,7 @@ def choose_provider_interactive(providers: list[dict[str, Any]]) -> dict[str, An
|
|
|
241
241
|
|
|
242
242
|
def resolve_config_value(value: str, *, field: str) -> str:
|
|
243
243
|
"""Expand ``${ENV_VAR}`` placeholders; raise if any remain unresolved."""
|
|
244
|
-
resolved = _resolve_env(value)
|
|
244
|
+
resolved = str(_resolve_env(value))
|
|
245
245
|
match = _ENV_VAR_RE.search(resolved)
|
|
246
246
|
if match:
|
|
247
247
|
env_name = match.group(1)
|
|
@@ -41,6 +41,7 @@ class AgentFactory:
|
|
|
41
41
|
interaction_mode: InteractionMode | str | None = None,
|
|
42
42
|
backend: Backend | None = None,
|
|
43
43
|
sandbox: Any | None = None,
|
|
44
|
+
checkpointer: Any | None = None,
|
|
44
45
|
) -> Any:
|
|
45
46
|
from flowjet.core.modes import apply_profile
|
|
46
47
|
|
|
@@ -49,16 +50,18 @@ class AgentFactory:
|
|
|
49
50
|
cfg = apply_profile(config, profile, backend=resolved)
|
|
50
51
|
|
|
51
52
|
if resolved is Backend.SOOTHE:
|
|
52
|
-
return cls.create_soothe(cfg, profile, mode, sandbox=sandbox)
|
|
53
|
+
return cls.create_soothe(cfg, profile, mode, sandbox=sandbox, checkpointer=checkpointer)
|
|
53
54
|
|
|
54
55
|
if profile.surface is Surface.SERVER:
|
|
55
56
|
from soothe_nano import create_dual_mode_nano_agent
|
|
56
57
|
|
|
57
|
-
return create_dual_mode_nano_agent(cfg, backend=sandbox)
|
|
58
|
+
return create_dual_mode_nano_agent(cfg, backend=sandbox, checkpointer=checkpointer)
|
|
58
59
|
|
|
59
60
|
from soothe_nano import create_nano_agent
|
|
60
61
|
|
|
61
|
-
return create_nano_agent(
|
|
62
|
+
return create_nano_agent(
|
|
63
|
+
cfg, interaction_mode=mode, backend=sandbox, checkpointer=checkpointer
|
|
64
|
+
)
|
|
62
65
|
|
|
63
66
|
@staticmethod
|
|
64
67
|
def soothe_builder() -> Any:
|
|
@@ -80,18 +83,33 @@ class AgentFactory:
|
|
|
80
83
|
mode: str | None = None,
|
|
81
84
|
*,
|
|
82
85
|
sandbox: Any | None = None,
|
|
86
|
+
checkpointer: Any | None = None,
|
|
83
87
|
) -> Any:
|
|
84
88
|
"""Build a ``soothe`` (host runtime) agent for ``profile``'s surface.
|
|
85
89
|
|
|
86
90
|
``interrupt_on`` is pinned to ``{}``: the host builder otherwise injects
|
|
87
91
|
HITL interrupts for mutating tools (``edit_file`` / ``write_file`` /
|
|
88
|
-
``delete`` / ``run_command``), and FlowJet
|
|
89
|
-
|
|
92
|
+
``delete`` / ``run_command``), and FlowJet does not surface those
|
|
93
|
+
approval requests as resumable pauses — only ``ask_user`` clarifications
|
|
94
|
+
are resumed (via the checkpointer + ``Command(resume=...)``), so a
|
|
95
|
+
mutating-tool interrupt would stall the run.
|
|
96
|
+
|
|
97
|
+
``checkpointer`` is attached at graph-compile time so both mode graphs
|
|
98
|
+
of a server-surface agent share it (a post-hoc ``graph.checkpointer``
|
|
99
|
+
assignment would only reach the default mode's lazily compiled graph).
|
|
90
100
|
"""
|
|
91
101
|
factory = cls.soothe_builder()
|
|
92
102
|
if profile.surface is Surface.SERVER:
|
|
93
|
-
return ModeRoutedSootheAgent(
|
|
94
|
-
|
|
103
|
+
return ModeRoutedSootheAgent(
|
|
104
|
+
config, factory, sandbox=sandbox, checkpointer=checkpointer
|
|
105
|
+
)
|
|
106
|
+
return factory(
|
|
107
|
+
config,
|
|
108
|
+
interaction_mode=mode,
|
|
109
|
+
interrupt_on={},
|
|
110
|
+
backend=sandbox,
|
|
111
|
+
checkpointer=checkpointer,
|
|
112
|
+
)
|
|
95
113
|
|
|
96
114
|
|
|
97
115
|
class ModeRoutedSootheAgent:
|
|
@@ -103,8 +121,10 @@ class ModeRoutedSootheAgent:
|
|
|
103
121
|
request must never land on the ``agent`` graph. Agents are built lazily, so
|
|
104
122
|
a worker that only serves ``ask`` never builds the ``agent`` graph.
|
|
105
123
|
|
|
106
|
-
Only ``astream``
|
|
107
|
-
(``flowjet.core.bridges.nano.mapping``) drives
|
|
124
|
+
Only ``astream`` and ``aget_state`` are proxied: that is the whole
|
|
125
|
+
surface the nano bridge (``flowjet.core.bridges.nano.mapping``) drives
|
|
126
|
+
— streaming a turn, and probing the thread snapshot for a pending
|
|
127
|
+
``ask_user`` interrupt before it.
|
|
108
128
|
"""
|
|
109
129
|
|
|
110
130
|
def __init__(
|
|
@@ -114,11 +134,13 @@ class ModeRoutedSootheAgent:
|
|
|
114
134
|
modes: tuple[str, ...] = ("agent", "ask"),
|
|
115
135
|
*,
|
|
116
136
|
sandbox: Any | None = None,
|
|
137
|
+
checkpointer: Any | None = None,
|
|
117
138
|
) -> None:
|
|
118
139
|
self._config = config
|
|
119
140
|
self._factory = factory
|
|
120
141
|
self._modes = modes
|
|
121
142
|
self._sandbox = sandbox
|
|
143
|
+
self._checkpointer = checkpointer
|
|
122
144
|
self._agents: dict[str, Any] = {}
|
|
123
145
|
|
|
124
146
|
def _agent_for(self, mode: str) -> Any:
|
|
@@ -130,18 +152,40 @@ class ModeRoutedSootheAgent:
|
|
|
130
152
|
interaction_mode=resolved,
|
|
131
153
|
interrupt_on={},
|
|
132
154
|
backend=self._sandbox,
|
|
155
|
+
checkpointer=self._checkpointer,
|
|
133
156
|
)
|
|
134
157
|
self._agents[resolved] = agent
|
|
135
158
|
return agent
|
|
136
159
|
|
|
160
|
+
def _agent_for_config(self, config: Any) -> Any:
|
|
161
|
+
"""Pick the mode agent for a runnable config (mirrors ``astream``)."""
|
|
162
|
+
configurable: dict[str, Any] = {}
|
|
163
|
+
if isinstance(config, dict):
|
|
164
|
+
raw = config.get("configurable") or {}
|
|
165
|
+
if isinstance(raw, dict):
|
|
166
|
+
configurable = raw
|
|
167
|
+
mode = str(configurable.get("interaction_mode") or "agent")
|
|
168
|
+
return self._agent_for(mode)
|
|
169
|
+
|
|
137
170
|
async def astream(self, payload: Any, **kwargs: Any) -> Any:
|
|
138
171
|
"""Proxy ``astream`` to the agent built for the request's mode."""
|
|
139
172
|
config = kwargs.get("config") or {}
|
|
140
|
-
|
|
141
|
-
mode = str(configurable.get("interaction_mode") or "agent")
|
|
142
|
-
agent = self._agent_for(mode)
|
|
173
|
+
agent = self._agent_for_config(config)
|
|
143
174
|
async for chunk in agent.astream(payload, **kwargs):
|
|
144
175
|
yield chunk
|
|
145
176
|
|
|
177
|
+
async def aget_state(self, config: Any = None) -> Any:
|
|
178
|
+
"""Proxy ``aget_state`` to the agent built for the config's mode.
|
|
179
|
+
|
|
180
|
+
Soft-fails to ``None`` when the mode agent cannot read graph state
|
|
181
|
+
(no checkpointer), matching ``SootheNanoAgent.aget_state`` semantics
|
|
182
|
+
so pending-interrupt probes degrade instead of raising.
|
|
183
|
+
"""
|
|
184
|
+
agent = self._agent_for_config(config)
|
|
185
|
+
get_state = getattr(agent, "aget_state", None)
|
|
186
|
+
if not callable(get_state):
|
|
187
|
+
return None
|
|
188
|
+
return await get_state(config=config)
|
|
189
|
+
|
|
146
190
|
|
|
147
191
|
__all__ = ["AgentFactory", "ModeRoutedSootheAgent"]
|
|
@@ -8,7 +8,7 @@ delegates here. core remains server-agnostic — no imports from
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
12
12
|
|
|
13
13
|
from flowjet.core.backend_config import Backend, resolve_backend
|
|
14
14
|
from flowjet.core.bridges.nano import build_isolating_nano_backend
|
|
@@ -42,6 +42,13 @@ class ServerRuntimeBuilder:
|
|
|
42
42
|
workspace host↔guest and the adapters bind soothe-nano's sandbox backend
|
|
43
43
|
at agent build time. The AgentRuntime isolation is unchanged (in-process
|
|
44
44
|
thread pool).
|
|
45
|
+
|
|
46
|
+
``checkpointer_policy`` ("auto" by default) makes each worker's adapter
|
|
47
|
+
open the shared thread-state checkpointer resolved from the config's
|
|
48
|
+
durability settings — sqlite lands on
|
|
49
|
+
``$SOOTHE_DATA_DIR/databases/checkpoints.db``. This is what keeps one
|
|
50
|
+
ACP session's conversation continuous across turns, pool workers and
|
|
51
|
+
restarts, and what makes ``ask_user`` pauses resumable.
|
|
45
52
|
"""
|
|
46
53
|
|
|
47
54
|
@classmethod
|
|
@@ -57,6 +64,7 @@ class ServerRuntimeBuilder:
|
|
|
57
64
|
content_source: str | None,
|
|
58
65
|
allow_external_workspace: bool,
|
|
59
66
|
pool_settings: Any,
|
|
67
|
+
checkpointer_policy: str | None = "auto",
|
|
60
68
|
) -> RuntimeBackend:
|
|
61
69
|
backend = resolve_backend(Surface.SERVER)
|
|
62
70
|
sandbox_manager = cls.sandbox_manager(config_path)
|
|
@@ -77,10 +85,11 @@ class ServerRuntimeBuilder:
|
|
|
77
85
|
allow_external_workspace=allow_external_workspace,
|
|
78
86
|
workspace_resolver=workspace_resolver,
|
|
79
87
|
sandbox_manager=sandbox_manager,
|
|
88
|
+
checkpointer_policy=checkpointer_policy,
|
|
80
89
|
)
|
|
81
90
|
if backend is Backend.SOOTHE:
|
|
82
|
-
return build_isolating_soothe_backend(**common)
|
|
83
|
-
return build_isolating_nano_backend(**common)
|
|
91
|
+
return cast("RuntimeBackend", build_isolating_soothe_backend(**common))
|
|
92
|
+
return cast("RuntimeBackend", build_isolating_nano_backend(**common))
|
|
84
93
|
|
|
85
94
|
@classmethod
|
|
86
95
|
def sandbox_manager(cls, config_path: str | None) -> ArcBoxToolSandboxManager | None:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Workspace env binding and sqlite checkpointer opening."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from soothe_nano.utils.resolver import resolve_checkpointer
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Treatments of the checkpointer policy that mean "do not persist".
|
|
17
|
+
_OFF_VALUES = frozenset({"", "off", "disabled", "none", "false", "0"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def checkpointer_policy_enabled(policy: str | None) -> bool:
|
|
21
|
+
"""Whether a checkpointer policy string enables persistence."""
|
|
22
|
+
return str(policy or "").strip().lower() not in _OFF_VALUES
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Workspaces:
|
|
26
|
+
"""Bind the workspace root used by file/shell tools."""
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def ensure(workspace: Path | None = None) -> Path:
|
|
30
|
+
"""Use ``workspace`` or cwd as ``SOOTHE_WORKSPACE`` for file/shell tools."""
|
|
31
|
+
root = (workspace or Path.cwd()).resolve()
|
|
32
|
+
os.environ.setdefault("SOOTHE_WORKSPACE", str(root))
|
|
33
|
+
try:
|
|
34
|
+
from soothe_nano.workspace import FrameworkFilesystem
|
|
35
|
+
|
|
36
|
+
FrameworkFilesystem.set_current_workspace(root)
|
|
37
|
+
except Exception: # pragma: no cover - optional API surface
|
|
38
|
+
logger.debug("Could not set FrameworkFilesystem workspace", exc_info=True)
|
|
39
|
+
return root
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Checkpointers:
|
|
43
|
+
"""Side persistence: an ``AsyncSqliteSaver`` on nano's sqlite data path."""
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
@asynccontextmanager
|
|
47
|
+
async def open_sqlite(config: Any) -> AsyncIterator[Any | None]:
|
|
48
|
+
"""Yield an ``AsyncSqliteSaver`` using nano's default sqlite data path.
|
|
49
|
+
|
|
50
|
+
Path: ``$SOOTHE_DATA_DIR/databases/checkpoints.db`` (default
|
|
51
|
+
``~/.soothe/data/databases/checkpoints.db``). The config copy forces
|
|
52
|
+
the sqlite backend so this works regardless of the durability settings
|
|
53
|
+
(the CLI always runs sqlite).
|
|
54
|
+
"""
|
|
55
|
+
sqlite_cfg = config.model_copy(
|
|
56
|
+
update={
|
|
57
|
+
"agent": config.agent.model_copy(
|
|
58
|
+
update={
|
|
59
|
+
"protocols": config.agent.protocols.model_copy(
|
|
60
|
+
update={
|
|
61
|
+
"durability": config.agent.protocols.durability.model_copy(
|
|
62
|
+
update={
|
|
63
|
+
"backend": "sqlite",
|
|
64
|
+
"checkpointer": "sqlite",
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
result = resolve_checkpointer(sqlite_cfg)
|
|
74
|
+
db_path = _sqlite_db_path(result)
|
|
75
|
+
if not db_path:
|
|
76
|
+
logger.warning("SQLite checkpointer path unresolved; running without persistence")
|
|
77
|
+
yield None
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
saver = await _connect_sqlite(db_path)
|
|
81
|
+
try:
|
|
82
|
+
yield saver
|
|
83
|
+
finally:
|
|
84
|
+
await saver.conn.close()
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
@asynccontextmanager
|
|
88
|
+
async def open(config: Any) -> AsyncIterator[Any | None]:
|
|
89
|
+
"""Yield a checkpointer for the config's resolved durability backend.
|
|
90
|
+
|
|
91
|
+
Server-surface counterpart of :meth:`open_sqlite`: honors
|
|
92
|
+
``agent.protocols.durability.checkpointer`` (which inherits
|
|
93
|
+
``persistence.default_backend``), so a deployment that asks for sqlite
|
|
94
|
+
gets thread state that survives across turns, pool workers and
|
|
95
|
+
restarts. ``postgresql`` is not wired for the server surface yet — it
|
|
96
|
+
warns and yields ``None`` (no persistence) rather than failing
|
|
97
|
+
startup.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
backend = str(config.resolve_checkpointer_backend()).strip().lower()
|
|
101
|
+
except Exception: # noqa: BLE001 - unknown config shape → sqlite default
|
|
102
|
+
backend = "sqlite"
|
|
103
|
+
if backend != "sqlite":
|
|
104
|
+
logger.warning(
|
|
105
|
+
"Checkpointer backend %r is not wired for the flowjet server "
|
|
106
|
+
"surface yet; thread state will not persist across turns",
|
|
107
|
+
backend,
|
|
108
|
+
)
|
|
109
|
+
yield None
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
result = resolve_checkpointer(config)
|
|
113
|
+
db_path = _sqlite_db_path(result)
|
|
114
|
+
if not db_path:
|
|
115
|
+
logger.warning("SQLite checkpointer path unresolved; running without persistence")
|
|
116
|
+
yield None
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
saver = await _connect_sqlite(db_path)
|
|
120
|
+
try:
|
|
121
|
+
yield saver
|
|
122
|
+
finally:
|
|
123
|
+
await saver.conn.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _sqlite_db_path(result: Any) -> str | None:
|
|
127
|
+
"""Extract the sqlite db path from a ``resolve_checkpointer`` result."""
|
|
128
|
+
if isinstance(result, tuple):
|
|
129
|
+
_placeholder, pool_or_path = result
|
|
130
|
+
if isinstance(pool_or_path, str):
|
|
131
|
+
return pool_or_path
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def _connect_sqlite(db_path: str) -> Any:
|
|
136
|
+
"""Open one ``AsyncSqliteSaver`` connection on ``db_path``.
|
|
137
|
+
|
|
138
|
+
WAL + busy timeout are load-bearing for the server: its thread pool opens
|
|
139
|
+
one connection per worker onto the same file, and the default rollback
|
|
140
|
+
journal would block readers behind writers and surface as
|
|
141
|
+
``database is locked``.
|
|
142
|
+
"""
|
|
143
|
+
import aiosqlite
|
|
144
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
145
|
+
from soothe_nano.utils.serde import create_soothe_serde
|
|
146
|
+
|
|
147
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
conn = await aiosqlite.connect(db_path)
|
|
149
|
+
await conn.execute("PRAGMA journal_mode=WAL")
|
|
150
|
+
await conn.execute("PRAGMA busy_timeout=5000")
|
|
151
|
+
checkpointer = AsyncSqliteSaver(conn, serde=create_soothe_serde())
|
|
152
|
+
await checkpointer.setup()
|
|
153
|
+
logger.debug("SQLite checkpointer ready at %s", db_path)
|
|
154
|
+
return checkpointer
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = ["Checkpointers", "Workspaces", "checkpointer_policy_enabled"]
|