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