by-framework 0.2.2.dev11__tar.gz → 0.2.2.dev13__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.
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/CLAUDE.md +1 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/PKG-INFO +3 -3
- by_framework-0.2.2.dev13/docs/architecture/KEY_FILES.md +483 -0
- by_framework-0.2.2.dev13/docs/architecture/suspend-resume-liveness.md +385 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-adk/src/by_framework_adk/adapter.py +28 -3
- by_framework-0.2.2.dev13/libs/by-framework-adk/tests/test_adapter.py +129 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/README.md +119 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/examples/01_basic_chat.py +96 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/examples/02_tools_and_ask_user.py +172 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/examples/03_agent_delegation.py +168 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/examples/04_task_group.py +223 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/examples/05_real_model_react.py +284 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/examples/README.md +41 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/examples/_infra.py +253 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/pyproject.toml +35 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/src/by_framework_agent/__init__.py +16 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/src/by_framework_agent/litellm_client.py +85 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/src/by_framework_agent/loop.py +814 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/src/by_framework_agent/model_client.py +45 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/src/by_framework_agent/testing.py +37 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/src/by_framework_agent/tool_spec.py +41 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/src/by_framework_agent/worker.py +81 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/tests/conftest.py +109 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/tests/test_agent_as_tool.py +243 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/tests/test_ask_user_suspend_resume.py +182 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/tests/test_cancel_cleanup.py +212 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/tests/test_native_agent_worker.py +314 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/tests/test_task_group_batching.py +339 -0
- by_framework-0.2.2.dev13/libs/by-framework-agent/tests/test_tool_calling_loop.py +331 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/src/by_framework_dashboard/dashboard.py +10 -1
- by_framework-0.2.2.dev13/libs/by-framework-langgraph/README.md +89 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/src/by_framework_langgraph/adapter.py +49 -4
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/tests/test_adapter.py +207 -8
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/pyproject.toml +3 -2
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/client/client.py +4 -3
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/common/constants.py +243 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/event_type.py +5 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/registry.py +35 -0
- by_framework-0.2.2.dev13/src/by_framework/core/wait_gate.py +209 -0
- by_framework-0.2.2.dev13/src/by_framework/core/wait_index.py +165 -0
- by_framework-0.2.2.dev13/src/by_framework/core/wait_reply.py +167 -0
- by_framework-0.2.2.dev13/src/by_framework/core/wait_sweeper.py +1126 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/metrics/snapshot.py +34 -3
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/context.py +400 -108
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/processor.py +113 -9
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/runner.py +73 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/worker.py +239 -13
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/test_registry.py +5 -0
- by_framework-0.2.2.dev13/tests/core/test_wait_gate.py +427 -0
- by_framework-0.2.2.dev13/tests/core/test_wait_index.py +133 -0
- by_framework-0.2.2.dev13/tests/core/test_wait_sweeper.py +700 -0
- by_framework-0.2.2.dev13/tests/integration/test_nested_chain.py +719 -0
- by_framework-0.2.2.dev13/tests/integration/test_orphan_recovery.py +1263 -0
- by_framework-0.2.2.dev13/tests/integration/test_scatter_gather.py +857 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_context.py +725 -1
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_gateway_worker.py +447 -0
- by_framework-0.2.2.dev13/tests/worker/test_processor.py +315 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_runner.py +252 -4
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/uv.lock +430 -2
- by_framework-0.2.2.dev11/docs/architecture/KEY_FILES.md +0 -186
- by_framework-0.2.2.dev11/libs/by-framework-langgraph/README.md +0 -46
- by_framework-0.2.2.dev11/tests/integration/test_scatter_gather.py +0 -68
- by_framework-0.2.2.dev11/tests/worker/test_processor.py +0 -132
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.codex/hooks.json +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.dockerignore +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/RELEASING.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/workflows/ci.yml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/workflows/deploy-smoke-test.yml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/workflows/publish.yml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/workflows/stale.yml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.github/workflows/verify.yml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.gitignore +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/.pre-commit-config.yaml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/AGENTS.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/CHANGELOG.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/CODE_OF_CONDUCT.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/CONTRIBUTING.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/LICENSE +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/Makefile +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/README_zh.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/SECURITY.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/assets/img/architecture_en.png +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/assets/img/architecture_zh.png +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/autoformat.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/deploy/Dockerfile +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/deploy/docker-compose.yml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/deploy/entrypoint.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/deploy/kubernetes/worker-deployment.yaml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/architecture/GUARD_AUTHORING.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/architecture/production-deployment.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/architecture/redis-cluster-mode.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/architecture/worker-readiness-endpoint.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/by-framework-trace-change-usage.html +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/by-framework-trace-complete-guide.html +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/plans/2026-05-15-worker-task-state-stats.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/plans/2026-06-10-trace-metrics-correlation.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/docs/worker_management.html +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/examples/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/examples/echo_worker.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/examples/send_and_verify.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-adk/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-adk/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-adk/src/by_framework_adk/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-adk/src/by_framework_adk/_utils.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-adk/src/by_framework_adk/worker.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-adk/tests/test_worker.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/Dockerfile +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/frontend/index.html +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/frontend/package-lock.json +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/frontend/package.json +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/frontend/src/main.jsx +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/frontend/src/styles.css +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/frontend/vite.config.js +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/src/by_framework_dashboard/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/src/by_framework_dashboard/adapters.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/src/by_framework_dashboard/static/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/src/by_framework_dashboard/static/app.js +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/src/by_framework_dashboard/static/index.html +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/src/by_framework_dashboard/static/styles.css +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-dashboard/tests/test_dashboard_server.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-byclaw/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-byclaw/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-byclaw/src/by_framework_history_byclaw/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-byclaw/src/by_framework_history_byclaw/byclaw_history.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-byclaw/tests/test_byclaw_history.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-postgres/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-postgres/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-postgres/src/by_framework_history_postgres/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-postgres/src/by_framework_history_postgres/postgres.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-history-postgres/tests/test_postgres_history_storage.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/src/by_framework_langgraph/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/src/by_framework_langgraph/_utils.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/src/by_framework_langgraph/tools.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/src/by_framework_langgraph/worker.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/tests/test_tools.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-langgraph/tests/test_utils.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-langfuse/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-langfuse/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-langfuse/src/by_framework_trace_langfuse/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-langfuse/src/by_framework_trace_langfuse/langfuse.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-langfuse/tests/test_langfuse_import.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-phoenix/README.md +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-phoenix/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-phoenix/src/by_framework_trace_phoenix/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-phoenix/src/by_framework_trace_phoenix/phoenix.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-phoenix/tests/test_phoenix_import.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-phoenix/tests/test_phoenix_plugin.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-query/pyproject.toml +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-query/src/by_framework_trace_query/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-query/src/by_framework_trace_query/client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-query/src/by_framework_trace_query/merger.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-query/src/by_framework_trace_query/models.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-query/src/by_framework_trace_query/redis_source.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/libs/by-framework-trace-query/tests/test_trace_query_sdk.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/pylintrc +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/.map.conf +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/check-doc-discipline.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/check-entry-freshness.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/check-map-territory.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/map-stop-hook.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/python_quality.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/test-growth-ladder.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/scripts/verify.sh +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/__main__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/admin/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/admin/cli.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/admin/worker_manager.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/client/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/client/byai_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/common/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/common/config.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/common/emitter.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/common/exceptions.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/common/logger.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/common/redis_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/availability.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/delivery_gate.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/discovery.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/extensions/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/extensions/agent_config.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/extensions/agent_config_audit.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/extensions/plugin.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/extensions/registry.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/extensions/trace_provider.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/action_type.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/agent_state.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/byai_codec.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/byai_command.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/byai_types.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/commands.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/content_codec.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/content_type.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/data_message.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/data_shapes.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/events.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/message.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/message_header.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/responses.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/protocol/results.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/agent_config_manager.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/agent_runtime_state.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/file_manager.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/file_paths.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/file_permissions.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/filestore/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/filestore/base.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/filestore/local.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/history/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/history/base.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/history/history_manager.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/history/in_memory.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/runtime/session_manager.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/wakeup_controller.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/core/workspace.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/errors/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/errors/base.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/errors/common.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/errors/execution.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/errors/http.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/errors/protocol.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/errors/registry.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/metrics/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/metrics/catalog.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/metrics/collector.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/metrics/read_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/trace/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/trace/external_trace.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/trace/span_recorder.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/trace/trace_schema.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/trace/trace_writer.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/util/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/util/discovery_http_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/util/generate_message_id.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/util/http_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/_control_handling.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/_execution_tracking.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/_message_processing.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/app.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/byai_context.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/byai_worker.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/health_server.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/heartbeat.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/sandbox/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/src/by_framework/worker/sandbox/hook_sandbox.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/admin/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/admin/test_cli.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/client/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/client/test_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/common/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/common/test_config.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/common/test_constants.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/common/test_exceptions.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/common/test_logger.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/common/test_redis_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/conftest.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/protocol/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/protocol/test_byai_codec.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/protocol/test_command_wire.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/protocol/test_protocol.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/protocol/test_results.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/history/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/history/test_history_persistence.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_file_access_context.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_file_manager_default_storage.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_file_paths.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_file_permissions.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_filestore_local_collection.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_filestore_local_mutation.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_filestore_local_read.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/runtime/test_filestore_local_search.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/test_agent_config_audit.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/test_availability.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/test_discovery.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/core/test_worker_manager.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/integration/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/integration/test_ask_user_flow.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/integration/test_callback_flow.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/integration/test_logger_integration.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/metrics/test_catalog.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/metrics/test_collector.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/metrics/test_metrics.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/metrics/test_read_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/metrics/test_snapshot.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/plugin/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/plugin/test_langfuse_plugin.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/plugin/test_plugin_discovery.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/plugin/test_plugin_improvements.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/plugin/test_plugin_registry.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/plugin/test_plugin_system.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/test_main.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/trace/test_external_trace.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/trace/test_logger_correlation.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/trace/test_otel_exporter.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/trace/test_span_recorder.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/trace/test_trace_writer.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/util/test_discovery_http_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/util/test_discovery_http_client_download.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/util/test_discovery_http_client_upload.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/util/test_http_client.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/__init__.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_admin_control_handling.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_app.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_byai_worker.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_control_handling.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_emitter.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_fetch_messages.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_health_server.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_heartbeat.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_message_processing.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_sandbox.py +0 -0
- {by_framework-0.2.2.dev11 → by_framework-0.2.2.dev13}/tests/worker/test_workspace.py +0 -0
|
@@ -165,6 +165,7 @@ Pre-commit hooks are configured in `.pre-commit-config.yaml` and run isort, ruff
|
|
|
165
165
|
| Redis connection setup, cluster-mode, or key-schema versioning (`RedisConfig`, `RedisKeys`, `_get_redis()`, admin-index writes) | `docs/architecture/redis-cluster-mode.md` |
|
|
166
166
|
| Worker deployment/production-readiness — README's 部署 section, `__main__.py` CLI flags, `run_worker()`'s signature, or shutdown/signal handling | `docs/architecture/production-deployment.md` |
|
|
167
167
|
| Worker readiness/health-check endpoint (`WorkerHealthServer`, `/readyz`, `--health-port`) — building it, or touching anything that changes what "ready" means | `docs/architecture/worker-readiness-endpoint.md` |
|
|
168
|
+
| suspend/resume liveness — the wait index, its idempotency gate, or the sweep that resolves callers whose reply never arrives | `docs/architecture/suspend-resume-liveness.md` |
|
|
168
169
|
|
|
169
170
|
## Maintaining this map
|
|
170
171
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: by-framework
|
|
3
|
-
Version: 0.2.2.
|
|
3
|
+
Version: 0.2.2.dev13
|
|
4
4
|
Summary: 分布式 Agent 调度框架
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Requires-Dist: dill>=0.4.1
|
|
8
8
|
Requires-Dist: httpx>=0.28.1
|
|
9
|
-
Requires-Dist: redis
|
|
9
|
+
Requires-Dist: redis==7.4.0
|
|
10
10
|
Requires-Dist: typing-extensions>=4.0.0
|
|
11
11
|
Provides-Extra: cli
|
|
12
12
|
Requires-Dist: rich>=13.0.0; extra == 'cli'
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# Key files — per-file index
|
|
2
|
+
|
|
3
|
+
On-demand reference, routed from CLAUDE.md's Reference map.
|
|
4
|
+
**Read a file's entry before editing that file. After editing, rewrite the
|
|
5
|
+
entry in place if behaviour changed.**
|
|
6
|
+
|
|
7
|
+
Entries describe CURRENT behaviour + load-bearing invariants only. Release
|
|
8
|
+
history lives in the changelog + git, never here — enforced by
|
|
9
|
+
`scripts/check-doc-discipline.sh`.
|
|
10
|
+
|
|
11
|
+
**Two doc types share this on-demand layer.** This index holds one entry per
|
|
12
|
+
file (single-file invariants). Knowledge about how *several* files interact —
|
|
13
|
+
or an invariant that would otherwise repeat across many entries — belongs in a
|
|
14
|
+
**subsystem doc** (`docs/architecture/<x>.md`, routed by a Reference-map row),
|
|
15
|
+
not smeared across entries. Lift it up when it spans files; see "Diarize a
|
|
16
|
+
subsystem".
|
|
17
|
+
|
|
18
|
+
## Diarize a file
|
|
19
|
+
|
|
20
|
+
An entry is a **diarization**: read many sources, write one page of judgement.
|
|
21
|
+
The move — whether you're seeding this index or the growth guard just flagged a
|
|
22
|
+
stale entry — is always the same:
|
|
23
|
+
|
|
24
|
+
1. **Read three sources**: the file, its tests, and the last ~10 commits that
|
|
25
|
+
touched it (`git log -p -10 -- <file>`). The odd, specific test assertions
|
|
26
|
+
and the fix commits are where invariants hide.
|
|
27
|
+
2. **Extract only load-bearing invariants** — what must not break when editing
|
|
28
|
+
this file, precise to the expression level. A recorded incident number
|
|
29
|
+
earns its place. Skip anything the code already states plainly.
|
|
30
|
+
3. **Write one entry**: a one-line role, then the invariants. **No feature
|
|
31
|
+
lists** — features are legible from the code; invariants are not.
|
|
32
|
+
|
|
33
|
+
The discipline is subtractive: if a sentence describes what the file *does*
|
|
34
|
+
rather than what must *hold*, cut it.
|
|
35
|
+
|
|
36
|
+
Entry anatomy:
|
|
37
|
+
|
|
38
|
+
- `src/path/file.ts` — one-line role. Load-bearing invariants, precise to
|
|
39
|
+
the expression level (e.g. "`ctx.remote === false` for trusted-only
|
|
40
|
+
sites"): what must not break when editing this file. No feature lists —
|
|
41
|
+
the code already says what it does.
|
|
42
|
+
|
|
43
|
+
## Entries
|
|
44
|
+
|
|
45
|
+
- `src/by_framework/worker/app.py` — Worker bootstrap (`run_worker`/`_run_worker_async`):
|
|
46
|
+
resolves Redis config, wires plugins, starts the runner. Redis-connection
|
|
47
|
+
precedence (see [[redis-cluster-mode]]) must be replicated here identically
|
|
48
|
+
to `common/config.py` and `admin/cli.py`. `max_concurrency + 10` is the
|
|
49
|
+
default connection-pool size when neither `redis_max_connections` nor
|
|
50
|
+
`BYAI_REDIS_MAX_CONNECTIONS` is set — don't decouple pool sizing from
|
|
51
|
+
concurrency without updating both. `_build_auto_trace_plugin()` must raise
|
|
52
|
+
if more than one `by_framework_trace_*` provider factory activates from
|
|
53
|
+
env — silently picking one would hide a misconfiguration. `close_redis()`
|
|
54
|
+
must stay in the `finally` block (including on `asyncio.CancelledError`)
|
|
55
|
+
so restarts don't leak the connection pool. `health_port` (readiness
|
|
56
|
+
endpoint, see [[worker-readiness-endpoint]]) is opt-in only — unlike
|
|
57
|
+
`max_concurrency`/`fetch_count`, its `BYAI_WORKER_HEALTH_PORT` env-var
|
|
58
|
+
fallback must never resolve to a default port number; leave it `None`
|
|
59
|
+
when unset so no port opens for deployments that never asked for one.
|
|
60
|
+
|
|
61
|
+
- `src/by_framework/common/config.py` — `RedisConfig`/`WorkerConfig`/`LoggingConfig`
|
|
62
|
+
env-loaded dataclasses. `RedisConfig.from_env()`'s cluster-mode/key-schema
|
|
63
|
+
precedence must stay mirrored across files — see [[redis-cluster-mode]].
|
|
64
|
+
`REDIS_DB` must keep working as a deprecated fallback (with a warning) for
|
|
65
|
+
`REDIS_DATABASE` — don't remove until deprecation is done.
|
|
66
|
+
`RedisConfig.max_connections` must stay `Optional[int] = None` (meaning
|
|
67
|
+
"unset") — `redis_client.init_redis()` distinguishes "unset" from an
|
|
68
|
+
explicit value; defaulting it to a concrete int would silently discard a
|
|
69
|
+
caller's explicit `max_connections` kwarg (fix 6ec070c).
|
|
70
|
+
`WorkerConfig.heartbeat_lease_ttl_seconds` defaults to 30s = 6x the 5s
|
|
71
|
+
heartbeat interval (`RedisKeys.WORKER_DEFAULT_LEASE_TTL_SECONDS`) — this
|
|
72
|
+
margin is a deliberate second line of defense against event-loop stalls;
|
|
73
|
+
don't shrink one without the other.
|
|
74
|
+
|
|
75
|
+
- `src/by_framework/admin/cli.py` — `by-admin` Typer CLI for cluster ops
|
|
76
|
+
(worker list/suspend/evict, type deny/allow, metrics snapshot). `_get_redis()`
|
|
77
|
+
must replicate the SDK-wide Redis resolution order — see
|
|
78
|
+
[[redis-cluster-mode]]. The module-global `_redis_url` must be assigned
|
|
79
|
+
unconditionally in `_global()` (not only `if redis_url:`), so a prior CLI
|
|
80
|
+
invocation's URL doesn't leak into a later one that didn't pass
|
|
81
|
+
`--redis-url`. `--help` text literally contains `REDIS_MODE=cluster` /
|
|
82
|
+
`REDIS_CLUSTER_NODES` / `REDIS_KEY_SCHEMA_VERSION=v2` and is pinned by
|
|
83
|
+
`test_help_mentions_cluster_env_configuration` — keep help text and actual
|
|
84
|
+
precedence logic in sync.
|
|
85
|
+
|
|
86
|
+
- `src/by_framework/common/constants.py` — Central Redis key/naming registry
|
|
87
|
+
(`RedisKeys`), key-schema versioning (v1/v2), core timing constants. Every
|
|
88
|
+
key factory must route through `_versioned()`, and every SCAN-based
|
|
89
|
+
enumeration must use the paired `_worker_scan_pattern()` /
|
|
90
|
+
`_worker_id_from_scanned_key()` helpers — see [[redis-cluster-mode]].
|
|
91
|
+
`get_key_schema_version()`'s precedence deliberately does *not* infer v2
|
|
92
|
+
from `REDIS_MODE=cluster` alone — must stay mirrored with
|
|
93
|
+
`RedisConfig.from_env()`'s mode precedence. Cross-entity index keys
|
|
94
|
+
(`admin_workers()`, `trace_index_session/worker/agent`) are deliberately
|
|
95
|
+
left *untagged* relative to the per-entity keys they index — never share a
|
|
96
|
+
Cluster hash tag with them (fix 8501407); `wait_index()` is one of them.
|
|
97
|
+
`WORKER_DEFAULT_LEASE_TTL_SECONDS = 30` must stay ~6x
|
|
98
|
+
`WORKER_DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 5`. `WAIT_INDEX_SHARDS` is a
|
|
99
|
+
cross-SDK protocol constant, not a tunable — changing it re-maps every
|
|
100
|
+
session to a different shard, so entries written before the change are
|
|
101
|
+
swept by nobody. `DEFAULT_ASK_USER_TIMEOUT_MS` (machine waiting on a
|
|
102
|
+
human, sized off `DEFAULT_SESSION_TTL`, which is in *seconds*) must stay
|
|
103
|
+
decoupled from `DEFAULT_REPLY_TIMEOUT_MS` (machine waiting on machine) —
|
|
104
|
+
one shared value either kills a human's turn or lets a hung callee sit.
|
|
105
|
+
`LivenessErrorCode` values are wire contract: append only, never rename.
|
|
106
|
+
`CLIENT_SOURCE_AGENT_TYPE` is the marker a client stamps on an execution
|
|
107
|
+
record it dispatched (TS writes the same string; Java writes no field at
|
|
108
|
+
all) — it is a sentinel, not an agent type, and every reader that treats a
|
|
109
|
+
record's `source_agent_type` as somewhere to send a reply must exclude it.
|
|
110
|
+
`wait_consumed()` is the idempotency gate's "already resolved" marker;
|
|
111
|
+
`WAIT_CONSUMED_TTL_SECONDS` bounds how far apart two copies of one reply
|
|
112
|
+
may be and still be recognized as duplicates, and is sized off
|
|
113
|
+
`DEFAULT_SESSION_TTL` — the marker must outlive every wait it may have to
|
|
114
|
+
arbitrate, and the longest is `ask_user`'s, whose deadline *equals* the
|
|
115
|
+
session TTL. Sizing it shorter (it was `TASK_GROUP_TTL_SECONDS`) reopens
|
|
116
|
+
two holes: a repeated user answer stops being a duplicate, and — worse — a
|
|
117
|
+
stale duplicate sub-agent reply that loses its own marker falls through to
|
|
118
|
+
the `ask_user` candidate for the same caller and claims a wait that is
|
|
119
|
+
still live, after which the real answer is dropped. `wait_sweep_lock()`
|
|
120
|
+
guards one wait-index shard while it is swept — see
|
|
121
|
+
[[suspend-resume-liveness]] for why its ownership is advisory rather than
|
|
122
|
+
a leader election, and why `WAIT_SWEEP_LOCK_TTL_SECONDS` must outlast one
|
|
123
|
+
shard's pass. `wait_renew_origin()` remembers a wait's *original* deadline
|
|
124
|
+
across the renewals that overwrite it; `WAIT_RENEW_MAX_MULTIPLE` is the
|
|
125
|
+
budget measured from it, and its TTL must stay well above the largest
|
|
126
|
+
`N * timeout` in use or the budget silently restarts mid-wait.
|
|
127
|
+
`WAIT_PRUNE_AFTER_SECONDS` is how far in the past an entry's score must
|
|
128
|
+
lie before the sweep's prune half deletes it unexamined; it must stay
|
|
129
|
+
*strictly* above `DEFAULT_SESSION_TTL`, because `DEFAULT_ASK_USER_TIMEOUT_MS`
|
|
130
|
+
equals that exactly and a threshold trimmed to it would sit on the boundary
|
|
131
|
+
of a live ask_user wait.
|
|
132
|
+
|
|
133
|
+
- `src/by_framework/core/wait_index.py` — Pure codec for the wait-index ZSET
|
|
134
|
+
member (`{session_id}|{parent_message_id}|{child_message_id}|{task_group_id}`)
|
|
135
|
+
plus shard selection. Every field must remain derivable from a single
|
|
136
|
+
`ResumeCommand` — `member_from_resume()` is what lets the idempotency gate
|
|
137
|
+
`ZREM` before any registry lookup, so a field a reply doesn't carry can
|
|
138
|
+
never be added. That function also *reverses* the header ids (a reply's
|
|
139
|
+
`header.message_id` is the caller's, its `header.parent_message_id` is the
|
|
140
|
+
sub-task's); keying by the reply's own `message_id` collapses every Task
|
|
141
|
+
Group sibling onto one member. `session_id`/`message_id` are
|
|
142
|
+
caller-controlled, so `|` and `\` are escaped rather than assumed absent.
|
|
143
|
+
`wait_index_shard()` must stay a fixed, language-portable hash (FNV-1a
|
|
144
|
+
32-bit) — Python's `hash()` is per-process salted, and TS/Java sweepers
|
|
145
|
+
must land on the same shard; `member_digest()` (SHA-1 hex) is the same deal
|
|
146
|
+
for every key named after a member. Keep this module I/O-free.
|
|
147
|
+
|
|
148
|
+
- `src/by_framework/core/wait_gate.py` — The idempotency gate: `ZREM`s the
|
|
149
|
+
caller's wait-index entry so exactly one copy of a reply wakes it. Its
|
|
150
|
+
whole difficulty is that `ZREM` returning 0 conflates "someone already
|
|
151
|
+
claimed this wait" with "this wait was never registered" (a pre-upgrade
|
|
152
|
+
dispatch, or an expired entry) — hence the `wait_consumed` marker written
|
|
153
|
+
by the winner, which is the *only* thing that separates them. Treating an
|
|
154
|
+
unmarked 0 as a duplicate drops every in-flight reply during a rolling
|
|
155
|
+
upgrade. Both this and the surrounding `except` must keep failing **open**:
|
|
156
|
+
a duplicated wake-up is recoverable, a dropped reply is permanent silence,
|
|
157
|
+
so any doubt (Redis error, missing marker) allows the message. Concurrent
|
|
158
|
+
copies are arbitrated by `ZREM`'s atomicity, not by the marker; the marker
|
|
159
|
+
only matters for copies separated in time, which is why the write-after-
|
|
160
|
+
claim window is not worth closing. `candidate_members()` must resolve each
|
|
161
|
+
candidate fully (claim, then check *its* marker) before trying the next:
|
|
162
|
+
the second candidate exists only because `ask_user` registers with an empty
|
|
163
|
+
`child_message_id` while its reply carries a client-chosen
|
|
164
|
+
`parent_message_id`, and falling through to it early would clear a live
|
|
165
|
+
ask_user wait belonging to the same caller. `emit_orphaned_reply()` is
|
|
166
|
+
observability for work that will now be thrown away — fail-soft, and never
|
|
167
|
+
able to change the drop/allow decision.
|
|
168
|
+
|
|
169
|
+
- `src/by_framework/core/wait_sweeper.py` — `WaitIndexSweeper`: the only
|
|
170
|
+
thing that can move a caller whose reply is never coming, since a
|
|
171
|
+
suspended caller has *ended* and left no timer behind. Subsystem context
|
|
172
|
+
and the full triage table: [[suspend-resume-liveness]]. It must never
|
|
173
|
+
`ZREM` the entry it acts on — the synthesized reply and a late real one
|
|
174
|
+
are meant to compete for the same entry in `wait_gate.py`, and clearing it
|
|
175
|
+
here forces the synthesized copy to bypass the gate, i.e. a second
|
|
176
|
+
ungated wake-up path. A callee that is itself suspended must be *renewed*,
|
|
177
|
+
never failed: the innermost wait has the earliest deadline by
|
|
178
|
+
construction, so failure climbs a chain hop by hop instead of collapsing
|
|
179
|
+
it — and that is also why such a callee is exempt from the renewal
|
|
180
|
+
ceiling, which arrives *earliest* for the outermost wait and would fail a
|
|
181
|
+
chain top-down. A live worker lease buys more time (that is what keeps
|
|
182
|
+
slow work from being killed) but only up to
|
|
183
|
+
`registered_at + WAIT_RENEW_MAX_MULTIPLE * timeout`, after which the callee
|
|
184
|
+
is failed with `CHILD_TIMEOUT`: a lease says the process is up, not that
|
|
185
|
+
the work is moving, so renewing on it alone hangs the caller forever. The
|
|
186
|
+
budget must be re-derived from the saved `wait_renew_origin`, never from
|
|
187
|
+
the score a renewal just wrote, and never from whether the callee's
|
|
188
|
+
`updated_at` advanced — a legitimate long model call stands just as still
|
|
189
|
+
as a deadlock. A Task Group orphan gets the *same* synthesized reply,
|
|
190
|
+
carrying the group id, so the existing join counts it; this file must
|
|
191
|
+
never write `task_group_results`/`completed` itself, or the copy that
|
|
192
|
+
reaches `total` leaves no reply to run the join. A caller that is already
|
|
193
|
+
terminal gets its entry cleaned up
|
|
194
|
+
and *no* reply — registration happens before the dispatch `xadd`, so a
|
|
195
|
+
failed `xadd` leaves exactly that. Synthesized failures must stay shaped
|
|
196
|
+
like a sub-agent's own failure (`error`/`error_code` in `reply_data`,
|
|
197
|
+
same stream, same header id reversal). `_synthesize_failure()` seeds its
|
|
198
|
+
reply's metadata from `child.get("metadata")` — the callee's execution
|
|
199
|
+
snapshot, where `context.py`'s `initialize_execution()` persists the
|
|
200
|
+
caller's original dispatch metadata — instead of an empty dict, so a
|
|
201
|
+
caller that never gets a real reply at all still gets its own metadata
|
|
202
|
+
back, consistent with every other reply shape to it. Two switches, and
|
|
203
|
+
they must stay separate: *compensation* (the triage, replies, renewals,
|
|
204
|
+
cancellation) is off unless `BY_FRAMEWORK_WAIT_SWEEPER_ENABLED` is set and is the rollback
|
|
205
|
+
switch for the whole liveness feature, while *pruning* is on by default
|
|
206
|
+
(`BY_FRAMEWORK_WAIT_PRUNE_ENABLED`) because nothing else ever removes a
|
|
207
|
+
wait-index entry — with compensation off, every call whose reply never
|
|
208
|
+
arrives would leak one forever into a shard ZSET that is shared across
|
|
209
|
+
sessions and so cannot carry a TTL. Pruning may stay unguarded only because
|
|
210
|
+
it decides nothing: it decodes no member and reads no execution record,
|
|
211
|
+
just one `ZREMRANGEBYSCORE` whose bound is a proof (every writer sets the
|
|
212
|
+
score to its own clock plus a non-negative offset, and only while the
|
|
213
|
+
caller's execution record exists, so an old score means the session
|
|
214
|
+
registry is gone and triage could reach nothing but "caller missing").
|
|
215
|
+
That argument covers renewed entries too — a renewal *raises* the score.
|
|
216
|
+
`CHILD_TIMEOUT` — and only it, since the
|
|
217
|
+
other outcomes have no live process on the other end — also asks the callee
|
|
218
|
+
to stop, by delegating wholesale to `GatewayClient.cancel_task` (which
|
|
219
|
+
targets `worker_ctrl_stream(worker_id)`; a cancel on the agent type's
|
|
220
|
+
competitive stream is claimed by an arbitrary worker whose in-memory table
|
|
221
|
+
has no such execution, so it records a cancellation while cancelling
|
|
222
|
+
nothing). That request must stay strictly *after* the reply is emitted and
|
|
223
|
+
strictly swallowed on failure: cancellation is cooperative, so a callee
|
|
224
|
+
wedged in a blocking call is both why the ceiling fired and the case it
|
|
225
|
+
cannot reach — nothing in the wake-up may depend on it. It also does not
|
|
226
|
+
silence the callee, whose own `CANCELLED` reply is dropped by the gate.
|
|
227
|
+
Opt out with `BY_FRAMEWORK_WAIT_CANCEL_ON_TIMEOUT`; per-call opt-out is
|
|
228
|
+
deliberately not offered (see the file's own note — the member is a wire
|
|
229
|
+
format, so it would cost a side-key write on every dispatch). Fail-soft
|
|
230
|
+
throughout: it runs inside every worker, so a raise here must not take the
|
|
231
|
+
worker down, and one bad entry must not abort a shard.
|
|
232
|
+
|
|
233
|
+
- `src/by_framework/core/wait_reply.py` — The single construction of a reply
|
|
234
|
+
built by someone other than the callee that owed it: the sweeper's
|
|
235
|
+
stand-ins and `call_agents`' compensation for a sub-task whose target agent
|
|
236
|
+
type was never available. Two spellings of that message drift, and the
|
|
237
|
+
drift only shows up as a hung caller, which is why neither site builds it
|
|
238
|
+
itself. `header.message_id` must be the caller's id (what the runner
|
|
239
|
+
reattaches by) and `header.parent_message_id` the sub-task's (what the join
|
|
240
|
+
keys by and what `wait_index.member_from_resume` rebuilds); the failure
|
|
241
|
+
detail must ride in `reply_data`, exactly where a sub-agent that ran and
|
|
242
|
+
raised puts it, so no caller can tell "failed" from "never got to fail".
|
|
243
|
+
Provenance (`synthesized_by`, `liveness_error_code`) goes on
|
|
244
|
+
`header.metadata` and must stay out of `reply_data` for the same reason.
|
|
245
|
+
`flush_pending_group_replies()` is called only after a handler returns
|
|
246
|
+
normally — inline delivery would put a reply on the caller's own control
|
|
247
|
+
stream strictly before it suspends, making a rare race certain, and a
|
|
248
|
+
handler that raised has already aborted its group.
|
|
249
|
+
|
|
250
|
+
- `src/by_framework/client/client.py` — `GatewayClient.send_message()` and
|
|
251
|
+
friends; publishes commands to Redis control streams and drives registry
|
|
252
|
+
execution-tracking as a side effect. On a `RESUME` dispatch, must look up
|
|
253
|
+
the original execution via `registry.get_execution_by_message_id(message_id,
|
|
254
|
+
session_id=...)`, reuse *that* `execution_id`, and skip
|
|
255
|
+
`initialize_execution()` for it — calling `initialize_execution()`
|
|
256
|
+
unconditionally silently detaches the `ResumeCommand` from the suspended
|
|
257
|
+
`WAITING_USER` execution it's meant to continue, orphaning it (fix
|
|
258
|
+
90764e1, #75/#76/#77). The registry lookup must stay guarded with
|
|
259
|
+
`hasattr(registry, "get_execution_by_message_id")` so registry doubles/older
|
|
260
|
+
implementations fall back to minting a fresh execution_id. Root-dispatch
|
|
261
|
+
trace writes (`_write_trace_root_start/_end`) must only fire when
|
|
262
|
+
`not parent_message_id` — firing them on every `call_agent` hop would
|
|
263
|
+
duplicate trace roots.
|
|
264
|
+
|
|
265
|
+
- `src/by_framework/worker/runner.py` — `WorkerRunner`, the consume loop:
|
|
266
|
+
`XREADGROUP` fetch, command dispatch, resume/suspend bookkeeping, denylist
|
|
267
|
+
enforcement. `_active_agent_type_streams()` must read only the in-memory
|
|
268
|
+
`self._denied_agent_types` frozenset — no Redis `SISMEMBER` call inside the
|
|
269
|
+
hot consume-loop path; refreshed only by the heartbeat thread's
|
|
270
|
+
`denylist_refresh` callback (bounded staleness ~1 heartbeat interval) (fix
|
|
271
|
+
8f23c78). The frozenset must be swapped by whole-reference reassignment,
|
|
272
|
+
relying on CPython GIL atomicity across the heartbeat thread and the async
|
|
273
|
+
loop — never mutate the set in place without adding a lock. A
|
|
274
|
+
`ResumeCommand` that fails to resolve to an existing execution must log a
|
|
275
|
+
warning — silently starting a disconnected new execution is the exact
|
|
276
|
+
failure mode this log surfaces (fix 90764e1, #77). Terminal-state
|
|
277
|
+
replay-skip logic is coupled to `ResumeCommand` handling: skip replaying an
|
|
278
|
+
execution already in a terminal state *unless* the command is a
|
|
279
|
+
`ResumeCommand`. `_health_server` (see [[worker-readiness-endpoint]]) must
|
|
280
|
+
start before any other step in `start()` (currently first line of the
|
|
281
|
+
`try:` block) so a probe hitting the port during startup gets an honest
|
|
282
|
+
`starting` 503 instead of connection-refused, and must `stop()` as the
|
|
283
|
+
*last* step of `_shutdown()` — after every other teardown step, not
|
|
284
|
+
before — so `/readyz` stays reachable (reporting `draining`) for the
|
|
285
|
+
entire drain. `self._draining = True` must stay the first line of
|
|
286
|
+
`_shutdown()`, ahead of every other teardown step, not just ahead of the
|
|
287
|
+
health-server stop. `is_resumed_execution` infers "already been through a
|
|
288
|
+
worker" from `existing_execution["status"] != QUEUED`, so QUEUED must stay
|
|
289
|
+
the *only* status an execution can hold before its first pickup — a
|
|
290
|
+
suspended caller persists as `WAITING_AGENT`/`WAITING_USER` precisely to
|
|
291
|
+
keep that inference true; reusing QUEUED for any post-pickup state silently
|
|
292
|
+
makes a resume re-derive its identity from the message header instead of
|
|
293
|
+
the record. Every `ResumeCommand` passes the `core/wait_gate.py`
|
|
294
|
+
idempotency gate *here*, before the execution lookup — and being upstream
|
|
295
|
+
of `GatewayWorker` is what also puts it before Task Group join, whose
|
|
296
|
+
`HINCRBY completed` a duplicate would push past `total` and aggregate a
|
|
297
|
+
second time. A dropped reply is acked and reported, never left pending.
|
|
298
|
+
Background tasks (`MetricsCollector`, `WaitIndexSweeper`) are started
|
|
299
|
+
best-effort — a background component that fails to construct must never
|
|
300
|
+
prevent the worker from consuming — and every one of them must be
|
|
301
|
+
cancelled and awaited in `_shutdown()`, or shutdown hangs on it.
|
|
302
|
+
|
|
303
|
+
- `src/by_framework/worker/health_server.py` — `WorkerHealthServer`: the
|
|
304
|
+
`/readyz` readiness HTTP endpoint, on its own daemon thread (mirrors
|
|
305
|
+
`heartbeat.py`'s "don't share the main event loop" pattern — see that
|
|
306
|
+
file's own docstring). Full design record, including why this exists and
|
|
307
|
+
the hard rule against ever wiring it to a liveness check:
|
|
308
|
+
[[worker-readiness-endpoint]]. `_compute_reason()`'s check order is the
|
|
309
|
+
entire contract — `starting > draining > evicted > suspended >
|
|
310
|
+
consumer_stalled > serving`, first match wins; reordering these checks
|
|
311
|
+
silently changes what an operator is told during a real incident. All
|
|
312
|
+
Worker state is read via constructor-injected callables (`has_started`,
|
|
313
|
+
`is_draining`, `admin_lifecycle`, `consumer_healthy`) — this class must
|
|
314
|
+
never reach into `WorkerRunner` directly, which is what keeps it testable
|
|
315
|
+
standalone against fake state (see `tests/worker/test_health_server.py`).
|
|
316
|
+
|
|
317
|
+
- `src/by_framework/worker/worker.py` — `GatewayWorker`: per-message lifecycle
|
|
318
|
+
(`_handle_message`), Task Group join, and the agent-return reply.
|
|
319
|
+
`_enqueue_agent_return()` builds the *only* thing that resumes a suspended
|
|
320
|
+
caller — its header must keep `message_id` = the caller's
|
|
321
|
+
`parent_message_id` and `parent_message_id` = this sub-task's own
|
|
322
|
+
`message_id`; Group Join keys `task_group_results` by the latter because
|
|
323
|
+
it's the only per-sibling-unique value (fix 9d4a0a4). Group Join must stay
|
|
324
|
+
the single accounting path: a result written or `completed` incremented
|
|
325
|
+
anywhere else can be the increment that reaches `total`, leaving no reply
|
|
326
|
+
to wake the caller (fix 55c7e6f; a dispatch-time failure must instead emit
|
|
327
|
+
the `FAILED` reply a sub-agent would have sent).
|
|
328
|
+
`_persist_single_call_result()` covers the non-group path only (early-return
|
|
329
|
+
when `header.task_group_id` is set, or the group's own write is duplicated)
|
|
330
|
+
and must stay fail-soft — a persist error costs recoverability, never the
|
|
331
|
+
reply. Its stored payload must stay field-for-field isomorphic with the
|
|
332
|
+
`result_data` the join path writes; both are read by the same recovery
|
|
333
|
+
code. `should_emit_stream_end` reads `context._is_suspended` /
|
|
334
|
+
`_permission_transferred`, so those flags' accuracy in `context.py` is
|
|
335
|
+
load-bearing here.
|
|
336
|
+
Who gets replied to comes from `_resolve_reply_command()`, never from a
|
|
337
|
+
resume's own header: a `ResumeCommand`'s `source_agent_type` /
|
|
338
|
+
`parent_message_id` / `task_group_id` describe the *sub-agent* that just
|
|
339
|
+
finished, so a resumed execution must rebuild its caller from the execution
|
|
340
|
+
record `initialize_execution()` wrote (`existing_data`). Treating "is a
|
|
341
|
+
resume" as "has no caller" drops the middle link's result in any chain of
|
|
342
|
+
depth >= 3. That record names `CLIENT_SOURCE_AGENT_TYPE` for anything a
|
|
343
|
+
*client* dispatched, which is a marker and not an agent type — it must be
|
|
344
|
+
excluded explicitly, or every root execution that ever resumes (an
|
|
345
|
+
`ask_user` round is the common one) posts its result to a control stream
|
|
346
|
+
nobody consumes and, worse, stops emitting the end-of-stream event the user
|
|
347
|
+
is waiting on, because it now believes it owes an agent a reply.
|
|
348
|
+
`_resolve_reply_command()` restores `header.metadata` from the same
|
|
349
|
+
execution snapshot (`existing_data["metadata"]`) as a full *replacement*,
|
|
350
|
+
never merged with the resuming command's own metadata: that metadata
|
|
351
|
+
belongs to whatever woke this execution up (an `ask_user` answer, or a
|
|
352
|
+
sub-call's reply), not to the original caller, and leaking it would let a
|
|
353
|
+
transient hop overwrite the caller's own data instead of being layered
|
|
354
|
+
under `task_result.metadata` like `_enqueue_agent_return()`'s merge
|
|
355
|
+
already does correctly.
|
|
356
|
+
The success path must NOT reply while `context._is_suspended` — a suspended
|
|
357
|
+
execution has no result, and forwarding the value the handler returned in
|
|
358
|
+
order to unwind both wakes the caller early and consumes the single reply it
|
|
359
|
+
was waiting for. "Suspended" here must carry the same terminal-status
|
|
360
|
+
exception as `_apply_suspended_status()`: a handler that returned a terminal
|
|
361
|
+
status is recorded finished and will never resume to reply later, so it owes
|
|
362
|
+
its caller a reply now. The `CancelledError`/`Exception` paths must reply
|
|
363
|
+
anyway: a dead execution will never resume to produce one.
|
|
364
|
+
`_apply_suspended_status()` overwrites a non-terminal business status with
|
|
365
|
+
`context._suspended_state`, so what lands in the registry is
|
|
366
|
+
`WAITING_AGENT`/`WAITING_USER` rather than the handler's placeholder; a
|
|
367
|
+
terminal status always wins.
|
|
368
|
+
`flush_pending_group_replies()` runs immediately after `process_command`
|
|
369
|
+
returns and nowhere else: the stand-ins it delivers belong to a group whose
|
|
370
|
+
caller must already have finished suspending, and a handler that raised has
|
|
371
|
+
aborted its group, so its queued stand-ins must die with it.
|
|
372
|
+
|
|
373
|
+
- `src/by_framework/worker/context.py` — `AgentContext`: the agent-facing
|
|
374
|
+
runtime surface; `_dispatch_single_task()` is the one dispatch path behind
|
|
375
|
+
both `call_agent` and `call_agents`. `_is_suspended` /
|
|
376
|
+
`_permission_transferred` are flipped *before* the availability check, so
|
|
377
|
+
every early return that means "nothing was dispatched" must restore the
|
|
378
|
+
values captured on entry — restoring to `False` instead is wrong, since an
|
|
379
|
+
earlier `call_agent` on the same context may legitimately have suspended
|
|
380
|
+
it. An availability rejection returns a `FAILED` result dict, it does not
|
|
381
|
+
raise; `call_agents` depends on that to keep fanning out. It must
|
|
382
|
+
compensate such a member with a *reply* (`core/wait_reply.py`, queued on
|
|
383
|
+
`_pending_group_replies` for the worker to flush after `process_command`
|
|
384
|
+
returns) and must never write `task_group_results`/`HINCRBY completed`
|
|
385
|
+
itself: that is a second implementation of the join's accounting, and when
|
|
386
|
+
its increment is the one that reaches `total` no reply is left to run the
|
|
387
|
+
join and the caller hangs forever — reachable both when every target is
|
|
388
|
+
offline and when a sibling's reply is joined mid-fan-out. It also registers
|
|
389
|
+
a wait entry for the member it could not dispatch, so an undelivered
|
|
390
|
+
stand-in is still compensable by a sweep. On a genuine dispatch exception
|
|
391
|
+
mid-batch, `call_agents` must mark the group `aborted` before re-raising,
|
|
392
|
+
or already-sent siblings' replies resume a caller that is already dead;
|
|
393
|
+
queued stand-ins die with the raise for the same reason. `call_agents`
|
|
394
|
+
rejects `message_id` outright for more than one task: a group's per-sibling
|
|
395
|
+
identity *is* its sub-task message_id (it keys both `task_group_results` and
|
|
396
|
+
the wait index), so pinning one across the fan-out overwrites every
|
|
397
|
+
sibling's result and leaves the gate one entry to claim, hanging the caller
|
|
398
|
+
short of `total`. It fails loudly rather than silently minting ids, and the
|
|
399
|
+
single-task use — where the id collides with nothing — is unaffected.
|
|
400
|
+
Every `wait_for_reply=True` dispatch registers one wait-index entry via
|
|
401
|
+
`_register_wait()` (one per sub-task for a group, so each can be resolved
|
|
402
|
+
independently), keyed by `parent_message_id` = the id the awaited reply
|
|
403
|
+
carries as `header.message_id`; getting that direction wrong makes the
|
|
404
|
+
entry unmatchable by the reply that should clear it. `ask_user` registers
|
|
405
|
+
with an empty `child_message_id` (no sub-task) and its own, much larger
|
|
406
|
+
timeout — sharing `call_agent`'s would time out a human. Because that
|
|
407
|
+
member repeats across consecutive ask_user rounds, registering also clears
|
|
408
|
+
the previous round's `wait_consumed` marker, or the gate would read round
|
|
409
|
+
2's answer as round 1's duplicate. Registration is fail-soft and must stay
|
|
410
|
+
so: it is bookkeeping, not the dispatch.
|
|
411
|
+
`initialize_execution()`'s payload carries `source_agent_type` and
|
|
412
|
+
`task_group_id` because they are the only durable record of who a
|
|
413
|
+
suspended callee owes its reply to (see `worker.py`). It also carries
|
|
414
|
+
`metadata` (a copy of the dispatched `command.header.metadata`) for the
|
|
415
|
+
same reason: it is the only durable record of what the caller's metadata
|
|
416
|
+
*was*, and `worker.py`'s `_resolve_reply_command()` reads it back
|
|
417
|
+
verbatim so a caller's metadata survives however many times the callee
|
|
418
|
+
suspends and resumes before it finally replies.
|
|
419
|
+
`_suspended_state` records *which* state the execution is waiting in and is
|
|
420
|
+
what the framework persists; keep it set/rolled-back in lockstep with
|
|
421
|
+
`_is_suspended`.
|
|
422
|
+
|
|
423
|
+
- `src/by_framework/worker/processor.py` — `GatewayProcessor`: the standalone
|
|
424
|
+
message-lifecycle path for callers that don't subclass `GatewayWorker`. It
|
|
425
|
+
duplicates `worker.py`'s reply logic, so every reply-side invariant there
|
|
426
|
+
applies here too and the two must be changed together — the shape has
|
|
427
|
+
already drifted once and taken the same bug twice. Specifically:
|
|
428
|
+
`_enqueue_callback()`'s `header.message_id` must be the *caller's*
|
|
429
|
+
message_id (this dispatch's `parent_message_id`), since that is what the
|
|
430
|
+
caller's suspended execution is reattached by; a freshly minted id resolves
|
|
431
|
+
to no execution. `_resolve_reply_header()` must read a resumed execution's
|
|
432
|
+
caller from the execution record, not from the resume header (which names
|
|
433
|
+
the sub-agent), while excluding `CLIENT_SOURCE_AGENT_TYPE` for the reason
|
|
434
|
+
spelled out under `worker.py`; and no callback may be sent while
|
|
435
|
+
`context._is_suspended` (with the same terminal-status exception).
|
|
436
|
+
Being a *second* entry point for replies, it carries the same
|
|
437
|
+
`core/wait_gate.py` gate as `runner.py` — a gate on one of two doors is not
|
|
438
|
+
a gate, and an ungated reply here would both wake a resolved caller and
|
|
439
|
+
leave its wait-index entry behind for a sweep to resolve all over again.
|
|
440
|
+
`process()` returns `None` for a reply it drops. It also flushes
|
|
441
|
+
`call_agents`' queued stand-ins on the same terms as `worker.py` — after
|
|
442
|
+
the handler returns, never when it raised.
|
|
443
|
+
|
|
444
|
+
- `src/by_framework/core/registry.py` — `WorkerRegistry`: Redis-backed worker
|
|
445
|
+
membership/heartbeat/execution-state, admin lifecycle, locking primitives.
|
|
446
|
+
`mark_execution_finished()` must stamp `finished_at` only
|
|
447
|
+
`if is_terminal_state(status)` — stamping it unconditionally makes a
|
|
448
|
+
suspended `WAITING_USER` execution look completed to
|
|
449
|
+
`metrics/snapshot.py`'s latency/`completed_count` math (fix 90764e1, #76).
|
|
450
|
+
`heartbeat_worker()` uses an atomic Lua CAS script with token-mode (verify
|
|
451
|
+
stored token before overwrite) and legacy no-token mode — must not be
|
|
452
|
+
replaced by a plain `SET`; return codes `1`=success / `0`=owned-by-another /
|
|
453
|
+
`-1`=unparseable-legacy are relied on by callers. `_RELEASE_LOCK_SCRIPT` /
|
|
454
|
+
`_REFRESH_LOCK_SCRIPT` are Redlock-style token-verified delete/expire — must
|
|
455
|
+
stay atomic, and an empty-string token means "unconditional" (no-token
|
|
456
|
+
legacy mode). Those two scripts are also the shared Redlock primitives
|
|
457
|
+
behind `acquire_scoped_lock()`/`release_scoped_lock()` (used for wait-index
|
|
458
|
+
shard claims), so the value written must stay a cjson-decodable object
|
|
459
|
+
carrying a `token` field — a bare token string parses as unparseable legacy
|
|
460
|
+
data and leaves the holder unable to release its own lock.
|
|
461
|
+
`set_worker_admin_state`/`clear_worker_admin_state`: the
|
|
462
|
+
per-worker `worker_admin(id)` hash write must complete independently of the
|
|
463
|
+
`admin_workers()` global-index update — see [[redis-cluster-mode]].
|
|
464
|
+
|
|
465
|
+
- `src/by_framework/core/protocol/responses.py` — `SendMessageResponse` /
|
|
466
|
+
`CancelTaskResponse` / `CancelSessionResponse` frozen dataclasses +
|
|
467
|
+
`ExecutionStatus` string constants. `ExecutionStatus` string values
|
|
468
|
+
(`"SUCCESS"`, `"NOT_FOUND"`, `"WORKER_NOT_ONLINE"`, etc.) are a wire-level
|
|
469
|
+
contract matched by literal string elsewhere in the client and its tests —
|
|
470
|
+
renaming a value is a cross-file breaking change.
|
|
471
|
+
`ERR_AGENT_TYPE_NOT_FOUND = ERR_AGENT_TYPE_UNAVAILABLE` is a deliberate
|
|
472
|
+
alias; both names must keep resolving to the same string. Response
|
|
473
|
+
dataclasses are `@dataclass(frozen=True)` — don't drop `frozen` or add
|
|
474
|
+
mutable defaults.
|
|
475
|
+
|
|
476
|
+
- `src/by_framework/core/protocol/content_type.py` — `SseMessageType` /
|
|
477
|
+
`SseReasonMessageType` enums: numeric string codes for SSE messages sent to
|
|
478
|
+
the frontend/other-language SDKs. These codes are an external protocol
|
|
479
|
+
contract — once shipped, a code's meaning must never change; only append
|
|
480
|
+
new codes. `SseReasonMessageType.think_text = "1002"` intentionally reuses
|
|
481
|
+
`SseMessageType.text`'s value — looks like a copy-paste bug but is
|
|
482
|
+
deliberate protocol code reuse; verify against frontend/other-language SDKs
|
|
483
|
+
before "fixing".
|