create-windy 0.3.2 → 0.3.5
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.
- package/README.md +27 -9
- package/dist/cli.js +825 -117
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +18 -11
- package/template/_gitignore +1 -0
- package/template/apps/agent-server/README.md +2 -0
- package/template/apps/agent-server/pyproject.toml +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
- package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
- package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
- package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
- package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
- package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
- package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
- package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
- package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
- package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
- package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
- package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
- package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
- package/template/apps/agent-server/tests/test_api.py +24 -1
- package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
- package/template/apps/agent-server/tests/test_config.py +4 -0
- package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
- package/template/apps/agent-server/tests/test_multimodal.py +51 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
- package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
- package/template/apps/agent-server/tests/test_telemetry.py +25 -0
- package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
- package/template/apps/agent-server/uv.lock +165 -0
- package/template/apps/document-server/Dockerfile +19 -0
- package/template/apps/document-server/pyproject.toml +43 -0
- package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
- package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
- package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
- package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
- package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
- package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
- package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
- package/template/apps/document-server/tests/__init__.py +1 -0
- package/template/apps/document-server/tests/test_config.py +42 -0
- package/template/apps/document-server/tests/test_service.py +140 -0
- package/template/apps/document-server/uv.lock +430 -0
- package/template/apps/embedding-server/Dockerfile +17 -0
- package/template/apps/embedding-server/pyproject.toml +44 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
- package/template/apps/embedding-server/tests/__init__.py +1 -0
- package/template/apps/embedding-server/tests/test_adapter.py +92 -0
- package/template/apps/embedding-server/tests/test_config.py +38 -0
- package/template/apps/embedding-server/uv.lock +449 -0
- package/template/apps/server/src/agent/background-operation-port.ts +37 -0
- package/template/apps/server/src/agent/background-operation.test.ts +231 -0
- package/template/apps/server/src/agent/background-operation.ts +252 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
- package/template/apps/server/src/agent/execution-grants.ts +7 -1
- package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
- package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
- package/template/apps/server/src/agent/idempotency-key.ts +11 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
- package/template/apps/server/src/agent/remote-runtime.ts +173 -5
- package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
- package/template/apps/server/src/agent/run-authorization.ts +180 -0
- package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
- package/template/apps/server/src/agent/run-contracts.ts +88 -1
- package/template/apps/server/src/agent/run-definition-support.ts +58 -0
- package/template/apps/server/src/agent/run-errors.ts +25 -0
- package/template/apps/server/src/agent/run-facade.test.ts +57 -46
- package/template/apps/server/src/agent/run-facade.ts +185 -65
- package/template/apps/server/src/agent/run-release.ts +78 -0
- package/template/apps/server/src/agent/run-route-input.ts +169 -0
- package/template/apps/server/src/agent/run-routes.ts +63 -76
- package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
- package/template/apps/server/src/agent/sandbox-client.ts +200 -0
- package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
- package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
- package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
- package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
- package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
- package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
- package/template/apps/server/src/agent/tool-approval.ts +115 -0
- package/template/apps/server/src/agent/tool-audit.ts +62 -0
- package/template/apps/server/src/agent/tool-host.test.ts +39 -11
- package/template/apps/server/src/agent/tool-host.ts +184 -17
- package/template/apps/server/src/agent/tool-registry.ts +52 -47
- package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
- package/template/apps/server/src/agent/vision-input.ts +120 -0
- package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
- package/template/apps/server/src/ai-artifact/codec.ts +137 -0
- package/template/apps/server/src/ai-artifact/routes.ts +37 -0
- package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
- package/template/apps/server/src/application-services.ts +62 -2
- package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
- package/template/apps/server/src/auth/reauthentication.ts +19 -0
- package/template/apps/server/src/auth/runtime.ts +9 -1
- package/template/apps/server/src/data-access/context.ts +6 -4
- package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
- package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
- package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
- package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
- package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
- package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
- package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
- package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
- package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
- package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
- package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
- package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
- package/template/apps/server/src/data-governance/export/routes.ts +11 -1
- package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
- package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
- package/template/apps/server/src/data-governance/export/service.ts +90 -113
- package/template/apps/server/src/data-governance/export/source.ts +101 -0
- package/template/apps/server/src/data-governance/export/types.ts +27 -0
- package/template/apps/server/src/document/contracts.ts +76 -0
- package/template/apps/server/src/document/facade.ts +339 -0
- package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
- package/template/apps/server/src/document/remote-runtime.ts +223 -0
- package/template/apps/server/src/document/routes.ts +193 -0
- package/template/apps/server/src/embedding/contracts.ts +83 -0
- package/template/apps/server/src/embedding/error.ts +10 -0
- package/template/apps/server/src/embedding/facade.ts +217 -0
- package/template/apps/server/src/embedding/input.ts +172 -0
- package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
- package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
- package/template/apps/server/src/embedding/routes.ts +152 -0
- package/template/apps/server/src/index.ts +40 -43
- package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
- package/template/apps/server/src/jobs/artifact-service.ts +10 -0
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
- package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
- package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
- package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +17 -0
- package/template/apps/server/src/jobs/execution.test.ts +34 -0
- package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
- package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
- package/template/apps/server/src/jobs/repository.ts +3 -0
- package/template/apps/server/src/jobs/worker.ts +80 -41
- package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
- package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
- package/template/apps/server/src/module-host/durable-context.ts +124 -0
- package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
- package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
- package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
- package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
- package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
- package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
- package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
- package/template/apps/server/src/module-host/governed-resources.ts +203 -0
- package/template/apps/server/src/module-host/http-runtime.ts +95 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +49 -1
- package/template/apps/server/src/module-host/initialize.ts +39 -16
- package/template/apps/server/src/module-host/job-port.test.ts +262 -0
- package/template/apps/server/src/module-host/job-port.ts +334 -0
- package/template/apps/server/src/module-host/request-context.test.ts +88 -2
- package/template/apps/server/src/module-host/request-context.ts +56 -22
- package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
- package/template/apps/server/src/module-host/route-installer.ts +36 -1
- package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/module-runtime-validation.ts +7 -3
- package/template/apps/server/src/speech/contracts.ts +76 -0
- package/template/apps/server/src/speech/facade.ts +316 -0
- package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
- package/template/apps/server/src/speech/remote-runtime.ts +227 -0
- package/template/apps/server/src/speech/routes.ts +180 -0
- package/template/apps/server/src/system/actor.ts +39 -0
- package/template/apps/server/src/system/route-helpers.ts +1 -0
- package/template/apps/server/src/workflow/service.ts +2 -1
- package/template/apps/speech-server/Dockerfile +22 -0
- package/template/apps/speech-server/pyproject.toml +43 -0
- package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
- package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
- package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
- package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
- package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
- package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
- package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
- package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
- package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
- package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
- package/template/apps/speech-server/tests/__init__.py +1 -0
- package/template/apps/speech-server/tests/test_config.py +33 -0
- package/template/apps/speech-server/tests/test_service.py +154 -0
- package/template/apps/speech-server/uv.lock +430 -0
- package/template/apps/web/src/app/business-routes.ts +22 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
- package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
- package/template/apps/web/src/components/common/FormDialog.vue +6 -0
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
- package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
- package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
- package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
- package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
- package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
- package/template/apps/web/src/layout/AppShell.vue +28 -25
- package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
- package/template/apps/web/src/router/business-routes.ts +68 -0
- package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
- package/template/apps/web/src/router/index.ts +7 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/access-api.ts +5 -1
- package/template/apps/web/src/services/access-api.webtest.ts +35 -0
- package/template/docker-compose.yml +127 -0
- package/template/docs/architecture/ai-egress.md +12 -9
- package/template/docs/architecture/ai-runtime.md +23 -11
- package/template/docs/architecture/durable-jobs.md +25 -3
- package/template/docs/architecture/foundation-infrastructure.md +163 -0
- package/template/docs/architecture/module-host.md +115 -0
- package/template/docs/architecture/server-module-interface.md +39 -0
- package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
- package/template/docs/development/business-web-routes.md +66 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/development/module-lifecycle.md +71 -0
- package/template/docs/operations/observability.md +5 -1
- package/template/docs/platform/agent-runtime.md +21 -11
- package/template/docs/platform/agent-tool-sandbox.md +58 -0
- package/template/docs/platform/agent-tools.md +5 -0
- package/template/docs/platform/document-ai-runtime.md +204 -0
- package/template/docs/platform/governed-export-renderers.md +132 -0
- package/template/docs/platform/navigation-registry.md +5 -0
- package/template/docs/platform/organization-membership.md +2 -1
- package/template/docs/platform/speech-ai-runtime.md +156 -0
- package/template/docs/platform/system-crud-api.md +7 -1
- package/template/docs/platform/vision-ai-runtime.md +97 -0
- package/template/docs/platform/web-system-crud.md +6 -0
- package/template/package.json +16 -2
- package/template/packages/ai-client/index.ts +4 -0
- package/template/packages/ai-client/package.json +5 -2
- package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
- package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
- package/template/packages/ai-client/src/agent/client/client.ts +119 -7
- package/template/packages/ai-client/src/agent/client/run.ts +14 -0
- package/template/packages/ai-client/src/agent/client/types.ts +18 -0
- package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
- package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
- package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
- package/template/packages/ai-client/src/agent/index.ts +1 -0
- package/template/packages/ai-client/src/artifact-ref.ts +8 -0
- package/template/packages/ai-client/src/document/client.test.ts +114 -0
- package/template/packages/ai-client/src/document/client.ts +134 -0
- package/template/packages/ai-client/src/document/contracts.ts +152 -0
- package/template/packages/ai-client/src/document/error.ts +43 -0
- package/template/packages/ai-client/src/document/index.ts +3 -0
- package/template/packages/ai-client/src/document/run.ts +104 -0
- package/template/packages/ai-client/src/document/validation.ts +115 -0
- package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
- package/template/packages/ai-client/src/embedding/client.ts +96 -0
- package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
- package/template/packages/ai-client/src/embedding/error.ts +61 -0
- package/template/packages/ai-client/src/embedding/index.ts +3 -0
- package/template/packages/ai-client/src/embedding/validation.ts +117 -0
- package/template/packages/ai-client/src/speech/client.test.ts +135 -0
- package/template/packages/ai-client/src/speech/client.ts +104 -0
- package/template/packages/ai-client/src/speech/contracts.ts +129 -0
- package/template/packages/ai-client/src/speech/error.ts +43 -0
- package/template/packages/ai-client/src/speech/index.ts +3 -0
- package/template/packages/ai-client/src/speech/run.ts +133 -0
- package/template/packages/ai-client/src/speech/validation.ts +132 -0
- package/template/packages/config/package.json +2 -2
- package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
- package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
- package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
- package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
- package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
- package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
- package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
- package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +149 -0
- package/template/packages/database/src/schema/common.ts +2 -14
- package/template/packages/database/src/schema/durable-jobs.ts +5 -0
- package/template/packages/database/src/schema/governed-exports.ts +17 -5
- package/template/packages/database/src/schema/index.ts +3 -0
- package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
- package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
- package/template/packages/database/src/schema.test.ts +6 -23
- package/template/packages/jobs/package.json +3 -2
- package/template/packages/jobs/src/handlers.test.ts +13 -0
- package/template/packages/jobs/src/handlers.ts +7 -1
- package/template/packages/jobs/src/types.ts +4 -0
- package/template/packages/modules/package.json +2 -2
- package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
- package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
- package/template/packages/modules/src/ai-operation-composition.ts +59 -1
- package/template/packages/modules/src/manifest.ts +25 -1
- package/template/packages/modules/src/tool-composition.ts +18 -0
- package/template/packages/server-sdk/index.ts +50 -0
- package/template/packages/server-sdk/package.json +10 -4
- package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
- package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
- package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
- package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
- package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
- package/template/packages/server-sdk/src/durable-handler.ts +29 -0
- package/template/packages/server-sdk/src/jobs-port.ts +69 -0
- package/template/packages/server-sdk/src/module-host.ts +16 -2
- package/template/packages/server-sdk/src/request-context.ts +6 -2
- package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
- package/template/packages/server-sdk/src/tool-registration.ts +2 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/rbac.ts +30 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from .contracts import AgentError, JsonValue
|
|
9
|
+
from .postgres_store_backend import PostgresStoreBackend
|
|
10
|
+
from .sqlite_store import RunStoreCapacityError
|
|
11
|
+
from .store import (
|
|
12
|
+
CanonicalEvent,
|
|
13
|
+
IdempotencyConflictError,
|
|
14
|
+
RunRecord,
|
|
15
|
+
status_for,
|
|
16
|
+
)
|
|
17
|
+
from .store_cipher import RunStoreCipher
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PostgresRunStore:
|
|
21
|
+
"""Multi-replica Run store with encrypted payloads and renewable leases."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
database_url: str,
|
|
26
|
+
cipher: RunStoreCipher,
|
|
27
|
+
*,
|
|
28
|
+
max_runs: int = 100_000,
|
|
29
|
+
max_events_per_run: int = 2_000,
|
|
30
|
+
max_output_bytes: int = 16 * 1024 * 1024,
|
|
31
|
+
terminal_ttl_seconds: int = 7 * 24 * 60 * 60,
|
|
32
|
+
lease_seconds: int = 30,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._cipher = cipher
|
|
35
|
+
self._max_runs = max_runs
|
|
36
|
+
self._max_events = max_events_per_run
|
|
37
|
+
self._max_output_bytes = max_output_bytes
|
|
38
|
+
self._terminal_ttl = terminal_ttl_seconds
|
|
39
|
+
self._lease_seconds = lease_seconds
|
|
40
|
+
self._owner = f"agent_{uuid.uuid4().hex}"
|
|
41
|
+
self._backend = PostgresStoreBackend(
|
|
42
|
+
database_url,
|
|
43
|
+
cipher,
|
|
44
|
+
self._owner,
|
|
45
|
+
terminal_ttl_seconds,
|
|
46
|
+
)
|
|
47
|
+
self._pool = self._backend.pool
|
|
48
|
+
|
|
49
|
+
async def create_or_get(
|
|
50
|
+
self,
|
|
51
|
+
record: RunRecord,
|
|
52
|
+
invocation: JsonValue = None,
|
|
53
|
+
) -> tuple[RunRecord, bool]:
|
|
54
|
+
await self._backend.ready()
|
|
55
|
+
async with self._pool.connection() as connection:
|
|
56
|
+
async with connection.transaction():
|
|
57
|
+
await connection.execute("select pg_advisory_xact_lock(74123001)")
|
|
58
|
+
await self._backend.prune(connection)
|
|
59
|
+
current = await self._backend.by_idempotency(
|
|
60
|
+
connection,
|
|
61
|
+
record.idempotency_key,
|
|
62
|
+
)
|
|
63
|
+
if current:
|
|
64
|
+
if current.fingerprint != record.fingerprint:
|
|
65
|
+
raise IdempotencyConflictError
|
|
66
|
+
return current, False
|
|
67
|
+
count = await connection.execute(
|
|
68
|
+
"select count(*) as value from agent_runtime_runs"
|
|
69
|
+
)
|
|
70
|
+
row = await count.fetchone()
|
|
71
|
+
if not row:
|
|
72
|
+
raise RuntimeError("Agent Run Store 容量查询失败")
|
|
73
|
+
if int(row["value"]) >= self._max_runs:
|
|
74
|
+
raise RunStoreCapacityError("Agent Run Store 容量已满")
|
|
75
|
+
release_id = await self._backend.ensure_release(
|
|
76
|
+
connection,
|
|
77
|
+
record,
|
|
78
|
+
)
|
|
79
|
+
now = time.time()
|
|
80
|
+
inserted = await connection.execute(
|
|
81
|
+
"""
|
|
82
|
+
insert into agent_runtime_runs (
|
|
83
|
+
run_id, operation_key, idempotency_key, fingerprint,
|
|
84
|
+
status, release_id, release_json, trace_id, invocation_cipher,
|
|
85
|
+
lease_owner, lease_until,
|
|
86
|
+
created_at, updated_at, expires_at
|
|
87
|
+
) values (
|
|
88
|
+
%s, %s, %s, %s, 'accepted', %s, %s, %s, %s, %s,
|
|
89
|
+
extract(epoch from clock_timestamp()) + %s, %s, %s, %s
|
|
90
|
+
)
|
|
91
|
+
on conflict (idempotency_key) do nothing
|
|
92
|
+
returning run_id
|
|
93
|
+
""",
|
|
94
|
+
(
|
|
95
|
+
record.run_id,
|
|
96
|
+
record.operation_key,
|
|
97
|
+
record.idempotency_key,
|
|
98
|
+
record.fingerprint,
|
|
99
|
+
release_id,
|
|
100
|
+
json.dumps(record.release, ensure_ascii=False),
|
|
101
|
+
record.trace_id,
|
|
102
|
+
self._cipher.seal(invocation)
|
|
103
|
+
if invocation is not None
|
|
104
|
+
else None,
|
|
105
|
+
self._owner,
|
|
106
|
+
self._lease_seconds,
|
|
107
|
+
now,
|
|
108
|
+
now,
|
|
109
|
+
now + self._terminal_ttl,
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
if await inserted.fetchone() is None:
|
|
113
|
+
current = await self._backend.by_idempotency(
|
|
114
|
+
connection,
|
|
115
|
+
record.idempotency_key,
|
|
116
|
+
)
|
|
117
|
+
if current is None or current.fingerprint != record.fingerprint:
|
|
118
|
+
raise IdempotencyConflictError
|
|
119
|
+
return current, False
|
|
120
|
+
return record, True
|
|
121
|
+
|
|
122
|
+
async def get(self, run_id: str) -> RunRecord | None:
|
|
123
|
+
await self._backend.ready()
|
|
124
|
+
async with self._pool.connection() as connection:
|
|
125
|
+
return await self._backend.read(connection, run_id)
|
|
126
|
+
|
|
127
|
+
async def append(
|
|
128
|
+
self,
|
|
129
|
+
run_id: str,
|
|
130
|
+
event_type: str,
|
|
131
|
+
fields: dict[str, object] | None = None,
|
|
132
|
+
) -> CanonicalEvent | None:
|
|
133
|
+
await self._backend.ready()
|
|
134
|
+
async with self._pool.connection() as connection:
|
|
135
|
+
async with connection.transaction():
|
|
136
|
+
record = await self._backend.locked(connection, run_id)
|
|
137
|
+
if record.terminal:
|
|
138
|
+
return None
|
|
139
|
+
sequence = len(record.events) + 1
|
|
140
|
+
if sequence >= self._max_events and not event_type.startswith("run."):
|
|
141
|
+
event_type = "run.failed"
|
|
142
|
+
fields = failure_fields(
|
|
143
|
+
sequence,
|
|
144
|
+
"Agent 事件超过安全上限",
|
|
145
|
+
)
|
|
146
|
+
event: CanonicalEvent = {
|
|
147
|
+
"type": event_type,
|
|
148
|
+
"runId": run_id,
|
|
149
|
+
"sequence": sequence,
|
|
150
|
+
**(fields or {}),
|
|
151
|
+
}
|
|
152
|
+
await self._backend.insert_event(
|
|
153
|
+
connection,
|
|
154
|
+
run_id,
|
|
155
|
+
event,
|
|
156
|
+
status_for(event_type, record.status),
|
|
157
|
+
)
|
|
158
|
+
return event
|
|
159
|
+
|
|
160
|
+
async def complete(self, run_id: str, output: JsonValue) -> None:
|
|
161
|
+
encoded = json.dumps(output, ensure_ascii=False, separators=(",", ":"))
|
|
162
|
+
await self._backend.ready()
|
|
163
|
+
async with self._pool.connection() as connection:
|
|
164
|
+
async with connection.transaction():
|
|
165
|
+
record = await self._backend.locked(connection, run_id)
|
|
166
|
+
if record.terminal:
|
|
167
|
+
return
|
|
168
|
+
sequence = len(record.events) + 1
|
|
169
|
+
if len(encoded.encode()) > self._max_output_bytes:
|
|
170
|
+
event: CanonicalEvent = {
|
|
171
|
+
"type": "run.failed",
|
|
172
|
+
"runId": run_id,
|
|
173
|
+
"sequence": sequence,
|
|
174
|
+
**failure_fields(sequence, "Agent 输出超过安全上限"),
|
|
175
|
+
}
|
|
176
|
+
await self._backend.insert_event(
|
|
177
|
+
connection,
|
|
178
|
+
run_id,
|
|
179
|
+
event,
|
|
180
|
+
"failed",
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
event = {
|
|
184
|
+
"type": "run.completed",
|
|
185
|
+
"runId": run_id,
|
|
186
|
+
"sequence": sequence,
|
|
187
|
+
}
|
|
188
|
+
await self._backend.insert_event(
|
|
189
|
+
connection,
|
|
190
|
+
run_id,
|
|
191
|
+
event,
|
|
192
|
+
"completed",
|
|
193
|
+
self._cipher.seal(output),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def events_after(
|
|
197
|
+
self,
|
|
198
|
+
run_id: str,
|
|
199
|
+
after: int,
|
|
200
|
+
wait_seconds: float,
|
|
201
|
+
) -> tuple[list[CanonicalEvent], bool]:
|
|
202
|
+
deadline = time.monotonic() + wait_seconds
|
|
203
|
+
while True:
|
|
204
|
+
record = await self.get(run_id)
|
|
205
|
+
if record is None:
|
|
206
|
+
raise KeyError(run_id)
|
|
207
|
+
events = [event for event in record.events if sequence_of(event) > after]
|
|
208
|
+
if events or record.terminal or time.monotonic() >= deadline:
|
|
209
|
+
return events, record.terminal
|
|
210
|
+
await asyncio.sleep(min(0.5, max(0, deadline - time.monotonic())))
|
|
211
|
+
|
|
212
|
+
async def save_invocation(self, run_id: str, value: JsonValue) -> None:
|
|
213
|
+
await self._backend.update_run(
|
|
214
|
+
run_id,
|
|
215
|
+
"invocation_cipher = %s",
|
|
216
|
+
(self._cipher.seal(value),),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def recoverable(self) -> list[tuple[RunRecord, JsonValue]]:
|
|
220
|
+
await self._backend.ready()
|
|
221
|
+
async with self._pool.connection() as connection:
|
|
222
|
+
async with connection.transaction():
|
|
223
|
+
cursor = await connection.execute(
|
|
224
|
+
"""
|
|
225
|
+
select run_id from agent_runtime_runs
|
|
226
|
+
where status not in ('completed', 'failed', 'cancelled')
|
|
227
|
+
and invocation_cipher is not null
|
|
228
|
+
and (
|
|
229
|
+
lease_until < extract(epoch from clock_timestamp())
|
|
230
|
+
or lease_owner = %s
|
|
231
|
+
)
|
|
232
|
+
order by created_at for update skip locked limit 20
|
|
233
|
+
""",
|
|
234
|
+
(self._owner,),
|
|
235
|
+
)
|
|
236
|
+
rows = await cursor.fetchall()
|
|
237
|
+
result: list[tuple[RunRecord, JsonValue]] = []
|
|
238
|
+
for row in rows:
|
|
239
|
+
run_id = str(row["run_id"])
|
|
240
|
+
await connection.execute(
|
|
241
|
+
"""
|
|
242
|
+
update agent_runtime_runs
|
|
243
|
+
set lease_owner = %s,
|
|
244
|
+
lease_until = extract(epoch from clock_timestamp()) + %s
|
|
245
|
+
where run_id = %s
|
|
246
|
+
""",
|
|
247
|
+
(self._owner, self._lease_seconds, run_id),
|
|
248
|
+
)
|
|
249
|
+
record = await self._backend.read(connection, run_id)
|
|
250
|
+
cipher = await connection.execute(
|
|
251
|
+
"""
|
|
252
|
+
select invocation_cipher from agent_runtime_runs
|
|
253
|
+
where run_id = %s
|
|
254
|
+
""",
|
|
255
|
+
(run_id,),
|
|
256
|
+
)
|
|
257
|
+
value = await cipher.fetchone()
|
|
258
|
+
if record and value:
|
|
259
|
+
result.append(
|
|
260
|
+
(
|
|
261
|
+
record,
|
|
262
|
+
self._cipher.open(str(value["invocation_cipher"])),
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
async def clear_invocation(self, run_id: str) -> None:
|
|
268
|
+
await self._backend.update_run(
|
|
269
|
+
run_id,
|
|
270
|
+
"invocation_cipher = null",
|
|
271
|
+
(),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
async def renew_lease(self, run_id: str) -> None:
|
|
275
|
+
await self._backend.update_run(
|
|
276
|
+
run_id,
|
|
277
|
+
"lease_until = extract(epoch from clock_timestamp()) + %s",
|
|
278
|
+
(self._lease_seconds,),
|
|
279
|
+
owner_required=True,
|
|
280
|
+
active_required=True,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def release_lease(self, run_id: str) -> None:
|
|
284
|
+
await self._backend.update_run(
|
|
285
|
+
run_id,
|
|
286
|
+
"lease_owner = null, lease_until = 0",
|
|
287
|
+
(),
|
|
288
|
+
owner_required=True,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def record_evaluation(
|
|
292
|
+
self,
|
|
293
|
+
release_id: str,
|
|
294
|
+
evaluation_set_key: str,
|
|
295
|
+
evaluation_set_version: str,
|
|
296
|
+
metrics: dict[str, float],
|
|
297
|
+
) -> str:
|
|
298
|
+
return await self._backend.record_evaluation(
|
|
299
|
+
release_id,
|
|
300
|
+
evaluation_set_key,
|
|
301
|
+
evaluation_set_version,
|
|
302
|
+
metrics,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
async def close(self) -> None:
|
|
306
|
+
await self._backend.close()
|
|
307
|
+
|
|
308
|
+
async def probe(self) -> None:
|
|
309
|
+
await self._backend.ready()
|
|
310
|
+
async with self._pool.connection() as connection:
|
|
311
|
+
await connection.execute("select 1")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def sequence_of(event: CanonicalEvent) -> int:
|
|
315
|
+
value = event.get("sequence")
|
|
316
|
+
if not isinstance(value, int):
|
|
317
|
+
raise RuntimeError("Agent Event sequence 无效")
|
|
318
|
+
return value
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def failure_fields(sequence: int, message: str) -> dict[str, object]:
|
|
322
|
+
return {
|
|
323
|
+
"error": AgentError(
|
|
324
|
+
code="BUDGET_EXCEEDED",
|
|
325
|
+
message=message,
|
|
326
|
+
request_id=f"request_store_{sequence}",
|
|
327
|
+
retryable=False,
|
|
328
|
+
).model_dump(by_alias=True)
|
|
329
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from psycopg import AsyncConnection
|
|
10
|
+
from psycopg.rows import dict_row
|
|
11
|
+
from psycopg_pool import AsyncConnectionPool
|
|
12
|
+
|
|
13
|
+
from .contracts import JsonValue, RunStatus
|
|
14
|
+
from .store import (
|
|
15
|
+
CanonicalEvent,
|
|
16
|
+
IdempotencyConflictError,
|
|
17
|
+
ReleaseNotFoundError,
|
|
18
|
+
RunRecord,
|
|
19
|
+
)
|
|
20
|
+
from .store_cipher import RunStoreCipher
|
|
21
|
+
|
|
22
|
+
type DatabaseConnection = AsyncConnection[dict[str, Any]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PostgresStoreBackend:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
database_url: str,
|
|
29
|
+
cipher: RunStoreCipher,
|
|
30
|
+
owner: str,
|
|
31
|
+
terminal_ttl_seconds: int,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.pool = cast(
|
|
34
|
+
AsyncConnectionPool[DatabaseConnection],
|
|
35
|
+
AsyncConnectionPool(
|
|
36
|
+
database_url,
|
|
37
|
+
min_size=1,
|
|
38
|
+
max_size=10,
|
|
39
|
+
open=False,
|
|
40
|
+
kwargs={"row_factory": dict_row},
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
self.cipher = cipher
|
|
44
|
+
self.owner = owner
|
|
45
|
+
self.terminal_ttl = terminal_ttl_seconds
|
|
46
|
+
self._open_lock = asyncio.Lock()
|
|
47
|
+
self._opened = False
|
|
48
|
+
|
|
49
|
+
async def ready(self) -> None:
|
|
50
|
+
if self._opened:
|
|
51
|
+
return
|
|
52
|
+
async with self._open_lock:
|
|
53
|
+
if not self._opened:
|
|
54
|
+
await self.pool.open()
|
|
55
|
+
self._opened = True
|
|
56
|
+
|
|
57
|
+
async def close(self) -> None:
|
|
58
|
+
if self._opened:
|
|
59
|
+
await self.pool.close()
|
|
60
|
+
|
|
61
|
+
async def read(
|
|
62
|
+
self,
|
|
63
|
+
connection: DatabaseConnection,
|
|
64
|
+
run_id: str,
|
|
65
|
+
) -> RunRecord | None:
|
|
66
|
+
cursor = await connection.execute(
|
|
67
|
+
"select * from agent_runtime_runs where run_id = %s",
|
|
68
|
+
(run_id,),
|
|
69
|
+
)
|
|
70
|
+
row = await cursor.fetchone()
|
|
71
|
+
if not row:
|
|
72
|
+
return None
|
|
73
|
+
events_cursor = await connection.execute(
|
|
74
|
+
"""
|
|
75
|
+
select event_cipher from agent_runtime_events
|
|
76
|
+
where run_id = %s order by sequence
|
|
77
|
+
""",
|
|
78
|
+
(run_id,),
|
|
79
|
+
)
|
|
80
|
+
events = [
|
|
81
|
+
cast(CanonicalEvent, self.cipher.open(str(item["event_cipher"])))
|
|
82
|
+
for item in await events_cursor.fetchall()
|
|
83
|
+
]
|
|
84
|
+
output = (
|
|
85
|
+
self.cipher.open(str(row["output_cipher"]))
|
|
86
|
+
if row["output_cipher"]
|
|
87
|
+
else None
|
|
88
|
+
)
|
|
89
|
+
return RunRecord(
|
|
90
|
+
run_id=str(row["run_id"]),
|
|
91
|
+
operation_key=str(row["operation_key"]),
|
|
92
|
+
input=None,
|
|
93
|
+
idempotency_key=str(row["idempotency_key"]),
|
|
94
|
+
fingerprint=str(row["fingerprint"]),
|
|
95
|
+
release=cast(JsonValue, row["release_json"]),
|
|
96
|
+
trace_id=str(row["trace_id"]),
|
|
97
|
+
status=cast(RunStatus, str(row["status"])),
|
|
98
|
+
events=events,
|
|
99
|
+
output=output,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def locked(
|
|
103
|
+
self,
|
|
104
|
+
connection: DatabaseConnection,
|
|
105
|
+
run_id: str,
|
|
106
|
+
) -> RunRecord:
|
|
107
|
+
await connection.execute(
|
|
108
|
+
"select run_id from agent_runtime_runs where run_id = %s for update",
|
|
109
|
+
(run_id,),
|
|
110
|
+
)
|
|
111
|
+
record = await self.read(connection, run_id)
|
|
112
|
+
if record is None:
|
|
113
|
+
raise KeyError(run_id)
|
|
114
|
+
return record
|
|
115
|
+
|
|
116
|
+
async def insert_event(
|
|
117
|
+
self,
|
|
118
|
+
connection: DatabaseConnection,
|
|
119
|
+
run_id: str,
|
|
120
|
+
event: CanonicalEvent,
|
|
121
|
+
status: RunStatus,
|
|
122
|
+
output_cipher: str | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
await connection.execute(
|
|
125
|
+
"""
|
|
126
|
+
insert into agent_runtime_events (run_id, sequence, event_cipher)
|
|
127
|
+
values (%s, %s, %s)
|
|
128
|
+
""",
|
|
129
|
+
(
|
|
130
|
+
run_id,
|
|
131
|
+
event["sequence"],
|
|
132
|
+
self.cipher.seal(cast(JsonValue, event)),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
now = time.time()
|
|
136
|
+
await connection.execute(
|
|
137
|
+
"""
|
|
138
|
+
update agent_runtime_runs
|
|
139
|
+
set status = %s, output_cipher = coalesce(%s, output_cipher),
|
|
140
|
+
updated_at = %s, expires_at = %s
|
|
141
|
+
where run_id = %s
|
|
142
|
+
""",
|
|
143
|
+
(
|
|
144
|
+
status,
|
|
145
|
+
output_cipher,
|
|
146
|
+
now,
|
|
147
|
+
now + self.terminal_ttl,
|
|
148
|
+
run_id,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def update_run(
|
|
153
|
+
self,
|
|
154
|
+
run_id: str,
|
|
155
|
+
assignment: str,
|
|
156
|
+
values: tuple[object, ...],
|
|
157
|
+
*,
|
|
158
|
+
owner_required: bool = False,
|
|
159
|
+
active_required: bool = False,
|
|
160
|
+
) -> None:
|
|
161
|
+
await self.ready()
|
|
162
|
+
owner_clause = " and lease_owner = %s" if owner_required else ""
|
|
163
|
+
active_clause = (
|
|
164
|
+
" and status not in ('completed', 'failed', 'cancelled')"
|
|
165
|
+
if active_required
|
|
166
|
+
else ""
|
|
167
|
+
)
|
|
168
|
+
parameters = (
|
|
169
|
+
*values,
|
|
170
|
+
run_id,
|
|
171
|
+
*((self.owner,) if owner_required else ()),
|
|
172
|
+
)
|
|
173
|
+
async with self.pool.connection() as connection:
|
|
174
|
+
cursor = await connection.execute(
|
|
175
|
+
f"update agent_runtime_runs set {assignment} "
|
|
176
|
+
f"where run_id = %s{owner_clause}{active_clause}",
|
|
177
|
+
parameters,
|
|
178
|
+
)
|
|
179
|
+
if cursor.rowcount != 1:
|
|
180
|
+
raise KeyError(run_id)
|
|
181
|
+
|
|
182
|
+
async def by_idempotency(
|
|
183
|
+
self,
|
|
184
|
+
connection: DatabaseConnection,
|
|
185
|
+
idempotency_key: str,
|
|
186
|
+
) -> RunRecord | None:
|
|
187
|
+
cursor = await connection.execute(
|
|
188
|
+
"""
|
|
189
|
+
select run_id from agent_runtime_runs
|
|
190
|
+
where idempotency_key = %s
|
|
191
|
+
""",
|
|
192
|
+
(idempotency_key,),
|
|
193
|
+
)
|
|
194
|
+
row = await cursor.fetchone()
|
|
195
|
+
return await self.read(connection, str(row["run_id"])) if row else None
|
|
196
|
+
|
|
197
|
+
async def ensure_release(
|
|
198
|
+
self,
|
|
199
|
+
connection: DatabaseConnection,
|
|
200
|
+
record: RunRecord,
|
|
201
|
+
) -> str:
|
|
202
|
+
if not isinstance(record.release, dict):
|
|
203
|
+
raise IdempotencyConflictError
|
|
204
|
+
release_id = record.release.get("releaseId")
|
|
205
|
+
if not isinstance(release_id, str):
|
|
206
|
+
raise IdempotencyConflictError
|
|
207
|
+
encoded = json.dumps(
|
|
208
|
+
record.release,
|
|
209
|
+
sort_keys=True,
|
|
210
|
+
separators=(",", ":"),
|
|
211
|
+
)
|
|
212
|
+
await connection.execute(
|
|
213
|
+
"""
|
|
214
|
+
insert into agent_operation_releases (
|
|
215
|
+
release_id, operation_key, snapshot_json, created_at
|
|
216
|
+
) values (%s, %s, %s::jsonb, %s)
|
|
217
|
+
on conflict (release_id) do nothing
|
|
218
|
+
""",
|
|
219
|
+
(release_id, record.operation_key, encoded, time.time()),
|
|
220
|
+
)
|
|
221
|
+
cursor = await connection.execute(
|
|
222
|
+
"""
|
|
223
|
+
select operation_key, snapshot_json from agent_operation_releases
|
|
224
|
+
where release_id = %s
|
|
225
|
+
""",
|
|
226
|
+
(release_id,),
|
|
227
|
+
)
|
|
228
|
+
existing = await cursor.fetchone()
|
|
229
|
+
if not existing:
|
|
230
|
+
raise IdempotencyConflictError
|
|
231
|
+
actual = json.dumps(
|
|
232
|
+
existing["snapshot_json"],
|
|
233
|
+
sort_keys=True,
|
|
234
|
+
separators=(",", ":"),
|
|
235
|
+
)
|
|
236
|
+
if str(existing["operation_key"]) != record.operation_key or actual != encoded:
|
|
237
|
+
raise IdempotencyConflictError
|
|
238
|
+
return release_id
|
|
239
|
+
|
|
240
|
+
async def prune(self, connection: DatabaseConnection) -> None:
|
|
241
|
+
await connection.execute(
|
|
242
|
+
"""
|
|
243
|
+
delete from agent_runtime_runs
|
|
244
|
+
where status in ('completed', 'failed', 'cancelled')
|
|
245
|
+
and expires_at < %s
|
|
246
|
+
""",
|
|
247
|
+
(time.time(),),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def record_evaluation(
|
|
251
|
+
self,
|
|
252
|
+
release_id: str,
|
|
253
|
+
evaluation_set_key: str,
|
|
254
|
+
evaluation_set_version: str,
|
|
255
|
+
metrics: dict[str, float],
|
|
256
|
+
) -> str:
|
|
257
|
+
await self.ready()
|
|
258
|
+
encoded = json.dumps(metrics, sort_keys=True, separators=(",", ":"))
|
|
259
|
+
async with self.pool.connection() as connection:
|
|
260
|
+
async with connection.transaction():
|
|
261
|
+
release = await connection.execute(
|
|
262
|
+
"""
|
|
263
|
+
select release_id from agent_operation_releases
|
|
264
|
+
where release_id = %s
|
|
265
|
+
""",
|
|
266
|
+
(release_id,),
|
|
267
|
+
)
|
|
268
|
+
if await release.fetchone() is None:
|
|
269
|
+
raise ReleaseNotFoundError
|
|
270
|
+
cursor = await connection.execute(
|
|
271
|
+
"""
|
|
272
|
+
select evaluation_id, metrics_json
|
|
273
|
+
from agent_evaluation_results
|
|
274
|
+
where release_id = %s and evaluation_set_key = %s
|
|
275
|
+
and evaluation_set_version = %s
|
|
276
|
+
""",
|
|
277
|
+
(release_id, evaluation_set_key, evaluation_set_version),
|
|
278
|
+
)
|
|
279
|
+
existing = await cursor.fetchone()
|
|
280
|
+
if existing:
|
|
281
|
+
actual = json.dumps(
|
|
282
|
+
existing["metrics_json"],
|
|
283
|
+
sort_keys=True,
|
|
284
|
+
separators=(",", ":"),
|
|
285
|
+
)
|
|
286
|
+
if actual != encoded:
|
|
287
|
+
raise IdempotencyConflictError
|
|
288
|
+
return str(existing["evaluation_id"])
|
|
289
|
+
evaluation_id = f"eval_{uuid.uuid4().hex}"
|
|
290
|
+
inserted = await connection.execute(
|
|
291
|
+
"""
|
|
292
|
+
insert into agent_evaluation_results (
|
|
293
|
+
evaluation_id, release_id, evaluation_set_key,
|
|
294
|
+
evaluation_set_version, metrics_json, created_at
|
|
295
|
+
) values (%s, %s, %s, %s, %s::jsonb, %s)
|
|
296
|
+
on conflict (
|
|
297
|
+
release_id, evaluation_set_key, evaluation_set_version
|
|
298
|
+
) do nothing
|
|
299
|
+
returning evaluation_id
|
|
300
|
+
""",
|
|
301
|
+
(
|
|
302
|
+
evaluation_id,
|
|
303
|
+
release_id,
|
|
304
|
+
evaluation_set_key,
|
|
305
|
+
evaluation_set_version,
|
|
306
|
+
encoded,
|
|
307
|
+
time.time(),
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
row = await inserted.fetchone()
|
|
311
|
+
if row:
|
|
312
|
+
return str(row["evaluation_id"])
|
|
313
|
+
repeated = await connection.execute(
|
|
314
|
+
"""
|
|
315
|
+
select evaluation_id, metrics_json
|
|
316
|
+
from agent_evaluation_results
|
|
317
|
+
where release_id = %s and evaluation_set_key = %s
|
|
318
|
+
and evaluation_set_version = %s
|
|
319
|
+
""",
|
|
320
|
+
(release_id, evaluation_set_key, evaluation_set_version),
|
|
321
|
+
)
|
|
322
|
+
raced = await repeated.fetchone()
|
|
323
|
+
if not raced:
|
|
324
|
+
raise IdempotencyConflictError
|
|
325
|
+
actual = json.dumps(
|
|
326
|
+
raced["metrics_json"],
|
|
327
|
+
sort_keys=True,
|
|
328
|
+
separators=(",", ":"),
|
|
329
|
+
)
|
|
330
|
+
if actual != encoded:
|
|
331
|
+
raise IdempotencyConflictError
|
|
332
|
+
return str(raced["evaluation_id"])
|