voodoo-framework 2.0.0__tar.gz → 2.1.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.
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/PKG-INFO +14 -20
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/README.md +13 -19
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/__init__.py +1 -1
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/__init__.py +2 -0
- voodoo_framework-2.1.0/src/voodoo/cli/approvals.py +283 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/engine.py +64 -2
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/human.py +8 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/execution/migrations.py +5 -2
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/execution/postgres.py +27 -3
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/execution/sqlite.py +25 -3
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo_framework.egg-info/PKG-INFO +14 -20
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo_framework.egg-info/SOURCES.txt +2 -0
- voodoo_framework-2.1.0/tests/test_durable_hitl.py +346 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/LICENSE +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/pyproject.toml +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/setup.cfg +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/adapters/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/adapters/capabilities.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/adapters/registry.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/adapters/tailwind.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/adapters/voodoo_css.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/agent.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/agents/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/agents/models.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/agents/registry.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/agent.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/providers/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/providers/anthropic.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/providers/gemini.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/providers/mock.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/providers/ollama.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/providers/openai.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/tools/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ai/tools/registry.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/api.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/auth/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/auth/cookies.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/auth/guards.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/auth/jwt.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/auth/middleware.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/auth/passwords.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/auth/user.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/__main__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/agents.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/ai.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/auth.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/dev.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/doctor.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/executions.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/generate.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/inspect.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/new.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/objects.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/recover.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/routes.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/scaffolding.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/schedules.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/tasks.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/terminal.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/theme.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/cli/version.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/components.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/config.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/app.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/errors.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/events.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/render.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/routing.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/sitemap.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/core/state.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/data/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/data/base.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/data/model.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/i18n.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/mcp/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/mcp/client.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/memory/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/memory/interfaces.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/memory/sqlite.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/mesh/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/mesh/client.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/mesh/event_bus.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/capability.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/compute.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/constraint.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/effect.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/intent.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/resource.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/state.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/primitives/time.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/py.typed +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/queue.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/routing/api.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/routing/pages.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/adaptive.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/capability.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/constraint.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/context.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/errors.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/execution.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/graph.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/persistence.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/planner.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/scheduler.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/task.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/runtime/workflow.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/schedule.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/security/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/security/cors.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/security/csrf.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/security/headers.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/security/passwords.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/security/rate_limit.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/seo.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/static/client.js +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/status.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/cache/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/cache/interfaces.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/cache/memory.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/cache/redis.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/database/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/database/interfaces.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/database/postgres.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/database/sqlite.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/events/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/events/interfaces.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/events/local.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/events/postgres.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/events/sqlite.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/execution/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/manager.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/objects/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/objects/interfaces.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/objects/local.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/objects/s3.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/queue/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/queue/interfaces.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/queue/memory.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/queue/postgres.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/queue/redis.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/queue/sqlite.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/scheduler/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/scheduler/sqlite.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/telemetry/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/telemetry/api.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/telemetry/middleware.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/telemetry/store.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/theme.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/tools/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/tools/registry.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/component.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/events.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/library.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/markdown.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/rendering.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/state.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/styles/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/styles/presets.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/ui/styles/theme.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/workers/__init__.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/workers/queue.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo_framework.egg-info/dependency_links.txt +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo_framework.egg-info/entry_points.txt +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo_framework.egg-info/requires.txt +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo_framework.egg-info/top_level.txt +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_agent.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_agent_registry.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_agent_runtime.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_app.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_auth.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_checkpoint_resume.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_chrome.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_cli.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_cli_inspect.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_components.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_config.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_contract_api.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_data.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_design_system.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_events.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_execution_sqlite_store.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_http_runtime.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_human.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_i18n.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_integration.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_mcp.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_mcp_runtime.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_memory.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_mesh.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_mesh_event_bus.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_model.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_object_store.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_persistence.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_planner_adaptive.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_primitives.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_provider_switching.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_providers.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_queue.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_reactive.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_runtime.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_scheduler.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_security.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_seo.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_state.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_theme.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_theme_presets.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_tools.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_ui.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_websocket.py +0 -0
- {voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/tests/test_workers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voodoo-framework
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: The programmable runtime for adaptive applications and operational systems — web, APIs, agents, workers, human workflows, distributed systems, and physical systems in one runtime.
|
|
5
5
|
Author-email: Helder Perez <contact@helderperez.com>
|
|
6
6
|
Maintainer-email: Helder Perez <contact@helderperez.com>
|
|
@@ -466,7 +466,7 @@ voodoo auth secret-key
|
|
|
466
466
|
|
|
467
467
|
## Project Status & Roadmap
|
|
468
468
|
|
|
469
|
-
Voodoo is in active development (
|
|
469
|
+
Voodoo is in active development (v2.0.0, Beta). The core runtime, UI system, AI agents, MCP, durable execution, adaptive runtime, model-provider protocol, layered memory, and durable agent registry are production-ready. PostgreSQL, S3/R2, and Redis adapters are shipped behind optional extras; durable human-in-the-loop is the current focus.
|
|
470
470
|
|
|
471
471
|
```mermaid
|
|
472
472
|
timeline
|
|
@@ -477,26 +477,20 @@ timeline
|
|
|
477
477
|
Unified Runtime : ExecutionEngine : Planner : Adaptive supervisor
|
|
478
478
|
Durable Local Runtime : SQLite queue : Executions : Scheduler : Objects : Events
|
|
479
479
|
Production Providers : PostgreSQL : S3/R2 : Redis
|
|
480
|
-
section
|
|
480
|
+
section Shipped (v1.16–v2.0)
|
|
481
481
|
ModelProvider protocol : Model descriptors : Routing aliases
|
|
482
482
|
Memory capability : Layered memory : SQLite + FTS5
|
|
483
|
-
Durable Agents
|
|
484
|
-
section
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
|
491
|
-
|
|
492
|
-
|
|
|
493
|
-
|
|
|
494
|
-
| Durable local runtime (zero-infra) — SQLite storage, queue, executions, scheduler, objects, events | v1.3–v1.9 | ✅ Shipped |
|
|
495
|
-
| Adapter contracts & runtime configuration | v1.10–v1.11 | ✅ Shipped |
|
|
496
|
-
| Production providers (optional) — PostgreSQL, S3/R2, Redis | v1.12–v1.15 | ✅ Shipped |
|
|
497
|
-
| AI runtime — model-provider protocol, memory, durable agents, durable HITL | v1.16–v1.19 | 🚧 In Progress |
|
|
498
|
-
| Capability security & secrets | v2.0 | 📋 Planned |
|
|
499
|
-
| Protocol stability & DX — observability, schemas, local runtime | v2.1–v2.3 | 📋 Planned |
|
|
483
|
+
Durable Agents : Agent registry : v2.0
|
|
484
|
+
section In Progress (v2.1)
|
|
485
|
+
Durable HITL : Resumable approvals
|
|
486
|
+
section Planned (v2.2+)
|
|
487
|
+
Capability Security & Secrets : v2.2
|
|
488
|
+
Observability & Protocol : v2.3–v2.4
|
|
489
|
+
Local Runtime DX : v2.5
|
|
490
|
+
| AI runtime — model-provider protocol, memory, durable agent registry | v1.16–v2.0 | ✅ Shipped |
|
|
491
|
+
| Durable human-in-the-loop — resumable approvals | v2.1 | 🚧 In Progress |
|
|
492
|
+
| Capability security & secrets | v2.2 | 📋 Planned |
|
|
493
|
+
| Protocol stability & DX — observability, schemas, local runtime | v2.3–v2.5 | 📋 Planned |
|
|
500
494
|
|
|
501
495
|
See the [master roadmap](ROADMAP.md) and [sprint plan](SPRINT_PLAN.md) for details.
|
|
502
496
|
|
|
@@ -403,7 +403,7 @@ voodoo auth secret-key
|
|
|
403
403
|
|
|
404
404
|
## Project Status & Roadmap
|
|
405
405
|
|
|
406
|
-
Voodoo is in active development (
|
|
406
|
+
Voodoo is in active development (v2.0.0, Beta). The core runtime, UI system, AI agents, MCP, durable execution, adaptive runtime, model-provider protocol, layered memory, and durable agent registry are production-ready. PostgreSQL, S3/R2, and Redis adapters are shipped behind optional extras; durable human-in-the-loop is the current focus.
|
|
407
407
|
|
|
408
408
|
```mermaid
|
|
409
409
|
timeline
|
|
@@ -414,26 +414,20 @@ timeline
|
|
|
414
414
|
Unified Runtime : ExecutionEngine : Planner : Adaptive supervisor
|
|
415
415
|
Durable Local Runtime : SQLite queue : Executions : Scheduler : Objects : Events
|
|
416
416
|
Production Providers : PostgreSQL : S3/R2 : Redis
|
|
417
|
-
section
|
|
417
|
+
section Shipped (v1.16–v2.0)
|
|
418
418
|
ModelProvider protocol : Model descriptors : Routing aliases
|
|
419
419
|
Memory capability : Layered memory : SQLite + FTS5
|
|
420
|
-
Durable Agents
|
|
421
|
-
section
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
|
428
|
-
|
|
429
|
-
|
|
|
430
|
-
|
|
|
431
|
-
| Durable local runtime (zero-infra) — SQLite storage, queue, executions, scheduler, objects, events | v1.3–v1.9 | ✅ Shipped |
|
|
432
|
-
| Adapter contracts & runtime configuration | v1.10–v1.11 | ✅ Shipped |
|
|
433
|
-
| Production providers (optional) — PostgreSQL, S3/R2, Redis | v1.12–v1.15 | ✅ Shipped |
|
|
434
|
-
| AI runtime — model-provider protocol, memory, durable agents, durable HITL | v1.16–v1.19 | 🚧 In Progress |
|
|
435
|
-
| Capability security & secrets | v2.0 | 📋 Planned |
|
|
436
|
-
| Protocol stability & DX — observability, schemas, local runtime | v2.1–v2.3 | 📋 Planned |
|
|
420
|
+
Durable Agents : Agent registry : v2.0
|
|
421
|
+
section In Progress (v2.1)
|
|
422
|
+
Durable HITL : Resumable approvals
|
|
423
|
+
section Planned (v2.2+)
|
|
424
|
+
Capability Security & Secrets : v2.2
|
|
425
|
+
Observability & Protocol : v2.3–v2.4
|
|
426
|
+
Local Runtime DX : v2.5
|
|
427
|
+
| AI runtime — model-provider protocol, memory, durable agent registry | v1.16–v2.0 | ✅ Shipped |
|
|
428
|
+
| Durable human-in-the-loop — resumable approvals | v2.1 | 🚧 In Progress |
|
|
429
|
+
| Capability security & secrets | v2.2 | 📋 Planned |
|
|
430
|
+
| Protocol stability & DX — observability, schemas, local runtime | v2.3–v2.5 | 📋 Planned |
|
|
437
431
|
|
|
438
432
|
See the [master roadmap](ROADMAP.md) and [sprint plan](SPRINT_PLAN.md) for details.
|
|
439
433
|
|
|
@@ -3,6 +3,7 @@ import typer
|
|
|
3
3
|
from voodoo.cli import (
|
|
4
4
|
agents,
|
|
5
5
|
ai,
|
|
6
|
+
approvals,
|
|
6
7
|
dev,
|
|
7
8
|
doctor,
|
|
8
9
|
executions,
|
|
@@ -38,6 +39,7 @@ app.add_typer(schedules.schedules_app, name="schedules")
|
|
|
38
39
|
app.add_typer(executions.executions_app, name="executions")
|
|
39
40
|
app.add_typer(objects.objects_app, name="objects")
|
|
40
41
|
app.add_typer(agents.agents_app, name="agents")
|
|
42
|
+
app.add_typer(approvals.approvals_app, name="approvals")
|
|
41
43
|
app.add_typer(theme.theme_app, name="theme")
|
|
42
44
|
# Top-level aliases per the Sprint 3 CLI surface: `voodoo execution <id>`
|
|
43
45
|
# (timeline from the journal) and `voodoo events` (recent journal events).
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""voodoo approvals — decide pending human approvals from any machine.
|
|
2
|
+
|
|
3
|
+
Sprint 18: human approval is an execution state that survives process
|
|
4
|
+
death. This CLI reads and decides approvals directly from the durable
|
|
5
|
+
execution store (SQLite by default), so a decision can be made on any
|
|
6
|
+
machine — the waiting execution resumes on whichever worker picks it up.
|
|
7
|
+
|
|
8
|
+
voodoo approvals list [--pending] [--json]
|
|
9
|
+
voodoo approvals show <execution-id> [--json]
|
|
10
|
+
voodoo approvals approve <execution-id> [--by NAME] [--note TEXT]
|
|
11
|
+
voodoo approvals deny <execution-id> [--by NAME] [--reason TEXT]
|
|
12
|
+
|
|
13
|
+
With ``--app`` (e.g. ``main:app``) the application module is imported
|
|
14
|
+
first so registered participants (agents, workflows) re-register and the
|
|
15
|
+
approved execution actually re-runs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import typer
|
|
25
|
+
|
|
26
|
+
from voodoo.cli import terminal
|
|
27
|
+
|
|
28
|
+
approvals_app = typer.Typer(
|
|
29
|
+
name="approvals",
|
|
30
|
+
help="List, approve, and deny human approvals.",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
rich_markup_mode="rich",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve_store(store_path: str | None):
|
|
37
|
+
"""Return the configured execution store (SQLite by default)."""
|
|
38
|
+
from voodoo.config import config
|
|
39
|
+
from voodoo.runtime.persistence import JSONFileExecutionStore
|
|
40
|
+
from voodoo.storage.execution import SQLiteExecutionStore
|
|
41
|
+
|
|
42
|
+
if store_path is None:
|
|
43
|
+
store_path = os.environ.get("VOODOO_EXECUTION_STORE", config.db_path).replace(
|
|
44
|
+
":memory:", ".voodoo/state/data.db"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if Path(store_path).suffix == ".jsonl":
|
|
48
|
+
return JSONFileExecutionStore(store_path), store_path
|
|
49
|
+
return SQLiteExecutionStore(store_path), store_path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_app(app_str: str | None) -> None:
|
|
53
|
+
"""Import the application module so participants re-register."""
|
|
54
|
+
if app_str is None:
|
|
55
|
+
return
|
|
56
|
+
import importlib
|
|
57
|
+
import sys
|
|
58
|
+
|
|
59
|
+
module_name, _, attr = app_str.partition(":")
|
|
60
|
+
attr = attr or "app"
|
|
61
|
+
sys.path.insert(0, os.getcwd())
|
|
62
|
+
mod = importlib.import_module(module_name)
|
|
63
|
+
getattr(mod, attr, None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _list_approvals(store, pending_only: bool) -> list[dict]:
|
|
67
|
+
"""Read approvals from a store, tolerating stores without the seam."""
|
|
68
|
+
if not hasattr(store, "load_approvals"):
|
|
69
|
+
return []
|
|
70
|
+
return store.load_approvals(pending_only=pending_only)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@approvals_app.command("list")
|
|
74
|
+
def list_approvals(
|
|
75
|
+
pending_only: bool = typer.Option(
|
|
76
|
+
False, "--pending", help="Only show pending approvals"
|
|
77
|
+
),
|
|
78
|
+
store_path: str = typer.Option(
|
|
79
|
+
None, "--store", help="Path to the execution store (SQLite by default)"
|
|
80
|
+
),
|
|
81
|
+
app_str: str = typer.Option(
|
|
82
|
+
None, "--app", help="App instance (e.g. main:app) to import first"
|
|
83
|
+
),
|
|
84
|
+
json_mode: bool = typer.Option(
|
|
85
|
+
False, "--json", help="Machine-readable JSON output"
|
|
86
|
+
),
|
|
87
|
+
):
|
|
88
|
+
"""List approvals — pending and decided."""
|
|
89
|
+
store, resolved = _resolve_store(store_path)
|
|
90
|
+
_load_app(app_str)
|
|
91
|
+
approvals = _list_approvals(store, pending_only)
|
|
92
|
+
|
|
93
|
+
if json_mode or terminal.is_json_mode():
|
|
94
|
+
terminal.json_output({"store": resolved, "approvals": approvals})
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
terminal.wordmark()
|
|
98
|
+
terminal.blank()
|
|
99
|
+
if not approvals:
|
|
100
|
+
terminal.muted(
|
|
101
|
+
f"no {'pending ' if pending_only else ''}approvals in {resolved}"
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
for a in approvals:
|
|
105
|
+
terminal.info(
|
|
106
|
+
f" {str(a.get('id', ''))[:8]} {(a.get('question') or '-')[:60]}"
|
|
107
|
+
)
|
|
108
|
+
terminal.muted(
|
|
109
|
+
f" execution: {str(a.get('execution_id', ''))[:8]}"
|
|
110
|
+
f" status: {a.get('status', '-')}"
|
|
111
|
+
)
|
|
112
|
+
terminal.muted(
|
|
113
|
+
f" requested_by: {a.get('requested_by') or '-'}"
|
|
114
|
+
f" decided_by: {a.get('decided_by') or '-'}"
|
|
115
|
+
)
|
|
116
|
+
terminal.blank()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@approvals_app.command("show")
|
|
120
|
+
def show_approval(
|
|
121
|
+
execution_id: str = typer.Argument(..., help="Execution id of the approval"),
|
|
122
|
+
store_path: str = typer.Option(
|
|
123
|
+
None, "--store", help="Path to the execution store (SQLite by default)"
|
|
124
|
+
),
|
|
125
|
+
json_mode: bool = typer.Option(
|
|
126
|
+
False, "--json", help="Machine-readable JSON output"
|
|
127
|
+
),
|
|
128
|
+
):
|
|
129
|
+
"""Show one approval in detail."""
|
|
130
|
+
store, resolved = _resolve_store(store_path)
|
|
131
|
+
approval = (
|
|
132
|
+
store.load_approval(execution_id) if hasattr(store, "load_approval") else None
|
|
133
|
+
)
|
|
134
|
+
if approval is None:
|
|
135
|
+
terminal.error(
|
|
136
|
+
f"approval for execution '{execution_id}' not found in {resolved}"
|
|
137
|
+
)
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
if json_mode or terminal.is_json_mode():
|
|
141
|
+
terminal.json_output(approval)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
terminal.wordmark()
|
|
145
|
+
terminal.blank()
|
|
146
|
+
terminal.status_block(
|
|
147
|
+
[
|
|
148
|
+
("id", str(approval.get("id", ""))),
|
|
149
|
+
("execution", str(approval.get("execution_id", ""))),
|
|
150
|
+
("trace", str(approval.get("trace_id", "") or "-")),
|
|
151
|
+
("status", approval.get("status", "-")),
|
|
152
|
+
("requested_by", approval.get("requested_by") or "-"),
|
|
153
|
+
("decided_by", approval.get("decided_by") or "-"),
|
|
154
|
+
("decided_at", approval.get("decided_at") or "-"),
|
|
155
|
+
("participant", approval.get("participant") or "-"),
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
question = approval.get("question")
|
|
159
|
+
if question:
|
|
160
|
+
terminal.blank()
|
|
161
|
+
terminal.label_value("question", question)
|
|
162
|
+
reason = approval.get("reason")
|
|
163
|
+
if reason:
|
|
164
|
+
terminal.label_value("reason", reason)
|
|
165
|
+
terminal.blank()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _decide(
|
|
169
|
+
execution_id: str,
|
|
170
|
+
*,
|
|
171
|
+
decision: str,
|
|
172
|
+
by: str,
|
|
173
|
+
message: str | None,
|
|
174
|
+
store_path: str | None,
|
|
175
|
+
app_str: str | None,
|
|
176
|
+
json_mode: bool,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Shared approve/deny path: recover waiting executions, then decide."""
|
|
179
|
+
from voodoo.runtime.engine import engine as runtime_engine
|
|
180
|
+
|
|
181
|
+
store, resolved = _resolve_store(store_path)
|
|
182
|
+
runtime_engine.use_store(store)
|
|
183
|
+
# Restore waiting executions + rehydrate persisted approvals so the
|
|
184
|
+
# decision applies to the durable record — not just this process.
|
|
185
|
+
runtime_engine.recover()
|
|
186
|
+
_load_app(app_str)
|
|
187
|
+
|
|
188
|
+
if runtime_engine.approvals.get(execution_id) is None:
|
|
189
|
+
if json_mode or terminal.is_json_mode():
|
|
190
|
+
terminal.json_output(
|
|
191
|
+
{
|
|
192
|
+
"error": "approval not found",
|
|
193
|
+
"execution_id": execution_id,
|
|
194
|
+
"store": resolved,
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
terminal.error(f"approval for execution '{execution_id}' not found")
|
|
199
|
+
raise typer.Exit(1)
|
|
200
|
+
|
|
201
|
+
decide_fn = runtime_engine.approve if decision == "approve" else runtime_engine.deny
|
|
202
|
+
kwargs = {}
|
|
203
|
+
if decision == "approve":
|
|
204
|
+
kwargs["note"] = message
|
|
205
|
+
else:
|
|
206
|
+
kwargs["reason"] = message or "denied"
|
|
207
|
+
executed = asyncio.run(decide_fn(execution_id, by=by, **kwargs))
|
|
208
|
+
|
|
209
|
+
if json_mode or terminal.is_json_mode():
|
|
210
|
+
terminal.json_output(
|
|
211
|
+
{
|
|
212
|
+
"store": resolved,
|
|
213
|
+
"decision": decision,
|
|
214
|
+
"execution_id": execution_id,
|
|
215
|
+
"by": by,
|
|
216
|
+
"resumed": executed.describe() if executed is not None else None,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
terminal.wordmark()
|
|
222
|
+
terminal.blank()
|
|
223
|
+
icon = "✓" if decision == "approve" else "✗"
|
|
224
|
+
terminal.info(f" {icon} {decision}d execution {execution_id[:8]} by {by}")
|
|
225
|
+
if executed is not None:
|
|
226
|
+
terminal.muted(f" execution → {executed.status.value}")
|
|
227
|
+
else:
|
|
228
|
+
terminal.muted(" execution not found in engine (record updated only)")
|
|
229
|
+
terminal.blank()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@approvals_app.command("approve")
|
|
233
|
+
def approve_cmd(
|
|
234
|
+
execution_id: str = typer.Argument(..., help="Execution id to approve"),
|
|
235
|
+
by: str = typer.Option("human", "--by", help="Who is approving"),
|
|
236
|
+
note: str = typer.Option(None, "--note", help="Optional note"),
|
|
237
|
+
store_path: str = typer.Option(
|
|
238
|
+
None, "--store", help="Path to the execution store (SQLite by default)"
|
|
239
|
+
),
|
|
240
|
+
app_str: str = typer.Option(
|
|
241
|
+
None, "--app", help="App instance (e.g. main:app) to import first"
|
|
242
|
+
),
|
|
243
|
+
json_mode: bool = typer.Option(
|
|
244
|
+
False, "--json", help="Machine-readable JSON output"
|
|
245
|
+
),
|
|
246
|
+
):
|
|
247
|
+
"""Approve a waiting execution — it resumes on a worker."""
|
|
248
|
+
_decide(
|
|
249
|
+
execution_id,
|
|
250
|
+
decision="approve",
|
|
251
|
+
by=by,
|
|
252
|
+
message=note,
|
|
253
|
+
store_path=store_path,
|
|
254
|
+
app_str=app_str,
|
|
255
|
+
json_mode=json_mode,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@approvals_app.command("deny")
|
|
260
|
+
def deny_cmd(
|
|
261
|
+
execution_id: str = typer.Argument(..., help="Execution id to deny"),
|
|
262
|
+
by: str = typer.Option("human", "--by", help="Who is denying"),
|
|
263
|
+
reason: str = typer.Option(None, "--reason", help="Denial reason"),
|
|
264
|
+
store_path: str = typer.Option(
|
|
265
|
+
None, "--store", help="Path to the execution store (SQLite by default)"
|
|
266
|
+
),
|
|
267
|
+
app_str: str = typer.Option(
|
|
268
|
+
None, "--app", help="App instance (e.g. main:app) to import first"
|
|
269
|
+
),
|
|
270
|
+
json_mode: bool = typer.Option(
|
|
271
|
+
False, "--json", help="Machine-readable JSON output"
|
|
272
|
+
),
|
|
273
|
+
):
|
|
274
|
+
"""Deny a waiting execution — it fails with the denial reason."""
|
|
275
|
+
_decide(
|
|
276
|
+
execution_id,
|
|
277
|
+
decision="deny",
|
|
278
|
+
by=by,
|
|
279
|
+
message=reason,
|
|
280
|
+
store_path=store_path,
|
|
281
|
+
app_str=app_str,
|
|
282
|
+
json_mode=json_mode,
|
|
283
|
+
)
|
|
@@ -117,6 +117,10 @@ class ExecutionEngine:
|
|
|
117
117
|
self.approvals = ApprovalRegistry()
|
|
118
118
|
self._execution_store: Any = None
|
|
119
119
|
self._checkpoint_sequences: dict[str, int] = {}
|
|
120
|
+
#: Durable compute registry — named participants that can be
|
|
121
|
+
#: re-resolved after a restart (Sprint 18). Maps participant
|
|
122
|
+
#: name → compute callable + metadata.
|
|
123
|
+
self._participants: dict[str, Any] = {}
|
|
120
124
|
|
|
121
125
|
# -- persistence / recovery ---------------------------------------------
|
|
122
126
|
|
|
@@ -124,6 +128,35 @@ class ExecutionEngine:
|
|
|
124
128
|
"""Attach an ExecutionStore (persistence seam, Phase 11)."""
|
|
125
129
|
self._execution_store = store
|
|
126
130
|
|
|
131
|
+
def register_participant(
|
|
132
|
+
self,
|
|
133
|
+
name: str,
|
|
134
|
+
compute: ComputeFn | None = None,
|
|
135
|
+
*,
|
|
136
|
+
execute: Any | None = None,
|
|
137
|
+
kind: str = "compute",
|
|
138
|
+
capabilities: list[str] | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Register a named compute participant for durable resume (Sprint 18).
|
|
141
|
+
|
|
142
|
+
Participants registered here can be re-resolved by name after a
|
|
143
|
+
process restart, making ``WAITING_FOR_HUMAN`` executions fully
|
|
144
|
+
resumable on any worker (ROADMAP §50).
|
|
145
|
+
"""
|
|
146
|
+
self._participants[name] = {
|
|
147
|
+
"compute": compute,
|
|
148
|
+
"execute": execute,
|
|
149
|
+
"kind": kind,
|
|
150
|
+
"capabilities": capabilities or [],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def resolve_participant(self, name: str) -> Any | None:
|
|
154
|
+
"""Resolve a registered participant by name.
|
|
155
|
+
|
|
156
|
+
Returns the registration dict, or ``None`` when not registered.
|
|
157
|
+
"""
|
|
158
|
+
return self._participants.get(name)
|
|
159
|
+
|
|
127
160
|
def _persist(self, execution: Execution) -> None:
|
|
128
161
|
"""Checkpoint an execution — raises on failure (spec §51.16)."""
|
|
129
162
|
if self._execution_store is None:
|
|
@@ -253,6 +286,7 @@ class ExecutionEngine:
|
|
|
253
286
|
else None
|
|
254
287
|
),
|
|
255
288
|
reason=record["reason"],
|
|
289
|
+
participant=record.get("participant"),
|
|
256
290
|
)
|
|
257
291
|
self.approvals.records[execution.id] = approval
|
|
258
292
|
|
|
@@ -273,7 +307,7 @@ class ExecutionEngine:
|
|
|
273
307
|
self._persist_approval(approval)
|
|
274
308
|
self._journal_approval_decision(
|
|
275
309
|
execution_id,
|
|
276
|
-
"
|
|
310
|
+
"approval.granted",
|
|
277
311
|
{"by": by, "capability": approval.capability},
|
|
278
312
|
)
|
|
279
313
|
waiting = self.executions.get(execution_id)
|
|
@@ -283,6 +317,16 @@ class ExecutionEngine:
|
|
|
283
317
|
"human.approved",
|
|
284
318
|
{"execution_id": execution_id, "by": by, "capability": approval.capability},
|
|
285
319
|
)
|
|
320
|
+
|
|
321
|
+
# Durable resume (Sprint 18): when the live compute is gone (e.g.
|
|
322
|
+
# after a restart) but a registered participant exists, re-resolve
|
|
323
|
+
# the compute from the participant registry. The waiting execution's
|
|
324
|
+
# persisted intent supplies the outcome when the approval record
|
|
325
|
+
# has none (a restarted process serializes no live objects).
|
|
326
|
+
if approval.compute is None and approval.participant is not None:
|
|
327
|
+
approval.compute = self._participant_compute(approval)
|
|
328
|
+
if approval.intent is None and waiting is not None:
|
|
329
|
+
approval.intent = waiting.intent
|
|
286
330
|
if approval.compute is None or approval.intent is None:
|
|
287
331
|
if waiting is not None:
|
|
288
332
|
waiting.complete(result={"approved": True, "by": by})
|
|
@@ -310,6 +354,15 @@ class ExecutionEngine:
|
|
|
310
354
|
waiting.complete(result=resumed.result)
|
|
311
355
|
return resumed
|
|
312
356
|
|
|
357
|
+
def _participant_compute(self, approval: Any) -> ComputeFn | None:
|
|
358
|
+
"""Synchronously re-resolve compute from the participant registry."""
|
|
359
|
+
if approval.participant is None:
|
|
360
|
+
return None
|
|
361
|
+
participant = self.resolve_participant(approval.participant)
|
|
362
|
+
if participant is None:
|
|
363
|
+
return None
|
|
364
|
+
return participant["compute"]
|
|
365
|
+
|
|
313
366
|
async def deny(
|
|
314
367
|
self, execution_id: str, *, by: str = "human", reason: str = "denied"
|
|
315
368
|
) -> Execution | None:
|
|
@@ -324,7 +377,7 @@ class ExecutionEngine:
|
|
|
324
377
|
self._persist_approval(approval)
|
|
325
378
|
self._journal_approval_decision(
|
|
326
379
|
execution_id,
|
|
327
|
-
"
|
|
380
|
+
"approval.denied",
|
|
328
381
|
{"by": by, "reason": reason},
|
|
329
382
|
)
|
|
330
383
|
waiting = self.executions.get(execution_id)
|
|
@@ -526,6 +579,15 @@ class ExecutionEngine:
|
|
|
526
579
|
# Persist the pending approval so a restart can rehydrate it
|
|
527
580
|
# (spec §30 — decisions recorded as journal events on decide).
|
|
528
581
|
self._persist_approval(approval)
|
|
582
|
+
self._journal_approval_decision(
|
|
583
|
+
execution.id,
|
|
584
|
+
"approval.requested",
|
|
585
|
+
{
|
|
586
|
+
"capability": exc.context.get("capability"),
|
|
587
|
+
"question": exc.message,
|
|
588
|
+
"requested_by": execution.actor,
|
|
589
|
+
},
|
|
590
|
+
)
|
|
529
591
|
await self._emit(
|
|
530
592
|
"human.approval_required",
|
|
531
593
|
{
|
|
@@ -54,6 +54,12 @@ class Approval:
|
|
|
54
54
|
id: str = field(default_factory=lambda: str(uuid4()))
|
|
55
55
|
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
56
56
|
|
|
57
|
+
#: Registered participant name — the durable handle to what re-runs
|
|
58
|
+
#: once approved (resolved through the engine's planner registry;
|
|
59
|
+
#: Sprint 18). Takes precedence over the live ``compute`` callable
|
|
60
|
+
#: when resuming after a restart.
|
|
61
|
+
participant: str | None = None
|
|
62
|
+
|
|
57
63
|
# resumable payload — what to re-run once approved
|
|
58
64
|
intent: Intent | None = None
|
|
59
65
|
compute: ComputeFn | None = None
|
|
@@ -96,6 +102,7 @@ class ApprovalRegistry:
|
|
|
96
102
|
compute: ComputeFn | None = None,
|
|
97
103
|
output_type: type | None = None,
|
|
98
104
|
context: ExecutionContext | None = None,
|
|
105
|
+
participant: str | None = None,
|
|
99
106
|
) -> Approval:
|
|
100
107
|
approval = Approval(
|
|
101
108
|
execution_id=execution.id,
|
|
@@ -112,6 +119,7 @@ class ApprovalRegistry:
|
|
|
112
119
|
compute=compute,
|
|
113
120
|
output_type=output_type,
|
|
114
121
|
context=context,
|
|
122
|
+
participant=participant,
|
|
115
123
|
)
|
|
116
124
|
self.records[execution.id] = approval
|
|
117
125
|
return approval
|
{voodoo_framework-2.0.0 → voodoo_framework-2.1.0}/src/voodoo/storage/execution/migrations.py
RENAMED
|
@@ -15,7 +15,8 @@ from voodoo.storage.database.interfaces import Migration
|
|
|
15
15
|
from voodoo.storage.database.sqlite import register_framework_migration
|
|
16
16
|
|
|
17
17
|
# Versions follow the global framework namespace: … 6 events, 7 artifacts,
|
|
18
|
-
# 8 approvals
|
|
18
|
+
# 8 approvals (Sprint 18 added the `participant` column to the v8 CREATE —
|
|
19
|
+
# existing databases add it via a guarded ALTER in each store's setup).
|
|
19
20
|
EXECUTION_ARTIFACTS_MIGRATION_VERSION = 7
|
|
20
21
|
EXECUTION_APPROVALS_MIGRATION_VERSION = 8
|
|
21
22
|
|
|
@@ -47,6 +48,7 @@ EXECUTION_APPROVALS_MIGRATION = Migration(
|
|
|
47
48
|
name="execution_approvals",
|
|
48
49
|
statements=(
|
|
49
50
|
# Approvals (Sprint 4) — pending human decisions survive restarts.
|
|
51
|
+
# `participant` (Sprint 18) — durable compute handle for resume.
|
|
50
52
|
"""
|
|
51
53
|
CREATE TABLE IF NOT EXISTS approvals (
|
|
52
54
|
id TEXT PRIMARY KEY,
|
|
@@ -59,7 +61,8 @@ EXECUTION_APPROVALS_MIGRATION = Migration(
|
|
|
59
61
|
decided_by TEXT,
|
|
60
62
|
decided_at TEXT,
|
|
61
63
|
reason TEXT,
|
|
62
|
-
created_at TEXT NOT NULL
|
|
64
|
+
created_at TEXT NOT NULL,
|
|
65
|
+
participant TEXT
|
|
63
66
|
)
|
|
64
67
|
""",
|
|
65
68
|
"CREATE INDEX IF NOT EXISTS idx_approvals_execution "
|
|
@@ -81,6 +81,17 @@ class PostgresExecutionStore:
|
|
|
81
81
|
cur.execute(
|
|
82
82
|
_translate("ALTER TABLE executions ADD COLUMN checkpoint TEXT")
|
|
83
83
|
)
|
|
84
|
+
# Sprint 18: durable participant column for approvals — guarded
|
|
85
|
+
# because databases created at v8 already lack it.
|
|
86
|
+
with self._conn.cursor() as cur:
|
|
87
|
+
cur.execute(
|
|
88
|
+
"SELECT column_name FROM information_schema.columns "
|
|
89
|
+
"WHERE table_name = 'approvals' AND column_name = 'participant'"
|
|
90
|
+
)
|
|
91
|
+
if cur.fetchone() is None:
|
|
92
|
+
cur.execute(
|
|
93
|
+
_translate("ALTER TABLE approvals ADD COLUMN participant TEXT")
|
|
94
|
+
)
|
|
84
95
|
self._conn.commit()
|
|
85
96
|
|
|
86
97
|
# -- ExecutionStore protocol (sync) ------------------------------------
|
|
@@ -230,13 +241,14 @@ class PostgresExecutionStore:
|
|
|
230
241
|
_translate(
|
|
231
242
|
"INSERT INTO approvals ("
|
|
232
243
|
"id, execution_id, trace_id, capability, question, requested_by, "
|
|
233
|
-
"status, decided_by, decided_at, reason, created_at"
|
|
234
|
-
") VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) "
|
|
244
|
+
"status, decided_by, decided_at, reason, created_at, participant"
|
|
245
|
+
") VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) "
|
|
235
246
|
"ON CONFLICT (id) DO UPDATE SET "
|
|
236
247
|
"status = excluded.status, "
|
|
237
248
|
"decided_by = excluded.decided_by, "
|
|
238
249
|
"decided_at = excluded.decided_at, "
|
|
239
|
-
"reason = excluded.reason"
|
|
250
|
+
"reason = excluded.reason, "
|
|
251
|
+
"participant = excluded.participant"
|
|
240
252
|
),
|
|
241
253
|
(
|
|
242
254
|
approval.id,
|
|
@@ -252,6 +264,7 @@ class PostgresExecutionStore:
|
|
|
252
264
|
approval.created_at.isoformat()
|
|
253
265
|
if approval.created_at
|
|
254
266
|
else datetime.now(UTC).isoformat(),
|
|
267
|
+
getattr(approval, "participant", None),
|
|
255
268
|
),
|
|
256
269
|
)
|
|
257
270
|
self._conn.commit()
|
|
@@ -268,6 +281,17 @@ class PostgresExecutionStore:
|
|
|
268
281
|
row = cur.fetchone()
|
|
269
282
|
return dict(row) if row is not None else None
|
|
270
283
|
|
|
284
|
+
def load_approvals(self, pending_only: bool = False) -> list[dict[str, Any]]:
|
|
285
|
+
"""List approvals — newest first, optionally pending only (Sprint 18)."""
|
|
286
|
+
query = "SELECT * FROM approvals"
|
|
287
|
+
if pending_only:
|
|
288
|
+
query += " WHERE status = 'pending'"
|
|
289
|
+
query += " ORDER BY created_at DESC"
|
|
290
|
+
with self._lock, self._conn.cursor() as cur:
|
|
291
|
+
cur.execute(_translate(query))
|
|
292
|
+
rows = cur.fetchall()
|
|
293
|
+
return [dict(row) for row in rows]
|
|
294
|
+
|
|
271
295
|
# -- internals ---------------------------------------------------------
|
|
272
296
|
|
|
273
297
|
def _upsert_materialized(self, execution: Execution) -> None:
|