flowjet 2.1.2__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.2 → flowjet-2.1.4}/CHANGELOG.md +65 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/PKG-INFO +1 -1
- {flowjet-2.1.2 → flowjet-2.1.4}/pyproject.toml +14 -1
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/agent.py +1 -3
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/cli.py +206 -50
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/doctor_cmd.py +3 -1
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/progress.py +1 -1
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/setup_cmd.py +1 -1
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/stream.py +252 -108
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/agent/factory.py +56 -12
- {flowjet-2.1.2 → 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.2 → flowjet-2.1.4}/src/flowjet/core/bridges/base.py +90 -6
- {flowjet-2.1.2 → 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.2 → flowjet-2.1.4}/src/flowjet/core/bridges/nano/mapping.py +31 -15
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/bridges/soothe/backend.py +2 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/events.py +4 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/backend.py +1 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/pool.py +22 -3
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/request.py +5 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/workspace.py +7 -5
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/sandbox/resolver.py +1 -1
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/sandbox/settings.py +7 -2
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/streaming/tool_stream.py +3 -3
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/acp/agent.py +32 -18
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/acp/routes.py +2 -2
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/config.py +8 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/http/app.py +1 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/openai_compat/service.py +12 -2
- flowjet-2.1.2/src/flowjet/core/agent/workspace.py +0 -89
- {flowjet-2.1.2 → flowjet-2.1.4}/.gitignore +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/LICENSE +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/config/nano-local.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/config/nano-sandbox-soothe.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/config/nano.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/deploy/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/completion-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/impl/IG-001-openai-compatible-server.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/impl/IG-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/RFC-004-unified-package.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/RFC-005-arcbox-tool-sandbox.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/rfc-history.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/rfc-index.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/docs/specs/rfc-namings.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/examples/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/__main__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/__main__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/completion/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/completion/context.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/completion/engine.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/serve_cmd.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/shell/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/shell/_fj.zsh +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/shell/fj.bash +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/threads.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/cli/tool_stream.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/_warnings.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/agent/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/agent/config.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/agent/skills.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/backend_config.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/bridges/nano/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/bridges/nano/adapter.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/bridges/soothe/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/bridges/soothe/adapter.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/identity.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/modes.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/protocol.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/direct.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/fake.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/adapter.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/admission.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/errors.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/runtime/isolation/mode_gate.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/sandbox/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/sandbox/arbox.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/streaming/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/streaming/presentation.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/core/streaming/tool_results.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/home.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/__main__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/acp/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/http/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/http/auth.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/openai_compat/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/openai_compat/errors.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/openai_compat/projection.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/openai_compat/routes.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/openai_compat/schemas.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.4}/src/flowjet/server/openai_compat/store.py +0 -0
|
@@ -10,6 +10,71 @@ 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
|
+
|
|
35
|
+
## [2.1.3] — 2026-10-23
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **CLI: every failure path now surfaces a message — no more silent exits.**
|
|
40
|
+
The CLI could exit with no output (SIGTERM/SIGHUP, plugin `sys.exit`, broken
|
|
41
|
+
pipes, empty answers) and destroyed the original error stack even with `-v`.
|
|
42
|
+
This closes every identified gap in the error-to-output flow:
|
|
43
|
+
|
|
44
|
+
- `RunFailed` now carries `exc_type` + traceback; `stream_query` raises
|
|
45
|
+
`AgentRunError` so the CLI reports the real exception type and, with `-v`,
|
|
46
|
+
the original in-run stack instead of a re-raise site.
|
|
47
|
+
- SIGTERM/SIGHUP cancel the run gracefully: "interrupted by SIGTERM" with
|
|
48
|
+
conventional `128+signal` exit codes (was: instant silent death).
|
|
49
|
+
- Empty-answer runs warn on stderr ("agent produced no output · N tool calls
|
|
50
|
+
failed · last tool error") and exit 3.
|
|
51
|
+
- Partial narration is flushed to stderr before any failure/timeout/interrupt
|
|
52
|
+
message — a provider drop no longer loses the whole answer.
|
|
53
|
+
- `--timeout SECONDS` cancels a stuck run, reporting the last activity.
|
|
54
|
+
- `FJ_LOG_FILE=<path>` keeps WARNING+ with full tracebacks in a rotating log
|
|
55
|
+
for post-mortem diagnosis (incl. `logger.exception` from plugins).
|
|
56
|
+
- Multi-line errors truncated to one line now hint "run with -v".
|
|
57
|
+
- `BrokenPipeError`: exit 141 with a message; shutdown flush noise silenced
|
|
58
|
+
via `devnull` dup2 (`fj q | head`).
|
|
59
|
+
- Teardown suppresses `CancelledError` so it cannot mask the real result.
|
|
60
|
+
- Server parity: pool `worker_error`, OpenAI-compatible and ACP failures
|
|
61
|
+
populate/render `exc_type` + traceback (ACP shows "Error: <Type>: <msg>").
|
|
62
|
+
- Minor: thread-snapshot check failures are logged; config-load errors
|
|
63
|
+
honor `-v` with a traceback; plugin `sys.exit(0)` still fails loudly.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **ArcBox tool sandbox: guest network is now off by default**
|
|
68
|
+
(`sandbox.network` defaults to `false`, was `true`). With `true` the guest is
|
|
69
|
+
NAT'ed through the ArcBox host and the daemon only knows ENABLED|NONE, so
|
|
70
|
+
everything the host can route to — internal network and corporate VPN
|
|
71
|
+
included — was reachable from agent shell commands. With `false` the guest
|
|
72
|
+
boots without a network device (no route, no DNS): shell/code execution can
|
|
73
|
+
reach neither the public internet nor internal services. Web search/crawl is
|
|
74
|
+
unaffected — `web_search`/`fetch_url` are host-placement tools and never run
|
|
75
|
+
in the guest. Opt back in per deployment (`sandbox.network: true` in the
|
|
76
|
+
nano.yml) when the guest really needs egress.
|
|
77
|
+
|
|
13
78
|
## [2.1.2] — 2026-10-21
|
|
14
79
|
|
|
15
80
|
### Changed
|
|
@@ -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
|
|
|
@@ -10,17 +10,21 @@ import asyncio
|
|
|
10
10
|
import contextlib
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
+
import signal
|
|
13
14
|
import sys
|
|
15
|
+
import traceback
|
|
14
16
|
from collections.abc import Coroutine
|
|
15
17
|
from logging.handlers import RotatingFileHandler
|
|
16
18
|
from pathlib import Path
|
|
17
|
-
from typing import Any
|
|
19
|
+
from typing import Any, cast
|
|
18
20
|
|
|
19
21
|
from flowjet.cli import __version__
|
|
20
22
|
|
|
21
23
|
FORMAL_CLI = "flowjet"
|
|
22
24
|
CLI_ALIASES = frozenset({"fj", "fjf", FORMAL_CLI})
|
|
23
25
|
|
|
26
|
+
_logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
# ---------------------------------------------------------------------------
|
|
26
30
|
# CLI logging — stdlib only, kept here so ``fj -V`` / ``fj -h`` never pay the
|
|
@@ -29,18 +33,30 @@ CLI_ALIASES = frozenset({"fj", "fjf", FORMAL_CLI})
|
|
|
29
33
|
|
|
30
34
|
_BROWSER_USE_SETUP_LOGGING = "BROWSER_USE_SETUP_LOGGING"
|
|
31
35
|
_FJ_CONSOLE_HANDLER = "fj-console"
|
|
36
|
+
_FJ_FILE_HANDLER = "fj-file"
|
|
37
|
+
_LOG_FILE_ENV = "FJ_LOG_FILE"
|
|
32
38
|
|
|
33
39
|
|
|
34
40
|
class _CompactConsoleFormatter(logging.Formatter):
|
|
35
|
-
"""One-line console records
|
|
41
|
+
"""One-line console records; exceptions summarized on the same line.
|
|
42
|
+
|
|
43
|
+
Full tracebacks would smash the ephemeral progress line, but dropping the
|
|
44
|
+
exception entirely (the old behavior) hid the root cause even with ``-v``.
|
|
45
|
+
The full stack for run failures is available via ``AgentRunError``.
|
|
46
|
+
"""
|
|
36
47
|
|
|
37
48
|
def formatException(self, ei: object) -> str: # noqa: N802 - logging API
|
|
38
49
|
return ""
|
|
39
50
|
|
|
40
51
|
def format(self, record: logging.LogRecord) -> str:
|
|
41
|
-
# logger.exception sets exc_info;
|
|
42
|
-
|
|
43
|
-
record.
|
|
52
|
+
# logger.exception sets exc_info; fold "type: message" into the record
|
|
53
|
+
# message so format() stays one line without losing the cause.
|
|
54
|
+
if record.exc_info:
|
|
55
|
+
exc_val = record.exc_info[1]
|
|
56
|
+
if exc_val is not None:
|
|
57
|
+
record.msg = f"{record.getMessage()}: {type(exc_val).__name__}: {exc_val}"
|
|
58
|
+
record.exc_info = None
|
|
59
|
+
record.exc_text = None
|
|
44
60
|
return super().format(record)
|
|
45
61
|
|
|
46
62
|
|
|
@@ -52,6 +68,8 @@ def configure_cli_logging(*, verbose: bool = False) -> None:
|
|
|
52
68
|
lines do not interleave with the agent answer.
|
|
53
69
|
- Prevent ``lastResort`` traceback dumps from soothe tool failures.
|
|
54
70
|
- When ``verbose``, show WARNING+ as single-line messages on stderr.
|
|
71
|
+
- When ``FJ_LOG_FILE`` is set, mirror WARNING+ (with full tracebacks) to
|
|
72
|
+
that rotating file for post-mortem diagnosis of silent failures.
|
|
55
73
|
"""
|
|
56
74
|
os.environ.setdefault(_BROWSER_USE_SETUP_LOGGING, "false")
|
|
57
75
|
_remove_root_console_handlers()
|
|
@@ -68,6 +86,30 @@ def configure_cli_logging(*, verbose: bool = False) -> None:
|
|
|
68
86
|
root.addHandler(null)
|
|
69
87
|
if root.level < logging.WARNING:
|
|
70
88
|
root.setLevel(logging.WARNING)
|
|
89
|
+
_attach_file_log_handler(root)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _attach_file_log_handler(root: logging.Logger) -> None:
|
|
93
|
+
"""Attach the ``FJ_LOG_FILE`` rotating handler once, if requested.
|
|
94
|
+
|
|
95
|
+
Unlike the compact console formatter, the file keeps full tracebacks, so
|
|
96
|
+
``logger.exception`` records from soothe/plugins stay diagnosable after a
|
|
97
|
+
crash — including for deaths the CLI itself cannot report (SIGKILL).
|
|
98
|
+
"""
|
|
99
|
+
if any(getattr(h, "name", None) == _FJ_FILE_HANDLER for h in root.handlers):
|
|
100
|
+
return
|
|
101
|
+
path = os.environ.get(_LOG_FILE_ENV, "").strip()
|
|
102
|
+
if not path:
|
|
103
|
+
return
|
|
104
|
+
try:
|
|
105
|
+
handler = RotatingFileHandler(path, maxBytes=2_000_000, backupCount=2, encoding="utf-8")
|
|
106
|
+
except OSError:
|
|
107
|
+
_logger.warning("FJ_LOG_FILE is not writable: %s", path)
|
|
108
|
+
return
|
|
109
|
+
handler.set_name(_FJ_FILE_HANDLER)
|
|
110
|
+
handler.setLevel(logging.WARNING)
|
|
111
|
+
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s [%(name)s] %(message)s"))
|
|
112
|
+
root.addHandler(handler)
|
|
71
113
|
|
|
72
114
|
|
|
73
115
|
def silence_after_plugins(*, verbose: bool = False) -> None:
|
|
@@ -155,13 +197,18 @@ notes:
|
|
|
155
197
|
• Bypass mode (skip all security enforcement) is the CLI default — use -a for guardrails
|
|
156
198
|
• One query per thread at a time; different threads may run concurrently
|
|
157
199
|
• With -v, prints thread <id> and the project dir on stderr before the run
|
|
200
|
+
• --timeout N cancels a run after N seconds (0 = no limit)
|
|
201
|
+
• Exit codes: 0 ok · 1 error · 2 usage · 3 no output · 130/143 interrupted
|
|
202
|
+
• Set FJ_LOG_FILE=<path> to keep warnings & errors (with tracebacks) on file
|
|
158
203
|
"""
|
|
159
204
|
|
|
160
205
|
|
|
161
206
|
# Boolean short flags that may appear clustered (``-lv`` → ``-l -v``).
|
|
162
207
|
_BOOL_SHORTS = frozenset({"h", "V", "l", "v", "f", "a"})
|
|
163
208
|
# Short flags that consume the next argv token as a value.
|
|
164
|
-
_VALUE_FLAGS = frozenset(
|
|
209
|
+
_VALUE_FLAGS = frozenset(
|
|
210
|
+
{"-c", "--config", "-t", "--thread", "-w", "--workspace", "-n", "--timeout"}
|
|
211
|
+
)
|
|
165
212
|
_FLAG_ONLY = frozenset(
|
|
166
213
|
{
|
|
167
214
|
"-h",
|
|
@@ -179,7 +226,7 @@ _FLAG_ONLY = frozenset(
|
|
|
179
226
|
"--ask",
|
|
180
227
|
}
|
|
181
228
|
)
|
|
182
|
-
_EQUALS_PREFIXES = ("--config=", "--thread=", "--workspace=")
|
|
229
|
+
_EQUALS_PREFIXES = ("--config=", "--thread=", "--workspace=", "--timeout=")
|
|
183
230
|
|
|
184
231
|
|
|
185
232
|
def _expand_short_cluster(tok: str) -> list[str] | None:
|
|
@@ -244,6 +291,7 @@ def validate_arg_composition(args: Any) -> str | None:
|
|
|
244
291
|
- ``-n`` requires ``-l``/``--list``
|
|
245
292
|
- ``-l`` is exclusive with a query, ``-f``, ``-t``, ``-w``, ``--no-stream``, ``--ask``
|
|
246
293
|
- ``-t``/``--thread`` overrides ``-f``/``--follow`` when both are given
|
|
294
|
+
- ``--timeout`` must be >= 0 (0 = no limit)
|
|
247
295
|
"""
|
|
248
296
|
listing = bool(getattr(args, "list", False))
|
|
249
297
|
list_limit = getattr(args, "list_limit", None)
|
|
@@ -257,6 +305,10 @@ def validate_arg_composition(args: Any) -> str | None:
|
|
|
257
305
|
if list_limit is not None and not listing:
|
|
258
306
|
return "-n requires -l/--list"
|
|
259
307
|
|
|
308
|
+
timeout = getattr(args, "timeout", None)
|
|
309
|
+
if timeout is not None and timeout < 0:
|
|
310
|
+
return "--timeout must be >= 0 (0 = no limit)"
|
|
311
|
+
|
|
260
312
|
if listing:
|
|
261
313
|
if query:
|
|
262
314
|
return "-l/--list does not take a query (got leftover text after options)"
|
|
@@ -376,12 +428,21 @@ def _build_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
|
376
428
|
help="Ask mode: answer the query with no tools (pure Q&A)",
|
|
377
429
|
)
|
|
378
430
|
|
|
431
|
+
run = parser.add_argument_group("run")
|
|
432
|
+
run.add_argument(
|
|
433
|
+
"--timeout",
|
|
434
|
+
metavar="SECONDS",
|
|
435
|
+
type=float,
|
|
436
|
+
help="Cancel the run after N seconds (0 = no limit)",
|
|
437
|
+
)
|
|
438
|
+
|
|
379
439
|
paths = parser.add_argument_group("paths")
|
|
380
440
|
paths.add_argument(
|
|
381
441
|
"-c",
|
|
382
442
|
"--config",
|
|
383
443
|
metavar="PATH",
|
|
384
|
-
help
|
|
444
|
+
# Lazy help keeps the heavy default-config import off -V / normal runs.
|
|
445
|
+
help=cast("str", _LazyHelp(_default_config_help)),
|
|
385
446
|
)
|
|
386
447
|
paths.add_argument(
|
|
387
448
|
"-w",
|
|
@@ -408,7 +469,8 @@ def _build_setup_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
|
408
469
|
"-c",
|
|
409
470
|
"--config",
|
|
410
471
|
metavar="PATH",
|
|
411
|
-
help
|
|
472
|
+
# Lazy help keeps the heavy default-config import off -V / normal runs.
|
|
473
|
+
help=cast("str", _LazyHelp(_default_config_help)),
|
|
412
474
|
)
|
|
413
475
|
return parser
|
|
414
476
|
|
|
@@ -458,6 +520,8 @@ def _load_config_or_exit(args: argparse.Namespace) -> Any | int:
|
|
|
458
520
|
return 1
|
|
459
521
|
except Exception as exc:
|
|
460
522
|
sys.stderr.write(f"error: failed to load config: {exc}\n")
|
|
523
|
+
if getattr(args, "verbose", False):
|
|
524
|
+
sys.stderr.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
|
461
525
|
return 1
|
|
462
526
|
|
|
463
527
|
|
|
@@ -508,32 +572,16 @@ def run_pin_thread(thread_id: str) -> int:
|
|
|
508
572
|
async def _detect_pending_interrupt(agent: Any, thread_id: str) -> Any:
|
|
509
573
|
"""Return the pending ``ask_user`` interrupt value, or ``None``.
|
|
510
574
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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=...)``.
|
|
514
581
|
"""
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
try:
|
|
519
|
-
snapshot = await get_state({"configurable": {"thread_id": thread_id}})
|
|
520
|
-
except Exception:
|
|
521
|
-
return None
|
|
522
|
-
if snapshot is None:
|
|
523
|
-
return None
|
|
524
|
-
interrupts = getattr(snapshot, "interrupts", None) or ()
|
|
525
|
-
if not interrupts:
|
|
526
|
-
# Some snapshots nest interrupts under per-task state.
|
|
527
|
-
tasks = getattr(snapshot, "tasks", None) or ()
|
|
528
|
-
for task in tasks:
|
|
529
|
-
task_interrupts = getattr(task, "interrupts", None) or ()
|
|
530
|
-
interrupts = task_interrupts
|
|
531
|
-
break
|
|
532
|
-
for entry in interrupts:
|
|
533
|
-
value = getattr(entry, "value", None)
|
|
534
|
-
if isinstance(value, dict) and value.get("type") == "ask_user":
|
|
535
|
-
return value
|
|
536
|
-
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}})
|
|
537
585
|
|
|
538
586
|
|
|
539
587
|
async def run_async(args: argparse.Namespace) -> int:
|
|
@@ -567,6 +615,9 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
567
615
|
return config
|
|
568
616
|
|
|
569
617
|
workspace = Path(args.workspace).expanduser().resolve() if args.workspace else None
|
|
618
|
+
# ``--timeout 0`` means no limit (same as omitting the flag).
|
|
619
|
+
timeout_raw = getattr(args, "timeout", None)
|
|
620
|
+
timeout_s = float(timeout_raw) if timeout_raw else None
|
|
570
621
|
|
|
571
622
|
try:
|
|
572
623
|
async with open_sqlite_checkpointer(config) as checkpointer:
|
|
@@ -607,19 +658,21 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
607
658
|
sys.stdout.flush()
|
|
608
659
|
resume_value = {"answers": [args.query_text]}
|
|
609
660
|
if args.no_stream:
|
|
610
|
-
await invoke_query(
|
|
661
|
+
result = await invoke_query(
|
|
611
662
|
agent,
|
|
612
663
|
args.query_text,
|
|
613
664
|
thread_id=thread_id,
|
|
614
665
|
resume_value=resume_value,
|
|
666
|
+
timeout_s=timeout_s,
|
|
615
667
|
)
|
|
616
668
|
else:
|
|
617
|
-
await stream_query(
|
|
669
|
+
result = await stream_query(
|
|
618
670
|
agent,
|
|
619
671
|
args.query_text,
|
|
620
672
|
thread_id=thread_id,
|
|
621
673
|
show_tool_calls=args.verbose,
|
|
622
674
|
resume_value=resume_value,
|
|
675
|
+
timeout_s=timeout_s,
|
|
623
676
|
)
|
|
624
677
|
except ConcurrentSessionError as exc:
|
|
625
678
|
sys.stderr.write(f"error: {exc}\n")
|
|
@@ -627,6 +680,26 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
627
680
|
except KeyboardInterrupt:
|
|
628
681
|
sys.stderr.write("\ninterrupted\n")
|
|
629
682
|
return 130
|
|
683
|
+
except asyncio.CancelledError:
|
|
684
|
+
sig = _interrupt_signal
|
|
685
|
+
if sig is not None:
|
|
686
|
+
name = signal.Signals(sig).name
|
|
687
|
+
sys.stderr.write(f"\ninterrupted by {name} (signal {sig})\n")
|
|
688
|
+
return 128 + sig
|
|
689
|
+
sys.stderr.write("error: run cancelled\n")
|
|
690
|
+
return 130
|
|
691
|
+
except BrokenPipeError:
|
|
692
|
+
# Our stdout consumer (e.g. ``fj q | head``) closed early. The final
|
|
693
|
+
# flush guard in ``main`` silences the shutdown-time noise.
|
|
694
|
+
sys.stderr.write("error: output pipe closed\n")
|
|
695
|
+
return 141
|
|
696
|
+
except SystemExit as exc:
|
|
697
|
+
# A plugin/tool called sys.exit() mid-run; without this the process
|
|
698
|
+
# would exit with no message at all.
|
|
699
|
+
sys.stderr.write(
|
|
700
|
+
f"error: unexpected exit({exc.code}) raised during the run (plugin/tool called sys.exit)\n"
|
|
701
|
+
)
|
|
702
|
+
return 1
|
|
630
703
|
except Exception as exc:
|
|
631
704
|
from flowjet.cli.stream import write_cli_error
|
|
632
705
|
|
|
@@ -639,6 +712,10 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
639
712
|
append_history(args.query_text)
|
|
640
713
|
except Exception:
|
|
641
714
|
pass
|
|
715
|
+
if not result:
|
|
716
|
+
# Completed but produced no answer text (a warning is already on
|
|
717
|
+
# stderr) — scripts need a distinct, detectable exit code.
|
|
718
|
+
return 3
|
|
642
719
|
return 0
|
|
643
720
|
|
|
644
721
|
|
|
@@ -646,6 +723,55 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
646
723
|
# closed anyway.
|
|
647
724
|
_TEARDOWN_GRACE_S = 3.0
|
|
648
725
|
|
|
726
|
+
# Signal (SIGTERM/SIGHUP) that cancelled the current run, if any. Module-level
|
|
727
|
+
# because the loop-installed callback and `run_async` share the main thread.
|
|
728
|
+
_interrupt_signal: int | None = None
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _on_interrupt_signal(sig: int, task: asyncio.Task[int]) -> None:
|
|
732
|
+
# Convert SIGTERM/SIGHUP into a graceful task cancel (recorded for the exit code).
|
|
733
|
+
global _interrupt_signal
|
|
734
|
+
if _interrupt_signal is None:
|
|
735
|
+
_interrupt_signal = int(sig)
|
|
736
|
+
if not task.done():
|
|
737
|
+
task.cancel()
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _install_signal_cancellation(
|
|
741
|
+
loop: asyncio.AbstractEventLoop,
|
|
742
|
+
task: asyncio.Task[int],
|
|
743
|
+
) -> list[tuple[int, Any]]:
|
|
744
|
+
# Route SIGTERM/SIGHUP through the loop so the run unwinds cleanly.
|
|
745
|
+
#
|
|
746
|
+
# Without this the default disposition kills the process instantly — no
|
|
747
|
+
# cleanup, no message (the "no message and exit" symptom). Platforms
|
|
748
|
+
# without add_signal_handler (Windows) or non-main-thread callers keep
|
|
749
|
+
# the default behaviour.
|
|
750
|
+
installed: list[tuple[int, Any]] = []
|
|
751
|
+
for name in ("SIGTERM", "SIGHUP"):
|
|
752
|
+
sig = getattr(signal, name, None)
|
|
753
|
+
if sig is None:
|
|
754
|
+
continue
|
|
755
|
+
previous = signal.getsignal(sig)
|
|
756
|
+
try:
|
|
757
|
+
loop.add_signal_handler(sig, _on_interrupt_signal, sig, task)
|
|
758
|
+
except (NotImplementedError, RuntimeError, ValueError, OSError):
|
|
759
|
+
continue
|
|
760
|
+
installed.append((int(sig), previous))
|
|
761
|
+
return installed
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _remove_signal_cancellation(
|
|
765
|
+
loop: asyncio.AbstractEventLoop,
|
|
766
|
+
installed: list[tuple[int, Any]],
|
|
767
|
+
) -> None:
|
|
768
|
+
for sig, previous in installed:
|
|
769
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
770
|
+
loop.remove_signal_handler(signal.Signals(sig))
|
|
771
|
+
if previous not in (signal.SIG_DFL, None):
|
|
772
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
773
|
+
signal.signal(signal.Signals(sig), previous) # type: ignore[arg-type]
|
|
774
|
+
|
|
649
775
|
|
|
650
776
|
async def _cancel_stray_tasks(loop: asyncio.AbstractEventLoop, grace: float) -> None:
|
|
651
777
|
"""Cancel every task but this one, and close async generators, within ``grace``.
|
|
@@ -670,13 +796,24 @@ def run_one_shot(coro: Coroutine[Any, Any, int], *, grace: float = _TEARDOWN_GRA
|
|
|
670
796
|
one task that swallows ``CancelledError`` pins the process after the answer
|
|
671
797
|
has already been printed. ``loop.close()`` is also where libraries install
|
|
672
798
|
their own cleanup hooks, so it runs whether or not the strays went quietly.
|
|
799
|
+
|
|
800
|
+
SIGTERM/SIGHUP cancel the main task instead of killing the process, so the
|
|
801
|
+
run unwinds with a visible message and a conventional 128+signal exit code.
|
|
673
802
|
"""
|
|
803
|
+
global _interrupt_signal
|
|
804
|
+
_interrupt_signal = None
|
|
674
805
|
loop = asyncio.new_event_loop()
|
|
806
|
+
installed: list[tuple[int, Any]] = []
|
|
675
807
|
try:
|
|
676
808
|
asyncio.set_event_loop(loop)
|
|
677
|
-
|
|
809
|
+
task = loop.create_task(coro, name="fj-run")
|
|
810
|
+
installed = _install_signal_cancellation(loop, task)
|
|
811
|
+
return loop.run_until_complete(task)
|
|
678
812
|
finally:
|
|
679
|
-
|
|
813
|
+
_remove_signal_cancellation(loop, installed)
|
|
814
|
+
# CancelledError is a BaseException — a stray re-cancel during teardown
|
|
815
|
+
# must not mask the real result/exception.
|
|
816
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
680
817
|
loop.run_until_complete(_cancel_stray_tasks(loop, grace))
|
|
681
818
|
asyncio.set_event_loop(None)
|
|
682
819
|
loop.close()
|
|
@@ -702,6 +839,15 @@ def main_follow(argv: list[str] | None = None) -> int:
|
|
|
702
839
|
return main(_inject_follow(raw))
|
|
703
840
|
|
|
704
841
|
|
|
842
|
+
def _handle_broken_pipe() -> int:
|
|
843
|
+
"""Silence the shutdown flush noise after our stdout consumer closed early."""
|
|
844
|
+
try:
|
|
845
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
846
|
+
except (OSError, ValueError, AttributeError): # pragma: no cover - platform
|
|
847
|
+
pass
|
|
848
|
+
return 141 # 128 + SIGPIPE
|
|
849
|
+
|
|
850
|
+
|
|
705
851
|
def main(argv: list[str] | None = None) -> int:
|
|
706
852
|
# Silence noisy third-party warnings (requests version-mismatch,
|
|
707
853
|
# LangChain beta-API) before any heavy import pulls them in.
|
|
@@ -721,33 +867,43 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
721
867
|
if args.command == "setup":
|
|
722
868
|
from flowjet.cli.setup_cmd import run_setup
|
|
723
869
|
|
|
724
|
-
|
|
725
|
-
|
|
870
|
+
result = run_setup(getattr(args, "config", None))
|
|
871
|
+
elif args.command == "doctor":
|
|
726
872
|
from flowjet.cli.doctor_cmd import run_doctor
|
|
727
873
|
|
|
728
|
-
|
|
729
|
-
|
|
874
|
+
result = run_doctor(getattr(args, "doctor_argv", []))
|
|
875
|
+
elif args.command == "doctor-fix":
|
|
730
876
|
from flowjet.cli.doctor_fix_cmd import run_doctor_fix
|
|
731
877
|
|
|
732
|
-
|
|
733
|
-
|
|
878
|
+
result = run_doctor_fix(getattr(args, "doctor_fix_argv", []))
|
|
879
|
+
elif args.command == "__complete":
|
|
734
880
|
from flowjet.cli.completion import run_complete
|
|
735
881
|
|
|
736
|
-
|
|
737
|
-
|
|
882
|
+
result = run_complete(getattr(args, "complete_argv", []))
|
|
883
|
+
elif args.command == "serve":
|
|
738
884
|
# Lazy: the server extra (fastapi/uvicorn/acp) must never be an
|
|
739
885
|
# import-time dependency of the CLI.
|
|
740
886
|
from flowjet.cli.serve_cmd import run_serve
|
|
741
887
|
|
|
742
|
-
|
|
743
|
-
|
|
888
|
+
result = run_serve(getattr(args, "serve_argv", []))
|
|
889
|
+
elif args.command == "completion":
|
|
744
890
|
from flowjet.cli.completion import run_completion_script
|
|
745
891
|
|
|
746
|
-
|
|
747
|
-
|
|
892
|
+
result = run_completion_script(getattr(args, "completion_argv", []))
|
|
893
|
+
else:
|
|
894
|
+
result = run_one_shot(run_async(args))
|
|
748
895
|
except KeyboardInterrupt:
|
|
749
896
|
sys.stderr.write("\ninterrupted\n")
|
|
750
897
|
return 130
|
|
898
|
+
except BrokenPipeError:
|
|
899
|
+
return _handle_broken_pipe()
|
|
900
|
+
try:
|
|
901
|
+
# Flush before returning: a closed consumer would otherwise trigger
|
|
902
|
+
# "Exception ignored" noise during interpreter shutdown.
|
|
903
|
+
sys.stdout.flush()
|
|
904
|
+
except BrokenPipeError:
|
|
905
|
+
return _handle_broken_pipe()
|
|
906
|
+
return result
|
|
751
907
|
|
|
752
908
|
|
|
753
909
|
if __name__ == "__main__": # pragma: no cover
|
|
@@ -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)
|