specsmith 0.8.0.dev237__tar.gz → 0.10.0__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.
- {specsmith-0.8.0.dev237/src/specsmith.egg-info → specsmith-0.10.0}/PKG-INFO +20 -1
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/README.md +19 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/pyproject.toml +4 -1
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/__init__.py +1 -1
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/chat_runner.py +151 -30
- specsmith-0.10.0/src/specsmith/agent/core.py +98 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/events.py +58 -0
- specsmith-0.10.0/src/specsmith/agent/fallback.py +142 -0
- specsmith-0.10.0/src/specsmith/agent/profiles.py +655 -0
- specsmith-0.10.0/src/specsmith/agent/runner.py +434 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/cli.py +565 -143
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/vcs_commands.py +1 -1
- {specsmith-0.8.0.dev237 → specsmith-0.10.0/src/specsmith.egg-info}/PKG-INFO +20 -1
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith.egg-info/SOURCES.txt +7 -1
- specsmith-0.10.0/tests/test_agent_profiles.py +70 -0
- specsmith-0.10.0/tests/test_agent_runner_ready.py +75 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_chat_runner_openai_compat.py +6 -3
- specsmith-0.10.0/tests/test_fallback_chain.py +343 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_phase34_completion.py +4 -36
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_warp_parity.py +4 -115
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_warp_parity_followup.py +0 -95
- specsmith-0.8.0.dev237/src/specsmith/cloud_serve.py +0 -150
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/LICENSE +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/setup.cfg +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/belief.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/certainty.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/failure_graph.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/py.typed +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/recovery.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/session.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/stress_tester.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/epistemic/trace.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/__main__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/broker.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/cleanup.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/endpoints.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/indexer.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/mcp.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/memory.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/orchestrator.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/repl.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/router.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/rules.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/safety.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/suggester.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/tools.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/verifier.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/agent/voice.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/architect.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/auditor.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/auth.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/block_export.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/commands/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/compressor.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/config.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/console_utils.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/credit_analyzer.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/credits.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/differ.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/doctor.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/drive.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/epistemic/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/epistemic/belief.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/epistemic/certainty.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/epistemic/failure_graph.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/epistemic/recovery.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/epistemic/stress_tester.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/executor.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/exporter.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/app.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/main_window.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/session_tab.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/theme.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/widgets/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/widgets/chat_view.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/widgets/input_bar.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/widgets/provider_bar.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/widgets/token_meter.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/widgets/tool_panel.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/widgets/update_checker.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/gui/worker.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/history_search.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/importer.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/agent_skill.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/aider.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/base.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/claude_code.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/copilot.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/cursor.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/gemini.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/integrations/windsurf.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/languages.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/ledger.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/patent.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/phase.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/plugins.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/profiles.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/rate_limits.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/releaser.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/requirements.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/requirements_parser.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/retrieval.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/scaffolder.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/serve.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/session.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/skills.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/agents.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/bug_report.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/code_of_conduct.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/contributing.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/feature_request.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/license-Apache-2.0.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/license-MIT.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/pull_request_template.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/community/security.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/docs/architecture.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/docs/mkdocs.yml.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/docs/readthedocs.yaml.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/docs/requirements.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/docs/test-spec.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/editorconfig.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/gitattributes.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/gitignore.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/go/go.mod.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/go/main.go.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/belief-registry.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/context-budget.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/drift-metrics.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/epistemic-axioms.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/failure-modes.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/lifecycle.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/roles.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/rules.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/session-protocol.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/uncertainty-map.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/governance/verification.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/js/package.json.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/ledger.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/python/cli.py.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/python/init.py.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/python/pyproject.toml.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/readme.md.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/rust/Cargo.toml.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/rust/main.rs.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/scripts/exec.cmd.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/scripts/exec.sh.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/scripts/run.cmd.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/scripts/run.sh.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/scripts/setup.cmd.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/scripts/setup.sh.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/templates/workflows/release.yml.j2 +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/tool_installer.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/toolrules.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/tools.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/trace.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/updater.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/upgrader.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/validator.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/vcs/__init__.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/vcs/base.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/vcs/bitbucket.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/vcs/github.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/vcs/gitlab.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/wireframes.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith/workspace.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith.egg-info/dependency_links.txt +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith.egg-info/entry_points.txt +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith.egg-info/requires.txt +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/src/specsmith.egg-info/top_level.txt +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_CMD_001.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_auditor.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_chat_diff_decision.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_chat_stdin_protocol.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_cli.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_cli_workflows_history_drive.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_compressor.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_e2e_nexus.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_endpoints_cli.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_endpoints_store.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_epistemic.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_importer.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_integrations.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_mcp_client.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_nexus.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_phase1_4_new.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_rate_limits.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_scaffolder.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_skill_marketplace.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_smoke.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_suggester.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_tools.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_validator.py +0 -0
- {specsmith-0.8.0.dev237 → specsmith-0.10.0}/tests/test_vcs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands.
|
|
5
5
|
Author: BitConcepts
|
|
6
6
|
License-Expression: MIT
|
|
@@ -88,6 +88,25 @@ specsmith treats belief systems like code: codable, testable, and deployable. It
|
|
|
88
88
|
epistemically-governed projects, stress-tests requirements as BeliefArtifacts, runs
|
|
89
89
|
cryptographically-sealed trace vaults, and orchestrates AI agents under formal AEE governance.
|
|
90
90
|
|
|
91
|
+
**0.10.0 — Multi-Agent + BYOE.** A `/plan` goes to the architect, `/fix`
|
|
92
|
+
goes to the coder, `/review` goes to a reviewer that runs on a different
|
|
93
|
+
model family. Each *profile* is a `(provider, model, endpoint?, fallback_chain)`
|
|
94
|
+
bundle stored in `~/.specsmith/agents.json`; an *activity routing table*
|
|
95
|
+
maps slash commands and AEE phases to profiles; **BYOE endpoints**
|
|
96
|
+
(`~/.specsmith/endpoints.json`) let you point a profile at any
|
|
97
|
+
OpenAI-v1-compatible backend you self-host (vLLM, llama.cpp `server`,
|
|
98
|
+
LM Studio, TGI, ...). Cross-family **diversity guard**, capability
|
|
99
|
+
filtering, transient-failure fallback chains, and TraceVault decision
|
|
100
|
+
seals on every `/agent` pin are wired in by default. See
|
|
101
|
+
[`docs/site/agents.md`](docs/site/agents.md) for the five-minute walkthrough.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
specsmith agents preset apply default # frontier coder + cross-family reviewer
|
|
105
|
+
specsmith endpoints add --id home-vllm \
|
|
106
|
+
--base-url http://10.0.0.4:8000/v1 --auth bearer-keyring
|
|
107
|
+
specsmith run --agent opus-reviewer # one-shot per-session pin
|
|
108
|
+
```
|
|
109
|
+
|
|
91
110
|
It also co-installs the standalone `epistemic` Python library for direct use in any project:
|
|
92
111
|
|
|
93
112
|
```python
|
|
@@ -16,6 +16,25 @@ specsmith treats belief systems like code: codable, testable, and deployable. It
|
|
|
16
16
|
epistemically-governed projects, stress-tests requirements as BeliefArtifacts, runs
|
|
17
17
|
cryptographically-sealed trace vaults, and orchestrates AI agents under formal AEE governance.
|
|
18
18
|
|
|
19
|
+
**0.10.0 — Multi-Agent + BYOE.** A `/plan` goes to the architect, `/fix`
|
|
20
|
+
goes to the coder, `/review` goes to a reviewer that runs on a different
|
|
21
|
+
model family. Each *profile* is a `(provider, model, endpoint?, fallback_chain)`
|
|
22
|
+
bundle stored in `~/.specsmith/agents.json`; an *activity routing table*
|
|
23
|
+
maps slash commands and AEE phases to profiles; **BYOE endpoints**
|
|
24
|
+
(`~/.specsmith/endpoints.json`) let you point a profile at any
|
|
25
|
+
OpenAI-v1-compatible backend you self-host (vLLM, llama.cpp `server`,
|
|
26
|
+
LM Studio, TGI, ...). Cross-family **diversity guard**, capability
|
|
27
|
+
filtering, transient-failure fallback chains, and TraceVault decision
|
|
28
|
+
seals on every `/agent` pin are wired in by default. See
|
|
29
|
+
[`docs/site/agents.md`](docs/site/agents.md) for the five-minute walkthrough.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
specsmith agents preset apply default # frontier coder + cross-family reviewer
|
|
33
|
+
specsmith endpoints add --id home-vllm \
|
|
34
|
+
--base-url http://10.0.0.4:8000/v1 --auth bearer-keyring
|
|
35
|
+
specsmith run --agent opus-reviewer # one-shot per-session pin
|
|
36
|
+
```
|
|
37
|
+
|
|
19
38
|
It also co-installs the standalone `epistemic` Python library for direct use in any project:
|
|
20
39
|
|
|
21
40
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specsmith"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "Applied Epistemic Engineering toolkit — AEE agent sessions, execution profiles, FPGA/HDL governance, tool installer, 50+ CLI commands."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -171,6 +171,9 @@ module = [
|
|
|
171
171
|
"specsmith.importer",
|
|
172
172
|
"specsmith.agent.providers.gemini",
|
|
173
173
|
"specsmith.agent.runner",
|
|
174
|
+
"specsmith.agent.profiles",
|
|
175
|
+
"specsmith.agent.fallback",
|
|
176
|
+
"specsmith.agent.core",
|
|
174
177
|
"specsmith.agent.cleanup",
|
|
175
178
|
"specsmith.agent.orchestrator",
|
|
176
179
|
"specsmith.agent.repl",
|
|
@@ -8,4 +8,4 @@ from importlib.metadata import version as _pkg_version
|
|
|
8
8
|
try:
|
|
9
9
|
__version__: str = _pkg_version("specsmith")
|
|
10
10
|
except PackageNotFoundError: # running from source without install
|
|
11
|
-
__version__ = "0.
|
|
11
|
+
__version__ = "0.10.0" # fallback: keep in sync with pyproject.toml
|
|
@@ -53,6 +53,14 @@ class ChatRunResult:
|
|
|
53
53
|
files_changed: list[str] = field(default_factory=list)
|
|
54
54
|
verdict: VerifierVerdict | None = None
|
|
55
55
|
raw_text: str = ""
|
|
56
|
+
# C1: per-turn token + cost accounting. Populated by the provider
|
|
57
|
+
# driver when it can read counters from the response (Ollama and
|
|
58
|
+
# Anthropic both expose them). Falls back to a deterministic char-
|
|
59
|
+
# based heuristic so the TokenMeter chip is never zero on Ollama or
|
|
60
|
+
# OpenAI-compat endpoints that don't surface usage in streaming mode.
|
|
61
|
+
tokens_in: int = 0
|
|
62
|
+
tokens_out: int = 0
|
|
63
|
+
cost_usd: float = 0.0
|
|
56
64
|
|
|
57
65
|
def to_dict(self) -> dict[str, Any]:
|
|
58
66
|
return {
|
|
@@ -61,6 +69,9 @@ class ChatRunResult:
|
|
|
61
69
|
"files_changed": list(self.files_changed),
|
|
62
70
|
"confidence": self.verdict.confidence if self.verdict else 0.0,
|
|
63
71
|
"equilibrium": self.verdict.equilibrium if self.verdict else False,
|
|
72
|
+
"tokens_in": int(self.tokens_in),
|
|
73
|
+
"tokens_out": int(self.tokens_out),
|
|
74
|
+
"cost_usd": float(self.cost_usd),
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
|
|
@@ -103,44 +114,99 @@ def run_chat(
|
|
|
103
114
|
endpoint = None
|
|
104
115
|
if endpoint is not None:
|
|
105
116
|
try:
|
|
106
|
-
full_text = _run_openai_compat(
|
|
117
|
+
full_text, usage = _run_openai_compat(
|
|
118
|
+
messages, emitter, msg_block, endpoint=endpoint
|
|
119
|
+
)
|
|
107
120
|
except Exception: # noqa: BLE001 - degrade to auto-detect
|
|
108
|
-
full_text = None
|
|
121
|
+
full_text, usage = None, _UsageDelta()
|
|
109
122
|
if full_text is not None:
|
|
110
|
-
return _finalize(
|
|
123
|
+
return _finalize(
|
|
124
|
+
full_text,
|
|
125
|
+
"openai_compat",
|
|
126
|
+
project_dir,
|
|
127
|
+
confidence_target,
|
|
128
|
+
messages=messages,
|
|
129
|
+
usage=usage,
|
|
130
|
+
)
|
|
111
131
|
|
|
112
132
|
# Order matters: Ollama first because it's local-first and free.
|
|
113
133
|
for provider in (_run_ollama, _run_anthropic, _run_openai, _run_gemini):
|
|
114
134
|
try:
|
|
115
|
-
full_text = provider(messages, emitter, msg_block)
|
|
135
|
+
full_text, usage = provider(messages, emitter, msg_block)
|
|
116
136
|
except Exception: # noqa: BLE001 - any failure → next provider
|
|
117
137
|
continue
|
|
118
138
|
if full_text is None:
|
|
119
139
|
continue
|
|
120
|
-
return _finalize(
|
|
140
|
+
return _finalize(
|
|
141
|
+
full_text,
|
|
142
|
+
provider.__name__,
|
|
143
|
+
project_dir,
|
|
144
|
+
confidence_target,
|
|
145
|
+
messages=messages,
|
|
146
|
+
usage=usage,
|
|
147
|
+
)
|
|
121
148
|
return None
|
|
122
149
|
|
|
123
150
|
|
|
151
|
+
@dataclass
|
|
152
|
+
class _UsageDelta:
|
|
153
|
+
"""Per-turn token + cost counters reported by a provider driver.
|
|
154
|
+
|
|
155
|
+
All fields default to ``0`` so callers can construct a zero-value
|
|
156
|
+
instance without caring whether the provider supports usage tracking.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
tokens_in: int = 0
|
|
160
|
+
tokens_out: int = 0
|
|
161
|
+
cost_usd: float = 0.0
|
|
162
|
+
|
|
163
|
+
|
|
124
164
|
def _finalize(
|
|
125
165
|
full_text: str,
|
|
126
166
|
provider_fn_name: str,
|
|
127
167
|
project_dir: Path,
|
|
128
168
|
confidence_target: float,
|
|
169
|
+
*,
|
|
170
|
+
messages: list[dict[str, str]] | None = None,
|
|
171
|
+
usage: _UsageDelta | None = None,
|
|
129
172
|
) -> ChatRunResult:
|
|
130
173
|
sections = _parse_output_contract(full_text)
|
|
131
174
|
files_changed = _split_files_list(sections.get("files_changed", ""))
|
|
132
175
|
report = report_from_chat_sections(sections, files_changed=files_changed)
|
|
133
176
|
verdict = score(report, confidence_target=confidence_target)
|
|
134
177
|
summary = (sections.get("plan") or full_text.strip()[:200]).strip() or verdict.summary
|
|
178
|
+
|
|
179
|
+
# C1: when the provider didn't report exact counts, estimate from text.
|
|
180
|
+
# The four-chars-per-token rule of thumb is OpenAI's published guidance
|
|
181
|
+
# and matches Ollama / Anthropic / Gemini within ~10% across the model
|
|
182
|
+
# families we ship today — close enough for the TokenMeter chip and
|
|
183
|
+
# the ``credits record`` ledger event.
|
|
184
|
+
if usage is None:
|
|
185
|
+
usage = _UsageDelta()
|
|
186
|
+
if usage.tokens_in == 0 and messages is not None:
|
|
187
|
+
usage.tokens_in = _estimate_tokens("\n".join(m.get("content", "") for m in messages))
|
|
188
|
+
if usage.tokens_out == 0:
|
|
189
|
+
usage.tokens_out = _estimate_tokens(full_text)
|
|
190
|
+
|
|
135
191
|
return ChatRunResult(
|
|
136
192
|
provider=provider_fn_name.removeprefix("_run_"),
|
|
137
193
|
summary=summary,
|
|
138
194
|
files_changed=files_changed,
|
|
139
195
|
verdict=verdict,
|
|
140
196
|
raw_text=full_text,
|
|
197
|
+
tokens_in=int(usage.tokens_in),
|
|
198
|
+
tokens_out=int(usage.tokens_out),
|
|
199
|
+
cost_usd=float(usage.cost_usd),
|
|
141
200
|
)
|
|
142
201
|
|
|
143
202
|
|
|
203
|
+
def _estimate_tokens(text: str) -> int:
|
|
204
|
+
"""Rough char→token heuristic (4 chars/token, floor at 1 if non-empty)."""
|
|
205
|
+
if not text:
|
|
206
|
+
return 0
|
|
207
|
+
return max(1, len(text) // 4)
|
|
208
|
+
|
|
209
|
+
|
|
144
210
|
# ---------------------------------------------------------------------------
|
|
145
211
|
# Provider drivers — each returns the full assembled text or None
|
|
146
212
|
# ---------------------------------------------------------------------------
|
|
@@ -150,13 +216,14 @@ def _run_ollama(
|
|
|
150
216
|
messages: list[dict[str, str]],
|
|
151
217
|
emitter: EventEmitter,
|
|
152
218
|
block_id: str,
|
|
153
|
-
) -> str | None:
|
|
219
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
154
220
|
"""Stream from a local Ollama daemon using only stdlib."""
|
|
155
221
|
host = os.environ.get("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).rstrip("/")
|
|
156
222
|
model = os.environ.get("SPECSMITH_OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL)
|
|
223
|
+
usage = _UsageDelta()
|
|
157
224
|
|
|
158
225
|
if not _ollama_alive(host):
|
|
159
|
-
return None
|
|
226
|
+
return None, usage
|
|
160
227
|
|
|
161
228
|
payload = json.dumps({"model": model, "messages": messages, "stream": True}).encode("utf-8")
|
|
162
229
|
req = Request( # noqa: S310 - URL is a hardcoded localhost default
|
|
@@ -181,8 +248,13 @@ def _run_ollama(
|
|
|
181
248
|
emitter.token(block_id, chunk)
|
|
182
249
|
pieces.append(chunk)
|
|
183
250
|
if obj.get("done"):
|
|
251
|
+
# C1: Ollama exposes prompt_eval_count + eval_count on the
|
|
252
|
+
# final ``done`` message. Cost is zero for local models.
|
|
253
|
+
usage.tokens_in = int(obj.get("prompt_eval_count") or 0)
|
|
254
|
+
usage.tokens_out = int(obj.get("eval_count") or 0)
|
|
255
|
+
usage.cost_usd = 0.0
|
|
184
256
|
break
|
|
185
|
-
return "".join(pieces) if pieces else None
|
|
257
|
+
return ("".join(pieces) if pieces else None), usage
|
|
186
258
|
|
|
187
259
|
|
|
188
260
|
def _ollama_alive(host: str) -> bool:
|
|
@@ -197,14 +269,15 @@ def _run_anthropic(
|
|
|
197
269
|
messages: list[dict[str, str]],
|
|
198
270
|
emitter: EventEmitter,
|
|
199
271
|
block_id: str,
|
|
200
|
-
) -> str | None:
|
|
272
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
201
273
|
"""Use the anthropic SDK if installed and a key is configured."""
|
|
274
|
+
usage = _UsageDelta()
|
|
202
275
|
if not os.environ.get("ANTHROPIC_API_KEY"):
|
|
203
|
-
return None
|
|
276
|
+
return None, usage
|
|
204
277
|
try:
|
|
205
278
|
import anthropic
|
|
206
279
|
except ImportError:
|
|
207
|
-
return None
|
|
280
|
+
return None, usage
|
|
208
281
|
|
|
209
282
|
system = "\n".join(m["content"] for m in messages if m["role"] == "system")
|
|
210
283
|
user_msgs = [m for m in messages if m["role"] != "system"]
|
|
@@ -221,35 +294,54 @@ def _run_anthropic(
|
|
|
221
294
|
if text:
|
|
222
295
|
emitter.token(block_id, text)
|
|
223
296
|
pieces.append(text)
|
|
224
|
-
|
|
297
|
+
# C1: pull final usage off the SDK's `final_message`. Cost is the
|
|
298
|
+
# caller's problem (rate-limit module knows the model price); we
|
|
299
|
+
# report tokens here and let the credits ledger compute USD.
|
|
300
|
+
try:
|
|
301
|
+
final = stream.get_final_message()
|
|
302
|
+
usage.tokens_in = int(getattr(final.usage, "input_tokens", 0) or 0)
|
|
303
|
+
usage.tokens_out = int(getattr(final.usage, "output_tokens", 0) or 0)
|
|
304
|
+
except Exception: # noqa: BLE001 - usage is best-effort
|
|
305
|
+
pass
|
|
306
|
+
return ("".join(pieces) if pieces else None), usage
|
|
225
307
|
|
|
226
308
|
|
|
227
309
|
def _run_openai(
|
|
228
310
|
messages: list[dict[str, str]],
|
|
229
311
|
emitter: EventEmitter,
|
|
230
312
|
block_id: str,
|
|
231
|
-
) -> str | None:
|
|
313
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
232
314
|
"""Use the openai SDK if installed and a key is configured."""
|
|
315
|
+
usage = _UsageDelta()
|
|
233
316
|
if not os.environ.get("OPENAI_API_KEY"):
|
|
234
|
-
return None
|
|
317
|
+
return None, usage
|
|
235
318
|
try:
|
|
236
319
|
from openai import OpenAI
|
|
237
320
|
except ImportError:
|
|
238
|
-
return None
|
|
321
|
+
return None, usage
|
|
239
322
|
|
|
240
323
|
client = OpenAI()
|
|
324
|
+
# ``stream_options.include_usage`` makes the final SSE chunk carry a
|
|
325
|
+
# populated ``usage`` block (otherwise streaming responses emit it as
|
|
326
|
+
# ``None``). Older SDK versions silently ignore unknown kwargs.
|
|
241
327
|
stream = client.chat.completions.create(
|
|
242
328
|
model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"),
|
|
243
329
|
messages=messages,
|
|
244
330
|
stream=True,
|
|
331
|
+
stream_options={"include_usage": True},
|
|
245
332
|
)
|
|
246
333
|
pieces: list[str] = []
|
|
247
334
|
for chunk in stream:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
335
|
+
if chunk.choices:
|
|
336
|
+
text = chunk.choices[0].delta.content or ""
|
|
337
|
+
if text:
|
|
338
|
+
emitter.token(block_id, text)
|
|
339
|
+
pieces.append(text)
|
|
340
|
+
usage_obj = getattr(chunk, "usage", None)
|
|
341
|
+
if usage_obj is not None:
|
|
342
|
+
usage.tokens_in = int(getattr(usage_obj, "prompt_tokens", 0) or 0)
|
|
343
|
+
usage.tokens_out = int(getattr(usage_obj, "completion_tokens", 0) or 0)
|
|
344
|
+
return ("".join(pieces) if pieces else None), usage
|
|
253
345
|
|
|
254
346
|
|
|
255
347
|
def _run_openai_compat(
|
|
@@ -258,7 +350,7 @@ def _run_openai_compat(
|
|
|
258
350
|
block_id: str,
|
|
259
351
|
*,
|
|
260
352
|
endpoint: Any,
|
|
261
|
-
) -> str | None:
|
|
353
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
262
354
|
"""Stream from a user-registered OpenAI-v1-compatible endpoint (REQ-142).
|
|
263
355
|
|
|
264
356
|
Uses raw stdlib HTTP so the openai SDK is not a hard dependency for
|
|
@@ -266,13 +358,14 @@ def _run_openai_compat(
|
|
|
266
358
|
Server-Sent-Events ``data:`` lines, and forwards each ``content``
|
|
267
359
|
delta as a ``token`` event on ``block_id``.
|
|
268
360
|
"""
|
|
361
|
+
usage = _UsageDelta()
|
|
269
362
|
base_url = endpoint.base_url.rstrip("/")
|
|
270
363
|
url = f"{base_url}/chat/completions"
|
|
271
364
|
model = endpoint.default_model or os.environ.get("SPECSMITH_OPENAI_COMPAT_MODEL", "")
|
|
272
365
|
if not model:
|
|
273
366
|
# The endpoint did not pin a default model and the env override is
|
|
274
367
|
# absent. We cannot fabricate one; fall back to the auto-detect chain.
|
|
275
|
-
return None
|
|
368
|
+
return None, usage
|
|
276
369
|
|
|
277
370
|
headers: dict[str, str] = {
|
|
278
371
|
"Content-Type": "application/json",
|
|
@@ -281,11 +374,20 @@ def _run_openai_compat(
|
|
|
281
374
|
try:
|
|
282
375
|
token = endpoint.resolve_token()
|
|
283
376
|
except Exception: # noqa: BLE001 - fall back to auto-detect chain
|
|
284
|
-
return None
|
|
377
|
+
return None, usage
|
|
285
378
|
if token:
|
|
286
379
|
headers["Authorization"] = f"Bearer {token}"
|
|
287
380
|
|
|
288
|
-
body = json.dumps(
|
|
381
|
+
body = json.dumps(
|
|
382
|
+
{
|
|
383
|
+
"model": model,
|
|
384
|
+
"messages": messages,
|
|
385
|
+
"stream": True,
|
|
386
|
+
# Many vLLM/llama.cpp builds honour OpenAI's stream_options;
|
|
387
|
+
# the request is harmless if they don't.
|
|
388
|
+
"stream_options": {"include_usage": True},
|
|
389
|
+
}
|
|
390
|
+
).encode("utf-8")
|
|
289
391
|
req = Request(url, data=body, headers=headers, method="POST") # noqa: S310 - user-supplied
|
|
290
392
|
|
|
291
393
|
ctx = None
|
|
@@ -313,6 +415,10 @@ def _run_openai_compat(
|
|
|
313
415
|
except ValueError:
|
|
314
416
|
continue
|
|
315
417
|
choices = obj.get("choices") or []
|
|
418
|
+
usage_obj = obj.get("usage")
|
|
419
|
+
if usage_obj:
|
|
420
|
+
usage.tokens_in = int(usage_obj.get("prompt_tokens") or 0)
|
|
421
|
+
usage.tokens_out = int(usage_obj.get("completion_tokens") or 0)
|
|
316
422
|
if not choices:
|
|
317
423
|
continue
|
|
318
424
|
delta = (choices[0] or {}).get("delta") or {}
|
|
@@ -321,35 +427,50 @@ def _run_openai_compat(
|
|
|
321
427
|
emitter.token(block_id, chunk)
|
|
322
428
|
pieces.append(chunk)
|
|
323
429
|
except (URLError, TimeoutError, OSError):
|
|
324
|
-
return None
|
|
325
|
-
return "".join(pieces) if pieces else None
|
|
430
|
+
return None, usage
|
|
431
|
+
return ("".join(pieces) if pieces else None), usage
|
|
326
432
|
|
|
327
433
|
|
|
328
434
|
def _run_gemini(
|
|
329
435
|
messages: list[dict[str, str]],
|
|
330
436
|
emitter: EventEmitter,
|
|
331
437
|
block_id: str,
|
|
332
|
-
) -> str | None:
|
|
438
|
+
) -> tuple[str | None, _UsageDelta]:
|
|
333
439
|
"""Use google-genai SDK if installed and a key is configured."""
|
|
440
|
+
usage = _UsageDelta()
|
|
334
441
|
if not os.environ.get("GOOGLE_API_KEY"):
|
|
335
|
-
return None
|
|
442
|
+
return None, usage
|
|
336
443
|
try:
|
|
337
444
|
from google import genai
|
|
338
445
|
except ImportError:
|
|
339
|
-
return None
|
|
446
|
+
return None, usage
|
|
340
447
|
|
|
341
448
|
client = genai.Client()
|
|
342
449
|
prompt = "\n".join(f"{m['role']}: {m['content']}" for m in messages)
|
|
343
450
|
pieces: list[str] = []
|
|
451
|
+
last_chunk: Any = None
|
|
344
452
|
for chunk in client.models.generate_content_stream(
|
|
345
453
|
model=os.environ.get("GEMINI_MODEL", "gemini-2.5-flash"),
|
|
346
454
|
contents=prompt,
|
|
347
455
|
):
|
|
456
|
+
last_chunk = chunk
|
|
348
457
|
text = getattr(chunk, "text", "") or ""
|
|
349
458
|
if text:
|
|
350
459
|
emitter.token(block_id, text)
|
|
351
460
|
pieces.append(text)
|
|
352
|
-
|
|
461
|
+
# Gemini exposes ``usage_metadata`` on the final chunk. Field names
|
|
462
|
+
# vary across SDK versions; we accept the union.
|
|
463
|
+
meta = getattr(last_chunk, "usage_metadata", None) if last_chunk else None
|
|
464
|
+
if meta is not None:
|
|
465
|
+
usage.tokens_in = int(
|
|
466
|
+
getattr(meta, "prompt_token_count", 0) or getattr(meta, "input_token_count", 0) or 0
|
|
467
|
+
)
|
|
468
|
+
usage.tokens_out = int(
|
|
469
|
+
getattr(meta, "candidates_token_count", 0)
|
|
470
|
+
or getattr(meta, "output_token_count", 0)
|
|
471
|
+
or 0
|
|
472
|
+
)
|
|
473
|
+
return ("".join(pieces) if pieces else None), usage
|
|
353
474
|
|
|
354
475
|
|
|
355
476
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 BitConcepts, LLC. All rights reserved.
|
|
3
|
+
"""Shared agent runtime primitives (REQ-145).
|
|
4
|
+
|
|
5
|
+
Hosts low-level enums and dataclasses that span :mod:`specsmith.agent.runner`,
|
|
6
|
+
:mod:`specsmith.serve`, :mod:`specsmith.agent.profiles`, and
|
|
7
|
+
:mod:`specsmith.agent.fallback` without forcing them to import each other.
|
|
8
|
+
|
|
9
|
+
The historical ``cli.py`` referenced ``ModelTier`` from this module before
|
|
10
|
+
it existed in the source tree (the file was lost in an earlier refactor),
|
|
11
|
+
which produced an ``ImportError`` the moment ``specsmith run`` was
|
|
12
|
+
invoked. Restoring the symbol here is the prerequisite for the bridge
|
|
13
|
+
``ready`` event handshake to land before the VS Code extension's 20 s
|
|
14
|
+
startup timeout fires.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import enum
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ModelTier(str, enum.Enum):
|
|
25
|
+
"""Capability tier for an LLM call.
|
|
26
|
+
|
|
27
|
+
Ordered cheapest → most capable so that a fallback chain can iterate
|
|
28
|
+
in declaration order without external metadata.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
FAST = "fast"
|
|
32
|
+
BALANCED = "balanced"
|
|
33
|
+
POWERFUL = "powerful"
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def parse(
|
|
37
|
+
cls,
|
|
38
|
+
value: str | ModelTier | None,
|
|
39
|
+
default: ModelTier | None = None,
|
|
40
|
+
) -> ModelTier:
|
|
41
|
+
"""Tolerant parser used by CLI option handlers."""
|
|
42
|
+
if value is None or value == "":
|
|
43
|
+
return default or cls.BALANCED
|
|
44
|
+
if isinstance(value, cls):
|
|
45
|
+
return value
|
|
46
|
+
try:
|
|
47
|
+
return cls(str(value).strip().lower())
|
|
48
|
+
except ValueError:
|
|
49
|
+
return default or cls.BALANCED
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AgentState:
|
|
54
|
+
"""Mutable per-session metrics surfaced via ``specsmith serve``'s
|
|
55
|
+
``GET /api/status`` endpoint and the VS Code TokenMeter chip.
|
|
56
|
+
|
|
57
|
+
Field names mirror what :class:`specsmith.serve._AgentThread` reads off
|
|
58
|
+
``runner._state``; do not rename without updating that consumer.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
provider_name: str = ""
|
|
62
|
+
model_name: str = ""
|
|
63
|
+
profile_id: str = ""
|
|
64
|
+
session_tokens: int = 0
|
|
65
|
+
tokens_in: int = 0
|
|
66
|
+
tokens_out: int = 0
|
|
67
|
+
total_cost_usd: float = 0.0
|
|
68
|
+
tool_calls_made: int = 0
|
|
69
|
+
elapsed_minutes: float = 0.0
|
|
70
|
+
by_profile: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
def credit(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
profile_id: str,
|
|
76
|
+
tokens_in: int = 0,
|
|
77
|
+
tokens_out: int = 0,
|
|
78
|
+
cost_usd: float = 0.0,
|
|
79
|
+
tool_calls: int = 0,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Aggregate one turn's metrics into the running totals."""
|
|
82
|
+
self.tokens_in += int(tokens_in)
|
|
83
|
+
self.tokens_out += int(tokens_out)
|
|
84
|
+
self.session_tokens = self.tokens_in + self.tokens_out
|
|
85
|
+
self.total_cost_usd += float(cost_usd)
|
|
86
|
+
self.tool_calls_made += int(tool_calls)
|
|
87
|
+
bucket = self.by_profile.setdefault(
|
|
88
|
+
profile_id or "(default)",
|
|
89
|
+
{"tokens_in": 0, "tokens_out": 0, "cost_usd": 0.0, "tool_calls": 0, "turns": 0},
|
|
90
|
+
)
|
|
91
|
+
bucket["tokens_in"] += int(tokens_in)
|
|
92
|
+
bucket["tokens_out"] += int(tokens_out)
|
|
93
|
+
bucket["cost_usd"] = round(bucket["cost_usd"] + float(cost_usd), 6)
|
|
94
|
+
bucket["tool_calls"] += int(tool_calls)
|
|
95
|
+
bucket["turns"] += 1
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["AgentState", "ModelTier"]
|
|
@@ -19,6 +19,9 @@ Event kinds
|
|
|
19
19
|
* ``plan_step`` - status transition for a step in the active plan
|
|
20
20
|
block (REQ-114).
|
|
21
21
|
* ``task_complete`` - final block; carries final summary + profile.
|
|
22
|
+
* ``ready`` - emitted exactly once at process start (REQ-145);
|
|
23
|
+
the VS Code bridge waits up to 20 s for this
|
|
24
|
+
frame before declaring the agent unresponsive.
|
|
22
25
|
"""
|
|
23
26
|
|
|
24
27
|
from __future__ import annotations
|
|
@@ -58,6 +61,61 @@ class EventEmitter:
|
|
|
58
61
|
with contextlib.suppress(Exception):
|
|
59
62
|
self.stream.flush()
|
|
60
63
|
|
|
64
|
+
# ── Lifecycle helpers ────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
def ready(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
agent: str = "nexus",
|
|
70
|
+
version: str = "",
|
|
71
|
+
project_dir: str = "",
|
|
72
|
+
provider: str = "",
|
|
73
|
+
model: str = "",
|
|
74
|
+
profile_id: str = "",
|
|
75
|
+
capabilities: list[str] | None = None,
|
|
76
|
+
**extra: Any,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Emit the bridge handshake frame (REQ-145).
|
|
79
|
+
|
|
80
|
+
The VS Code extension's :class:`SpecsmithBridge` keys off this
|
|
81
|
+
single event to flip from ``starting`` → ``waiting`` and to start
|
|
82
|
+
flushing the queued user prompts. Schema is intentionally flat so
|
|
83
|
+
a ``JSON.parse`` line check is enough on the consumer side.
|
|
84
|
+
"""
|
|
85
|
+
payload: dict[str, Any] = {
|
|
86
|
+
"type": "ready",
|
|
87
|
+
"timestamp": _now_iso(),
|
|
88
|
+
"agent": agent,
|
|
89
|
+
"version": version,
|
|
90
|
+
"project_dir": project_dir,
|
|
91
|
+
"provider": provider,
|
|
92
|
+
"model": model,
|
|
93
|
+
"profile_id": profile_id,
|
|
94
|
+
"capabilities": list(capabilities or []),
|
|
95
|
+
}
|
|
96
|
+
payload.update(extra)
|
|
97
|
+
self.emit(payload)
|
|
98
|
+
|
|
99
|
+
def system(self, message: str, **extra: Any) -> None:
|
|
100
|
+
"""Emit a free-form system note (matches bridge.ts handler)."""
|
|
101
|
+
self.emit({"type": "system", "message": message, **extra})
|
|
102
|
+
|
|
103
|
+
def turn_done(self, **extra: Any) -> None:
|
|
104
|
+
"""Emit the per-turn terminator the bridge uses to clear timers."""
|
|
105
|
+
self.emit({"type": "turn_done", "timestamp": _now_iso(), **extra})
|
|
106
|
+
|
|
107
|
+
def error(self, message: str, *, recoverable: bool = False, **extra: Any) -> None:
|
|
108
|
+
"""Emit an error frame (recoverable = retry will be offered)."""
|
|
109
|
+
self.emit(
|
|
110
|
+
{
|
|
111
|
+
"type": "error",
|
|
112
|
+
"timestamp": _now_iso(),
|
|
113
|
+
"message": message,
|
|
114
|
+
"recoverable": bool(recoverable),
|
|
115
|
+
**extra,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
61
119
|
# ── Block helpers ────────────────────────────────────────────────────
|
|
62
120
|
|
|
63
121
|
def block_start(self, kind: str, *, agent: str = "nexus", **payload: Any) -> str:
|