agentengine-sdk-python 0.8.0__py3-none-any.whl → 0.8.2__py3-none-any.whl
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.
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +43 -7
- agentengine_sdk_python-0.8.2.dist-info/RECORD +778 -0
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +1 -1
- ksadk/a2a/__init__.py +2 -0
- ksadk/a2a/_space_client_events.py +381 -0
- ksadk/a2a/bootstrap.py +0 -2
- ksadk/a2a/card.py +6 -111
- ksadk/a2a/control_plane.py +239 -1
- ksadk/a2a/event_adapter.py +156 -64
- ksadk/a2a/executor.py +195 -172
- ksadk/a2a/routes.py +12 -5
- ksadk/a2a/server.py +1 -8
- ksadk/a2a/service_env.py +33 -0
- ksadk/a2a/space_client.py +43 -349
- ksadk/a2a_card.py +101 -0
- ksadk/a2ui/core.py +222 -53
- ksadk/agui/_agent_helpers.py +243 -0
- ksadk/agui/a2ui_projection.py +12 -5
- ksadk/agui/agent.py +474 -275
- ksadk/agui/config.py +11 -5
- ksadk/agui/routes.py +7 -15
- ksadk/api/client.py +530 -40
- ksadk/api/environment.py +27 -0
- ksadk/builders/code_builder.py +352 -53
- ksadk/builders/container_builder.py +71 -44
- ksadk/builders/framework_requirements.py +2 -1
- ksadk/builders/ks3_uploader.py +2 -2
- ksadk/builders/managed_runtime_builder.py +60 -28
- ksadk/cli/__init__.py +25 -0
- ksadk/cli/cmd_a2a.py +18 -31
- ksadk/cli/cmd_create.py +13 -26
- ksadk/cli/cmd_dashboard.py +15 -1
- ksadk/cli/cmd_deploy.py +28 -3
- ksadk/cli/cmd_eval.py +443 -0
- ksadk/cli/cmd_evalset.py +284 -0
- ksadk/cli/cmd_files.py +5 -1
- ksadk/cli/cmd_hermes.py +137 -163
- ksadk/cli/cmd_invoke.py +28 -38
- ksadk/cli/cmd_launch.py +12 -0
- ksadk/cli/cmd_managed_runtime.py +95 -0
- ksadk/cli/cmd_observe.py +85 -0
- ksadk/cli/cmd_openclaw.py +85 -479
- ksadk/cli/cmd_replay.py +28 -4
- ksadk/cli/cmd_run.py +29 -67
- ksadk/cli/cmd_studio.py +201 -0
- ksadk/cli/cmd_web.py +15 -6
- ksadk/cli/dry_run.py +2 -1
- ksadk/cli/env_options.py +83 -0
- ksadk/cli/global_options.py +3 -1
- ksadk/cli/hermes_env.py +151 -0
- ksadk/cli/invoke_payload.py +49 -0
- ksadk/cli/network_options.py +7 -5
- ksadk/cli/openclaw_env.py +458 -0
- ksadk/cli/runtime_bootstrap.py +51 -0
- ksadk/cli/ui.py +6 -0
- ksadk/cli/workflow_common.py +5 -5
- ksadk/codex/__init__.py +3 -3
- ksadk/codex/client.py +833 -49
- ksadk/codex/phase.py +1 -1
- ksadk/codex/runtime.py +720 -127
- ksadk/common/kop_client.py +232 -0
- ksadk/configs/__init__.py +1 -3
- ksadk/configs/env_registry.py +244 -45
- ksadk/configs/env_registry_pcm.py +181 -0
- ksadk/configs/env_var_spec.py +18 -0
- ksadk/configs/global_config.py +6 -1
- ksadk/configs/settings.py +5 -108
- ksadk/context_engine/__init__.py +98 -0
- ksadk/context_engine/assembler.py +176 -0
- ksadk/context_engine/baseline.py +419 -0
- ksadk/context_engine/cache_observability.py +228 -0
- ksadk/context_engine/capabilities.py +443 -0
- ksadk/context_engine/contributors.py +344 -0
- ksadk/context_engine/hosted_pipeline.py +417 -0
- ksadk/context_engine/models.py +117 -0
- ksadk/context_engine/planner.py +698 -0
- ksadk/context_engine/policies.py +319 -0
- ksadk/context_engine/projection.py +31 -0
- ksadk/context_engine/shadow_plan.py +266 -0
- ksadk/context_engine/tokenizer.py +164 -0
- ksadk/conversations/__init__.py +85 -94
- ksadk/conversations/context.py +126 -6
- ksadk/conversations/message_projection.py +292 -28
- ksadk/conversations/model_context.py +49 -3
- ksadk/conversations/runtime_compaction.py +207 -5
- ksadk/conversations/runtime_input.py +193 -3
- ksadk/conversations/runtime_invocation.py +81 -3
- ksadk/conversations/runtime_observability.py +157 -4
- ksadk/conversations/runtime_payloads.py +42 -0
- ksadk/conversations/runtime_preparation.py +280 -2
- ksadk/conversations/runtime_resume.py +71 -32
- ksadk/conversations/runtime_stream_events.py +94 -9
- ksadk/conversations/runtime_streaming.py +175 -22
- ksadk/conversations/semantic_summary.py +311 -1
- ksadk/conversations/session_lock.py +51 -0
- ksadk/deployment/env_forward.py +72 -0
- ksadk/deployment/managed_runtime.py +38 -0
- ksadk/deployment/manager.py +24 -8
- ksadk/deployment/providers/serverless.py +228 -92
- ksadk/evaluation/__init__.py +116 -0
- ksadk/evaluation/a2a_adapter.py +452 -0
- ksadk/evaluation/adapters.py +67 -0
- ksadk/evaluation/agent_eval_client.py +395 -0
- ksadk/evaluation/cloud_binding.py +80 -0
- ksadk/evaluation/cloud_converter.py +194 -0
- ksadk/evaluation/cloud_service.py +199 -0
- ksadk/evaluation/contracts.py +478 -0
- ksadk/evaluation/evalset.py +351 -0
- ksadk/evaluation/evaluators.py +627 -0
- ksadk/evaluation/evidence.py +258 -0
- ksadk/evaluation/executor.py +143 -0
- ksadk/evaluation/local_adapter.py +550 -0
- ksadk/evaluation/service_env.py +27 -0
- ksadk/evaluation/storage.py +68 -0
- ksadk/evaluation/studio_build_adapter.py +253 -0
- ksadk/evaluation/target.py +82 -0
- ksadk/events/__init__.py +13 -13
- ksadk/events/_v1_compat/__init__.py +1 -0
- ksadk/events/_v1_compat/models.py +299 -0
- ksadk/events/_v1_compat/parser.py +247 -0
- ksadk/events/_v1_compat/projection.py +753 -0
- ksadk/events/adapters/__init__.py +34 -0
- ksadk/events/adapters/_a2a_snapshot.py +622 -0
- ksadk/events/adapters/_a2a_support.py +275 -0
- ksadk/events/adapters/_codex_interactions.py +375 -0
- ksadk/events/adapters/_codex_items.py +801 -0
- ksadk/events/adapters/_codex_validators.py +311 -0
- ksadk/events/adapters/_langgraph_support.py +785 -0
- ksadk/events/adapters/a2a.py +841 -0
- ksadk/events/adapters/adk.py +503 -0
- ksadk/events/adapters/codex.py +717 -0
- ksadk/events/adapters/langgraph.py +745 -0
- ksadk/events/canonical.py +440 -0
- ksadk/events/canonical_replay.py +497 -0
- ksadk/events/canonical_store.py +468 -0
- ksadk/events/cold_recovery.py +257 -0
- ksadk/events/content.py +89 -0
- ksadk/events/identity.py +86 -0
- ksadk/events/pipeline.py +455 -0
- ksadk/events/projections.py +66 -0
- ksadk/events/reducer.py +661 -0
- ksadk/events/replay.py +3 -36
- ksadk/events/runtime_event.py +20 -256
- ksadk/events/session_event.py +324 -0
- ksadk/events/store.py +3 -316
- ksadk/events/v1_compat.py +41 -0
- ksadk/harness/__init__.py +3 -2
- ksadk/harness/app.py +114 -123
- ksadk/harness/config.py +1 -1
- ksadk/harness/runtime.py +467 -0
- ksadk/hermes_terminal.py +5 -0
- ksadk/identity/resolver.py +4 -3
- ksadk/interaction/__init__.py +28 -0
- ksadk/interaction/contracts.py +136 -0
- ksadk/interaction/ledger.py +189 -0
- ksadk/interaction/provider.py +122 -0
- ksadk/interaction/providers/__init__.py +52 -0
- ksadk/interaction/providers/adk.py +40 -0
- ksadk/interaction/providers/codex.py +94 -0
- ksadk/interaction/providers/langgraph.py +58 -0
- ksadk/kernel/__init__.py +117 -0
- ksadk/kernel/authorization.py +258 -0
- ksadk/kernel/bootstrap.py +1074 -0
- ksadk/kernel/contract_fingerprints.py +61 -0
- ksadk/kernel/contracts.py +362 -0
- ksadk/kernel/control.py +219 -0
- ksadk/kernel/errors.py +79 -0
- ksadk/kernel/ingress.py +1102 -0
- ksadk/kernel/mapping.py +111 -0
- ksadk/kernel/memory_store.py +1133 -0
- ksadk/kernel/postgres_store.py +1756 -0
- ksadk/kernel/recovery.py +452 -0
- ksadk/kernel/runtime_identity.py +103 -0
- ksadk/kernel/sql/001_agent_kernel.sql +120 -0
- ksadk/kernel/sqlite_store.py +1410 -0
- ksadk/kernel/state.py +145 -0
- ksadk/kernel/store.py +232 -0
- ksadk/kernel/worker.py +953 -0
- ksadk/knowledge_base/client.py +22 -4
- ksadk/knowledge_base/service.py +32 -5
- ksadk/managed_a2a_card.py +160 -0
- ksadk/managed_runtime.py +16 -9
- ksadk/memory/__init__.py +61 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
- ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
- ksadk/memory/adk/backends/sdk_ltm_backend.py +394 -5
- ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
- ksadk/memory/adk/long_term_memory.py +3 -2
- ksadk/memory/coordinator.py +461 -0
- ksadk/memory/events.py +202 -0
- ksadk/memory/extraction.py +228 -0
- ksadk/memory/ltm_backend_factory.py +16 -3
- ksadk/memory/models.py +316 -0
- ksadk/memory/policy.py +215 -0
- ksadk/memory/provider.py +47 -0
- ksadk/memory/provider_adapter.py +159 -0
- ksadk/memory/provider_resolver.py +71 -0
- ksadk/memory/providers/__init__.py +5 -0
- ksadk/memory/providers/local_sqlite.py +490 -0
- ksadk/memory/resolved_policy.py +145 -0
- ksadk/memory/service.py +198 -8
- ksadk/memory/tool.py +4 -0
- ksadk/model_proxy/bootstrap.py +16 -9
- ksadk/model_proxy/config.py +22 -1
- ksadk/model_proxy/detect.py +202 -9
- ksadk/model_proxy/namespace.py +67 -15
- ksadk/model_proxy/server.py +168 -6
- ksadk/model_proxy/transform.py +105 -3
- ksadk/observability/__init__.py +25 -0
- ksadk/observability/session_log.py +420 -0
- ksadk/observability/trajectory.py +105 -0
- ksadk/prompts/__init__.py +71 -0
- ksadk/prompts/compiler.py +215 -0
- ksadk/prompts/models.py +82 -0
- ksadk/prompts/projection.py +89 -0
- ksadk/prompts/resolved.py +137 -0
- ksadk/prompts/sources.py +233 -0
- ksadk/runners/_langgraph_runner_streams.py +822 -0
- ksadk/runners/adk_runner.py +14 -31
- ksadk/runners/base_runner.py +44 -3
- ksadk/runners/factory.py +3 -3
- ksadk/runners/langgraph_runner.py +252 -433
- ksadk/runners/utils/__init__.py +0 -10
- ksadk/runners/utils/loader.py +16 -0
- ksadk/runtime/__init__.py +67 -41
- ksadk/runtime/_runner_adapter/__init__.py +1 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
- ksadk/runtime/adapter.py +199 -16
- ksadk/runtime/conversation_execution.py +687 -0
- ksadk/runtime/executor.py +392 -0
- ksadk/runtime/factory.py +342 -0
- ksadk/runtime/framework_adapters.py +6 -12
- ksadk/runtime/hosted_finalizer.py +268 -0
- ksadk/runtime/launch.py +61 -0
- ksadk/runtime/preprocessing.py +49 -14
- ksadk/runtime/runner_adapter.py +330 -391
- ksadk/runtime/runner_loading.py +36 -0
- ksadk/runtime/usage.py +33 -0
- ksadk/sandbox/backends/e2b.py +36 -3
- ksadk/server/__init__.py +39 -4
- ksadk/server/app.py +18 -492
- ksadk/server/composition.py +36 -2
- ksadk/server/factory.py +210 -149
- ksadk/server/routes/common.py +15 -46
- ksadk/server/routes/control.py +13 -9
- ksadk/server/routes/kernel_ingress.py +258 -0
- ksadk/server/routes/misc.py +10 -33
- ksadk/server/routes/models.py +14 -6
- ksadk/server/routes/openai_compat.py +129 -27
- ksadk/server/routes/projection.py +84 -16
- ksadk/server/routes/run.py +347 -460
- ksadk/server/routes/sessions.py +37 -40
- ksadk/server/routes/streaming.py +52 -7
- ksadk/server/routes/workspace.py +54 -21
- ksadk/server/static/assets/{ArtifactsPanel-Cjnqt-8W.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-BqyqZfuB.js → CodeBlock-DOH6MVcn.js} +4 -4
- ksadk/server/static/assets/{MathMessageMarkdown-BpH7jiFM.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
- ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
- ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
- ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
- ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
- ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
- ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
- ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
- ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
- ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
- ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
- ksadk/server/static/assets/{chunk-FMBD7UC4-BGhvzDYt.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-D98vPd-E.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
- ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
- ksadk/server/static/assets/{chunk-4BX2VUAB-CsWIQRhc.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
- ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
- ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
- ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
- ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
- ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-S5V4N54A-CX3yotHq.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
- ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
- ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
- ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
- ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
- ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
- ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
- ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
- ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
- ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
- ksadk/server/static/assets/{dist-D82mtD2s.js → dist-8hqcx0sU.js} +1 -1
- ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
- ksadk/server/static/assets/{dist-DHn6nBzr.js → dist-BE7bu-DL.js} +1 -1
- ksadk/server/static/assets/{dist-eoW6tZXA.js → dist-BVDffZ0U.js} +1 -1
- ksadk/server/static/assets/{dist-C2xjE7cO.js → dist-BX8z72IC.js} +1 -1
- ksadk/server/static/assets/{dist-CgF3fj15.js → dist-Bf2M8m3N.js} +1 -1
- ksadk/server/static/assets/{dist-Bs-jQcRX.js → dist-BjU6y-A2.js} +1 -1
- ksadk/server/static/assets/{dist-Djgl2wVZ.js → dist-BytlxIDT.js} +1 -1
- ksadk/server/static/assets/{dist-CVQRZjPu.js → dist-CSh_DE14.js} +1 -1
- ksadk/server/static/assets/{dist-6CVKuTAe.js → dist-CXYYBw3_.js} +1 -1
- ksadk/server/static/assets/{dist-DlzCRy0S.js → dist-C_wsv-Qd.js} +1 -1
- ksadk/server/static/assets/{dist-DF06ORQo.js → dist-ClxGviKw.js} +1 -1
- ksadk/server/static/assets/{dist-Cf23NY4L.js → dist-CttYUqzS.js} +1 -1
- ksadk/server/static/assets/{dist-ivOya1LD.js → dist-DNp6rLRA.js} +1 -1
- ksadk/server/static/assets/{dist-BibcG1V9.js → dist-DUX-id_F.js} +1 -1
- ksadk/server/static/assets/{dist-BUHRxvDG.js → dist-DmldrHd_.js} +1 -1
- ksadk/server/static/assets/{dist-BZ9n9AjO.js → dist-DnfXs8Vn.js} +2 -2
- ksadk/server/static/assets/{dist-DqNnNFrw.js → dist-Dq9gLD7L.js} +1 -1
- ksadk/server/static/assets/{dist-CGcp2ex8.js → dist-W-TIVntx.js} +1 -1
- ksadk/server/static/assets/{dist-Cwt7koer.js → dist-dDdADKFA.js} +1 -1
- ksadk/server/static/assets/{dist-C7-3jInC.js → dist-dydu68Fl.js} +1 -1
- ksadk/server/static/assets/{dist-C99THtIA.js → dist-ngtN0DJB.js} +1 -1
- ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
- ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
- ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
- ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
- ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
- ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
- ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
- ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-DLOHGvWw.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-BIzKeejT.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-BWHoFSw2.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
- ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
- ksadk/server/static/assets/{linear-CvMBw9cA.js → linear-BHjG8b-J.js} +1 -1
- ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-CSAQxEAe.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
- ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
- ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
- ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
- ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-CxIR4JXI.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
- ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
- ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
- ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-DpGa6EAV.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-NTKNGGe5.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
- ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
- ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
- ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-jqoYs3jQ.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
- ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
- ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
- ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
- ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
- ksadk/server/static/index.html +2 -2
- ksadk/sessions/__init__.py +26 -9
- ksadk/sessions/_local_service_sync.py +688 -0
- ksadk/sessions/_local_tables.py +21 -0
- ksadk/sessions/_postgres_schema.py +305 -0
- ksadk/sessions/_postgres_tables.py +15 -0
- ksadk/sessions/base.py +80 -1
- ksadk/sessions/in_memory.py +53 -2
- ksadk/sessions/local_service.py +63 -604
- ksadk/sessions/postgres_service.py +58 -148
- ksadk/sessions/resilient.py +35 -1
- ksadk/skills/runtime/backends/e2b.py +1 -1
- ksadk/skills/service_client.py +13 -3
- ksadk/studio/__init__.py +5 -0
- ksadk/studio/agent_avatar_assets.py +120 -0
- ksadk/studio/agent_lifecycle.py +51 -0
- ksadk/studio/api.py +1598 -0
- ksadk/studio/api_catalog_routes.py +324 -0
- ksadk/studio/api_contracts.py +286 -0
- ksadk/studio/api_helpers.py +132 -0
- ksadk/studio/api_memory_routes.py +82 -0
- ksadk/studio/authoring.py +749 -0
- ksadk/studio/authoring_coordinator.py +1017 -0
- ksadk/studio/builder.py +397 -0
- ksadk/studio/capabilities.py +368 -0
- ksadk/studio/cloud.py +2099 -0
- ksadk/studio/codex_agent_service.py +694 -0
- ksadk/studio/codex_authoring.py +469 -0
- ksadk/studio/codex_builder.py +437 -0
- ksadk/studio/codex_manifest.py +307 -0
- ksadk/studio/codex_run.py +395 -0
- ksadk/studio/compiler.py +190 -0
- ksadk/studio/contracts.py +755 -0
- ksadk/studio/errors.py +47 -0
- ksadk/studio/event_store.py +214 -0
- ksadk/studio/framework_run.py +342 -0
- ksadk/studio/hosted_kernel.py +317 -0
- ksadk/studio/identifiers.py +33 -0
- ksadk/studio/manifest_resolver.py +131 -0
- ksadk/studio/mcp_runtime.py +229 -0
- ksadk/studio/model_client.py +629 -0
- ksadk/studio/model_profile_service.py +214 -0
- ksadk/studio/operations.py +275 -0
- ksadk/studio/otel_trace.py +1060 -0
- ksadk/studio/pagination.py +182 -0
- ksadk/studio/pcm_memory.py +85 -0
- ksadk/studio/python_tool_inspection.py +183 -0
- ksadk/studio/repository.py +327 -0
- ksadk/studio/resource_catalog.py +1260 -0
- ksadk/studio/run_service.py +1133 -0
- ksadk/studio/runtime_catalog.py +74 -0
- ksadk/studio/runtime_source.py +365 -0
- ksadk/studio/service.py +2249 -0
- ksadk/studio/shared_web.py +1004 -0
- ksadk/studio/skill_discovery.py +628 -0
- ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
- ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
- ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
- ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
- ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- ksadk/studio/static/index.html +15 -0
- ksadk/studio/templates.py +722 -0
- ksadk/studio/validator.py +265 -0
- ksadk/studio/workspace.py +135 -0
- ksadk/tools/gateway.py +9 -0
- ksadk/toolsets/__init__.py +27 -1
- ksadk/toolsets/a2a.py +426 -0
- ksadk/tracing/__init__.py +19 -21
- ksadk/tracing/setup.py +317 -403
- ksadk/tracing/span_utils.py +0 -44
- ksadk/version.py +1 -1
- ksadk_runtime_common/memory_backend/registry.py +56 -56
- ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
- ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
- ksadk_runtime_common/workspace_files/router.py +8 -2
- agentengine_sdk_python-0.8.0.dist-info/RECORD +0 -578
- ksadk/a2a/card_builder.py +0 -64
- ksadk/a2a/client.py +0 -309
- ksadk/events/parser.py +0 -191
- ksadk/harness/runner.py +0 -211
- ksadk/runners/codex_runner.py +0 -132
- ksadk/runners/utils/langfuse.py +0 -230
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
- ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +0 -303
- ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +0 -1
- ksadk/server/static/assets/arc-CFKwSk_B.js +0 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +0 -1
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-Dej8jX8c.js +0 -36
- ksadk/server/static/assets/blockDiagram-GPEHLZMM-1F64jqvy.js +0 -132
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-9q7AUAKb.js +0 -10
- ksadk/server/static/assets/channel-iUGv2N24.js +0 -1
- ksadk/server/static/assets/chunk-2J33WTMH-6qgwJm7Q.js +0 -1
- ksadk/server/static/assets/chunk-55IACEB6-BK8paA20.js +0 -1
- ksadk/server/static/assets/chunk-727SXJPM-B-JEhy7h.js +0 -206
- ksadk/server/static/assets/chunk-AQP2D5EJ-DYBvMHAa.js +0 -231
- ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +0 -1
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +0 -1
- ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
- ksadk/server/static/assets/dagre-BM42HDAG-C7TguAWo.js +0 -4
- ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
- ksadk/server/static/assets/diagram-2AECGRRQ-Dd17_kws.js +0 -43
- ksadk/server/static/assets/diagram-5GNKFQAL-DVIClIil.js +0 -10
- ksadk/server/static/assets/diagram-KO2AKTUF-Df_3gYQa.js +0 -3
- ksadk/server/static/assets/diagram-LMA3HP47-BhLk6ZtA.js +0 -24
- ksadk/server/static/assets/diagram-OG6HWLK6-D4GNGvuu.js +0 -24
- ksadk/server/static/assets/dist-DyvYLMW9.js +0 -1
- ksadk/server/static/assets/erDiagram-TEJ5UH35-CnfRcY2M.js +0 -85
- ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +0 -1
- ksadk/server/static/assets/flowDiagram-I6XJVG4X-DGaJjGNJ.js +0 -162
- ksadk/server/static/assets/ganttDiagram-6RSMTGT7-DpCoB2g9.js +0 -292
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +0 -1
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-9IKVFBVT.js +0 -106
- ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
- ksadk/server/static/assets/index-QSwCHuB8.js +0 -177
- ksadk/server/static/assets/index-jOPmTOZH.css +0 -2
- ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +0 -1
- ksadk/server/static/assets/infoDiagram-5YYISTIA-CYIIsb8y.js +0 -2
- ksadk/server/static/assets/katex-DATssjp1.js +0 -265
- ksadk/server/static/assets/mermaid-parser.core-mGAcEwcq.js +0 -161
- ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +0 -1
- ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +0 -1
- ksadk/server/static/assets/pieDiagram-4H26LBE5-By-ra07M.js +0 -30
- ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +0 -1
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DpW90iu3.js +0 -84
- ksadk/server/static/assets/stateDiagram-AJRCARHV-DGpd9yFG.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +0 -1
- ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +0 -1
- ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +0 -1
- ksadk/server/static/assets/vennDiagram-CIIHVFJN-l3Z7-wbP.js +0 -34
- ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +0 -1
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-BR54GPmr.js +0 -78
- ksadk/server/static/assets/xychartDiagram-2RQKCTM6-SkiH8GsK.js +0 -7
- ksadk/tracing/exporters/langfuse_exporter.py +0 -460
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""SQLite ``AgentKernelStore``(Phase 1 Task 3 Step 5)。
|
|
3
|
+
|
|
4
|
+
单文件 durable Inbox / Run / ActivationLease 存储:
|
|
5
|
+
- WAL journal + 每次 mutation ``BEGIN IMMEDIATE`` 做跨进程 CAS;
|
|
6
|
+
- schema migration 用 ``PRAGMA user_version`` 整数版本,重复启动幂等;
|
|
7
|
+
- 所有 fence 比较都发生在同一个写事务内,不匹配抛 :class:`StaleFenceError`;
|
|
8
|
+
- ControlEvent/v1 经注入的 SessionEventStore 追加;accepted 事件在 kernel
|
|
9
|
+
事务 commit 之前追加(persist-before-ack,见 :meth:`accept_command`),
|
|
10
|
+
事件写入失败时回滚 Inbox。
|
|
11
|
+
|
|
12
|
+
只面向单机本地部署(local dev / serverless pod 单写者场景);预发多写者
|
|
13
|
+
场景由 Task 4 的 PostgreSQL 适配器承接。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from uuid import uuid4
|
|
26
|
+
|
|
27
|
+
import aiosqlite
|
|
28
|
+
|
|
29
|
+
from ksadk.events.session_event import (
|
|
30
|
+
SessionEventStore,
|
|
31
|
+
SessionServiceEventStore,
|
|
32
|
+
envelope_to_session_event,
|
|
33
|
+
session_event_storage_id,
|
|
34
|
+
session_event_to_envelope,
|
|
35
|
+
validate_write_guard,
|
|
36
|
+
)
|
|
37
|
+
from ksadk.interaction.contracts import (
|
|
38
|
+
InteractionRecord,
|
|
39
|
+
InteractionReceipt,
|
|
40
|
+
InteractionSubmission,
|
|
41
|
+
is_terminal,
|
|
42
|
+
)
|
|
43
|
+
from ksadk.interaction.ledger import (
|
|
44
|
+
ALREADY_RESOLVED,
|
|
45
|
+
REVISION_MISMATCH,
|
|
46
|
+
REQUEST_CONFLICT,
|
|
47
|
+
interaction_event,
|
|
48
|
+
request_digest,
|
|
49
|
+
requested_event_payload,
|
|
50
|
+
resolve_outcome,
|
|
51
|
+
submission_digest,
|
|
52
|
+
)
|
|
53
|
+
from ksadk.kernel.contracts import (
|
|
54
|
+
ActivationLease,
|
|
55
|
+
ActivationWriteGuard,
|
|
56
|
+
AdmissionWriteGuard,
|
|
57
|
+
AgentControlCommand,
|
|
58
|
+
AgentControlReceipt,
|
|
59
|
+
ControlError,
|
|
60
|
+
SessionEventEnvelope,
|
|
61
|
+
)
|
|
62
|
+
from ksadk.kernel.errors import InvalidCommandError, StaleFenceError
|
|
63
|
+
from ksadk.kernel.state import (
|
|
64
|
+
InboxState,
|
|
65
|
+
assert_inbox_transition,
|
|
66
|
+
assert_run_transition,
|
|
67
|
+
is_active_run,
|
|
68
|
+
)
|
|
69
|
+
from ksadk.kernel.store import (
|
|
70
|
+
ActivationLeaseRequest,
|
|
71
|
+
InboxMessage,
|
|
72
|
+
RunRecord,
|
|
73
|
+
command_digest,
|
|
74
|
+
control_event,
|
|
75
|
+
new_message_id,
|
|
76
|
+
now_iso,
|
|
77
|
+
)
|
|
78
|
+
from ksadk.sessions._local_tables import KSADK_EVENTS_TABLE, KSADK_SESSIONS_TABLE
|
|
79
|
+
from ksadk.sessions.base import SessionEvent
|
|
80
|
+
from ksadk.sessions.local_service import LocalSessionService
|
|
81
|
+
|
|
82
|
+
SCHEMA_VERSION = 2
|
|
83
|
+
|
|
84
|
+
_SCHEMA = """
|
|
85
|
+
CREATE TABLE IF NOT EXISTS kernel_inbox (
|
|
86
|
+
message_id TEXT PRIMARY KEY,
|
|
87
|
+
agent_instance_id TEXT NOT NULL,
|
|
88
|
+
session_id TEXT NOT NULL,
|
|
89
|
+
idempotency_key TEXT NOT NULL,
|
|
90
|
+
request_digest TEXT NOT NULL,
|
|
91
|
+
accepted_seq INTEGER NOT NULL,
|
|
92
|
+
status TEXT NOT NULL CHECK (status IN ('accepted','claimed','completed','discarded')),
|
|
93
|
+
claimed_fence INTEGER,
|
|
94
|
+
payload_json TEXT NOT NULL,
|
|
95
|
+
UNIQUE(session_id, idempotency_key)
|
|
96
|
+
);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_kernel_inbox_claim
|
|
98
|
+
ON kernel_inbox (agent_instance_id, session_id, status, accepted_seq);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_kernel_inbox_idempotency
|
|
100
|
+
ON kernel_inbox (session_id, idempotency_key);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS kernel_runs (
|
|
103
|
+
run_id TEXT PRIMARY KEY,
|
|
104
|
+
agent_instance_id TEXT NOT NULL,
|
|
105
|
+
session_id TEXT NOT NULL,
|
|
106
|
+
state TEXT NOT NULL CHECK (state IN (
|
|
107
|
+
'pending','running','paused','waiting','completed','failed','cancelled','interrupted'
|
|
108
|
+
)),
|
|
109
|
+
activation_fence INTEGER NOT NULL DEFAULT 0,
|
|
110
|
+
created_at TEXT NOT NULL,
|
|
111
|
+
updated_at TEXT NOT NULL,
|
|
112
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
113
|
+
);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_kernel_runs_session_state
|
|
115
|
+
ON kernel_runs (session_id, state);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE IF NOT EXISTS kernel_activations (
|
|
118
|
+
agent_instance_id TEXT NOT NULL,
|
|
119
|
+
session_id TEXT NOT NULL,
|
|
120
|
+
activation_id TEXT NOT NULL,
|
|
121
|
+
fencing_token INTEGER NOT NULL,
|
|
122
|
+
lease_expires_at REAL NOT NULL,
|
|
123
|
+
lease_expires_at_iso TEXT NOT NULL,
|
|
124
|
+
released INTEGER NOT NULL DEFAULT 0,
|
|
125
|
+
runtime_type TEXT NOT NULL DEFAULT 'ksadk',
|
|
126
|
+
bundle_digest TEXT NOT NULL DEFAULT '',
|
|
127
|
+
capability_digest TEXT NOT NULL DEFAULT '',
|
|
128
|
+
PRIMARY KEY (agent_instance_id, session_id)
|
|
129
|
+
);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_kernel_activations_expiry
|
|
131
|
+
ON kernel_activations (lease_expires_at);
|
|
132
|
+
|
|
133
|
+
CREATE TABLE IF NOT EXISTS kernel_accepted_seq (
|
|
134
|
+
session_id TEXT PRIMARY KEY,
|
|
135
|
+
last_seq INTEGER NOT NULL DEFAULT 0
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
CREATE TABLE IF NOT EXISTS kernel_interactions (
|
|
139
|
+
interaction_id TEXT NOT NULL,
|
|
140
|
+
tenant_id TEXT NOT NULL,
|
|
141
|
+
agent_instance_id TEXT NOT NULL,
|
|
142
|
+
session_id TEXT NOT NULL,
|
|
143
|
+
run_id TEXT NOT NULL,
|
|
144
|
+
kind TEXT NOT NULL CHECK (kind IN ('approval','structured_input','plan_review','custom')),
|
|
145
|
+
request_schema_json TEXT NOT NULL,
|
|
146
|
+
presentation_json TEXT,
|
|
147
|
+
revision INTEGER NOT NULL,
|
|
148
|
+
status TEXT NOT NULL CHECK (status IN (
|
|
149
|
+
'pending','resolving','resolved','cancelled','expired'
|
|
150
|
+
)),
|
|
151
|
+
created_at TEXT NOT NULL,
|
|
152
|
+
expires_at TEXT,
|
|
153
|
+
provider_id TEXT NOT NULL DEFAULT '',
|
|
154
|
+
native_target_json TEXT,
|
|
155
|
+
continuation_json TEXT,
|
|
156
|
+
request_digest TEXT NOT NULL,
|
|
157
|
+
response_json TEXT,
|
|
158
|
+
outcome TEXT,
|
|
159
|
+
actor TEXT,
|
|
160
|
+
event_id TEXT,
|
|
161
|
+
accepted_seq INTEGER,
|
|
162
|
+
fencing_token INTEGER,
|
|
163
|
+
updated_at TEXT,
|
|
164
|
+
PRIMARY KEY (tenant_id, interaction_id)
|
|
165
|
+
);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_kernel_interactions_pending
|
|
167
|
+
ON kernel_interactions (tenant_id, session_id, status);
|
|
168
|
+
|
|
169
|
+
CREATE TABLE IF NOT EXISTS kernel_interaction_submissions (
|
|
170
|
+
tenant_id TEXT NOT NULL,
|
|
171
|
+
interaction_id TEXT NOT NULL,
|
|
172
|
+
idempotency_key TEXT NOT NULL,
|
|
173
|
+
submission_digest TEXT NOT NULL,
|
|
174
|
+
receipt_json TEXT NOT NULL,
|
|
175
|
+
PRIMARY KEY (tenant_id, interaction_id, idempotency_key)
|
|
176
|
+
);
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SQLiteAgentKernelStore:
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
db_path: str | Path,
|
|
184
|
+
session_event_store: SessionEventStore,
|
|
185
|
+
) -> None:
|
|
186
|
+
self.db_path = Path(db_path).expanduser().resolve()
|
|
187
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
self._events = session_event_store
|
|
189
|
+
self._write_lock = asyncio.Lock()
|
|
190
|
+
self._connection: aiosqlite.Connection | None = None
|
|
191
|
+
self._ready: asyncio.Future[None] | None = None
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------- lifecycle
|
|
194
|
+
|
|
195
|
+
async def _connect(self) -> aiosqlite.Connection:
|
|
196
|
+
if self._connection is None:
|
|
197
|
+
self._connection = await aiosqlite.connect(str(self.db_path))
|
|
198
|
+
self._connection.row_factory = aiosqlite.Row
|
|
199
|
+
await self._connection.execute("PRAGMA journal_mode=WAL")
|
|
200
|
+
await self._connection.execute("PRAGMA synchronous=FULL")
|
|
201
|
+
return self._connection
|
|
202
|
+
|
|
203
|
+
async def ensure_schema(self) -> None:
|
|
204
|
+
connection = await self._connect()
|
|
205
|
+
async with self._write_lock:
|
|
206
|
+
# CREATE ... IF NOT EXISTS + 整数 user_version,重复启动幂等。
|
|
207
|
+
await connection.executescript(_SCHEMA)
|
|
208
|
+
await connection.execute(f"PRAGMA user_version={SCHEMA_VERSION}")
|
|
209
|
+
await connection.commit()
|
|
210
|
+
|
|
211
|
+
async def close(self) -> None:
|
|
212
|
+
if self._connection is not None:
|
|
213
|
+
await self._connection.close()
|
|
214
|
+
self._connection = None
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------- helpers
|
|
217
|
+
|
|
218
|
+
async def _begin(self) -> aiosqlite.Connection:
|
|
219
|
+
connection = await self._connect()
|
|
220
|
+
await connection.execute("BEGIN IMMEDIATE")
|
|
221
|
+
return connection
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
async def _fetchone(connection: aiosqlite.Connection, sql: str, params: tuple) -> Any:
|
|
225
|
+
cursor = await connection.execute(sql, params)
|
|
226
|
+
row = await cursor.fetchone()
|
|
227
|
+
await cursor.close()
|
|
228
|
+
return row
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _activation_row(row: aiosqlite.Row | None) -> dict[str, Any] | None:
|
|
232
|
+
if row is None or row["released"]:
|
|
233
|
+
return None
|
|
234
|
+
return dict(row)
|
|
235
|
+
|
|
236
|
+
async def _check_fence(
|
|
237
|
+
self, connection: aiosqlite.Connection, agent_instance_id: str, session_id: str,
|
|
238
|
+
expected_fence: int,
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
row = await self._fetchone(
|
|
241
|
+
connection,
|
|
242
|
+
"SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
|
|
243
|
+
(agent_instance_id, session_id),
|
|
244
|
+
)
|
|
245
|
+
activation = self._activation_row(row)
|
|
246
|
+
if (
|
|
247
|
+
activation is None
|
|
248
|
+
or activation["lease_expires_at"] <= time.time()
|
|
249
|
+
or activation["fencing_token"] != int(expected_fence)
|
|
250
|
+
):
|
|
251
|
+
raise StaleFenceError(
|
|
252
|
+
"activation lease does not match expected fence",
|
|
253
|
+
details={
|
|
254
|
+
"agent_instance_id": agent_instance_id,
|
|
255
|
+
"session_id": session_id,
|
|
256
|
+
"expected_fence": int(expected_fence),
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
return activation
|
|
260
|
+
|
|
261
|
+
async def _emit_admission(
|
|
262
|
+
self, envelope: SessionEventEnvelope, command: AgentControlCommand
|
|
263
|
+
) -> None:
|
|
264
|
+
# admission 事实的 guard 绑定提交方 permit 引用与 command_id。
|
|
265
|
+
await self._events.append(
|
|
266
|
+
envelope,
|
|
267
|
+
guard=AdmissionWriteGuard(
|
|
268
|
+
authorization_ref=command.authorization_ref,
|
|
269
|
+
command_id=command.command_id,
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def _emit_activation(
|
|
274
|
+
self, envelope: SessionEventEnvelope, activation: dict[str, Any], fence: int
|
|
275
|
+
) -> SessionEventEnvelope:
|
|
276
|
+
return await self._events.append(
|
|
277
|
+
envelope,
|
|
278
|
+
guard=ActivationWriteGuard(
|
|
279
|
+
activation_id=activation["activation_id"], fencing_token=int(fence)
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def _receipt(
|
|
285
|
+
command: AgentControlCommand,
|
|
286
|
+
status: str,
|
|
287
|
+
*,
|
|
288
|
+
message_id: str | None = None,
|
|
289
|
+
accepted_seq: int | None = None,
|
|
290
|
+
error: ControlError | None = None,
|
|
291
|
+
) -> AgentControlReceipt:
|
|
292
|
+
return AgentControlReceipt(
|
|
293
|
+
command_id=command.command_id,
|
|
294
|
+
status=status, # type: ignore[arg-type]
|
|
295
|
+
message_id=message_id,
|
|
296
|
+
accepted_seq=accepted_seq,
|
|
297
|
+
error=error,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# --------------------------------------------------------------- commands
|
|
301
|
+
|
|
302
|
+
async def accept_command(
|
|
303
|
+
self, command: AgentControlCommand, *, queue_limit: int
|
|
304
|
+
) -> AgentControlReceipt:
|
|
305
|
+
if queue_limit < 1:
|
|
306
|
+
raise InvalidCommandError("queue_limit must be positive")
|
|
307
|
+
async with self._write_lock:
|
|
308
|
+
connection = await self._begin()
|
|
309
|
+
try:
|
|
310
|
+
existing = await self._fetchone(
|
|
311
|
+
connection,
|
|
312
|
+
"SELECT * FROM kernel_inbox WHERE session_id=? AND idempotency_key=?",
|
|
313
|
+
(command.session_id, command.idempotency_key),
|
|
314
|
+
)
|
|
315
|
+
if existing is not None:
|
|
316
|
+
if existing["request_digest"] != command_digest(command):
|
|
317
|
+
await connection.commit()
|
|
318
|
+
await self._emit_admission(
|
|
319
|
+
control_event(
|
|
320
|
+
session_id=command.session_id,
|
|
321
|
+
event_type="control.command_rejected",
|
|
322
|
+
payload={
|
|
323
|
+
"command_id": str(command.command_id),
|
|
324
|
+
"status": "rejected",
|
|
325
|
+
"reason": "idempotency_conflict",
|
|
326
|
+
},
|
|
327
|
+
causation_id=str(command.command_id),
|
|
328
|
+
),
|
|
329
|
+
command,
|
|
330
|
+
)
|
|
331
|
+
return self._receipt(
|
|
332
|
+
command,
|
|
333
|
+
"rejected",
|
|
334
|
+
error=ControlError(
|
|
335
|
+
code="idempotency_conflict",
|
|
336
|
+
message=(
|
|
337
|
+
"idempotency key reused with a different request digest"
|
|
338
|
+
),
|
|
339
|
+
retryable=False,
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
await connection.commit()
|
|
343
|
+
return self._receipt(
|
|
344
|
+
command,
|
|
345
|
+
"duplicate",
|
|
346
|
+
message_id=existing["message_id"],
|
|
347
|
+
accepted_seq=existing["accepted_seq"],
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
depth_row = await self._fetchone(
|
|
351
|
+
connection,
|
|
352
|
+
"SELECT COUNT(*) AS depth FROM kernel_inbox "
|
|
353
|
+
"WHERE agent_instance_id=? AND session_id=? AND status='accepted'",
|
|
354
|
+
(command.agent_instance_id, command.session_id),
|
|
355
|
+
)
|
|
356
|
+
if depth_row["depth"] >= queue_limit:
|
|
357
|
+
await connection.commit()
|
|
358
|
+
await self._emit_admission(
|
|
359
|
+
control_event(
|
|
360
|
+
session_id=command.session_id,
|
|
361
|
+
event_type="control.command_rejected",
|
|
362
|
+
payload={
|
|
363
|
+
"command_id": str(command.command_id),
|
|
364
|
+
"status": "queue_full",
|
|
365
|
+
"queue_limit": queue_limit,
|
|
366
|
+
},
|
|
367
|
+
causation_id=str(command.command_id),
|
|
368
|
+
),
|
|
369
|
+
command,
|
|
370
|
+
)
|
|
371
|
+
return self._receipt(
|
|
372
|
+
command,
|
|
373
|
+
"queue_full",
|
|
374
|
+
error=ControlError(
|
|
375
|
+
code="queue_full",
|
|
376
|
+
message=f"inbox reached queue_limit={queue_limit}",
|
|
377
|
+
retryable=True,
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
seq_row = await self._fetchone(
|
|
382
|
+
connection,
|
|
383
|
+
"SELECT last_seq FROM kernel_accepted_seq WHERE session_id=?",
|
|
384
|
+
(command.session_id,),
|
|
385
|
+
)
|
|
386
|
+
accepted_seq = (seq_row["last_seq"] if seq_row else 0) + 1
|
|
387
|
+
message_id = new_message_id()
|
|
388
|
+
await connection.execute(
|
|
389
|
+
"INSERT INTO kernel_accepted_seq (session_id, last_seq) VALUES (?, ?) "
|
|
390
|
+
"ON CONFLICT(session_id) DO UPDATE SET last_seq=excluded.last_seq",
|
|
391
|
+
(command.session_id, accepted_seq),
|
|
392
|
+
)
|
|
393
|
+
await connection.execute(
|
|
394
|
+
"INSERT INTO kernel_inbox (message_id, agent_instance_id, session_id,"
|
|
395
|
+
" idempotency_key, request_digest, accepted_seq, status, claimed_fence,"
|
|
396
|
+
" payload_json) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
397
|
+
(
|
|
398
|
+
message_id,
|
|
399
|
+
command.agent_instance_id,
|
|
400
|
+
command.session_id,
|
|
401
|
+
command.idempotency_key,
|
|
402
|
+
command_digest(command),
|
|
403
|
+
accepted_seq,
|
|
404
|
+
InboxState.ACCEPTED.value,
|
|
405
|
+
None,
|
|
406
|
+
command.model_dump_json(),
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
# persist-before-ack:session 事件库与 kernel 库是两个独立
|
|
410
|
+
# SQLite 文件,无法共享一个事务。诚实取舍是在 kernel 事务
|
|
411
|
+
# commit 之前追加 accepted 事件:事件写入失败 -> 回滚 Inbox,
|
|
412
|
+
# 不产生 "persisted-but-untracked" 半状态,客户端可安全重试。
|
|
413
|
+
# 残余窗口:事件已追加但 kernel commit 崩溃 -> 出现一条孤儿
|
|
414
|
+
# accepted 事件而无 Inbox 行;该窗口不返回 ack,重试会重新
|
|
415
|
+
# 走完整路径(seq 单调,可能产生一条重复 accepted 事件),
|
|
416
|
+
# 不存在已 ack 但未持久化的状态。
|
|
417
|
+
await self._emit_admission(
|
|
418
|
+
control_event(
|
|
419
|
+
session_id=command.session_id,
|
|
420
|
+
event_type="control.command_accepted",
|
|
421
|
+
payload={
|
|
422
|
+
"command_id": str(command.command_id),
|
|
423
|
+
"status": "accepted",
|
|
424
|
+
"message_id": message_id,
|
|
425
|
+
"accepted_seq": accepted_seq,
|
|
426
|
+
"command_type": command.command_type,
|
|
427
|
+
},
|
|
428
|
+
causation_id=str(command.command_id),
|
|
429
|
+
),
|
|
430
|
+
command,
|
|
431
|
+
)
|
|
432
|
+
await connection.commit()
|
|
433
|
+
except BaseException:
|
|
434
|
+
await connection.rollback()
|
|
435
|
+
raise
|
|
436
|
+
return self._receipt(
|
|
437
|
+
command, "accepted", message_id=message_id, accepted_seq=accepted_seq
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
async def load_message(self, message_id: str) -> InboxMessage | None:
|
|
441
|
+
connection = await self._connect()
|
|
442
|
+
row = await self._fetchone(
|
|
443
|
+
connection, "SELECT * FROM kernel_inbox WHERE message_id=?", (str(message_id),)
|
|
444
|
+
)
|
|
445
|
+
if row is None:
|
|
446
|
+
return None
|
|
447
|
+
return InboxMessage(
|
|
448
|
+
message_id=row["message_id"],
|
|
449
|
+
agent_instance_id=row["agent_instance_id"],
|
|
450
|
+
session_id=row["session_id"],
|
|
451
|
+
idempotency_key=row["idempotency_key"],
|
|
452
|
+
request_digest=row["request_digest"],
|
|
453
|
+
accepted_seq=row["accepted_seq"],
|
|
454
|
+
status=InboxState(row["status"]),
|
|
455
|
+
claimed_fence=row["claimed_fence"],
|
|
456
|
+
command=AgentControlCommand.model_validate_json(row["payload_json"]),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
async def claim_next(
|
|
460
|
+
self, agent_instance_id: str, session_id: str, fencing_token: int
|
|
461
|
+
) -> InboxMessage | None:
|
|
462
|
+
async with self._write_lock:
|
|
463
|
+
connection = await self._begin()
|
|
464
|
+
try:
|
|
465
|
+
activation = self._activation_row(await self._fetchone(
|
|
466
|
+
connection,
|
|
467
|
+
"SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
|
|
468
|
+
(agent_instance_id, session_id),
|
|
469
|
+
))
|
|
470
|
+
if (
|
|
471
|
+
activation is None
|
|
472
|
+
or activation["lease_expires_at"] <= time.time()
|
|
473
|
+
or activation["fencing_token"] != int(fencing_token)
|
|
474
|
+
):
|
|
475
|
+
raise StaleFenceError(
|
|
476
|
+
"activation lease does not match expected fence",
|
|
477
|
+
details={
|
|
478
|
+
"agent_instance_id": agent_instance_id,
|
|
479
|
+
"session_id": session_id,
|
|
480
|
+
"expected_fence": int(fencing_token),
|
|
481
|
+
},
|
|
482
|
+
)
|
|
483
|
+
row = await self._fetchone(
|
|
484
|
+
connection,
|
|
485
|
+
"SELECT * FROM kernel_inbox WHERE agent_instance_id=? AND session_id=? "
|
|
486
|
+
"AND (status='accepted' OR (status='claimed' AND claimed_fence != ?)) "
|
|
487
|
+
"ORDER BY accepted_seq LIMIT 1",
|
|
488
|
+
(agent_instance_id, session_id, int(fencing_token)),
|
|
489
|
+
)
|
|
490
|
+
if row is None:
|
|
491
|
+
await connection.commit()
|
|
492
|
+
return None
|
|
493
|
+
if row["status"] == InboxState.ACCEPTED.value:
|
|
494
|
+
assert_inbox_transition(InboxState(row["status"]), InboxState.CLAIMED)
|
|
495
|
+
await connection.execute(
|
|
496
|
+
"UPDATE kernel_inbox SET status='claimed', claimed_fence=? WHERE message_id=?",
|
|
497
|
+
(int(fencing_token), row["message_id"]),
|
|
498
|
+
)
|
|
499
|
+
await connection.commit()
|
|
500
|
+
except BaseException:
|
|
501
|
+
await connection.rollback()
|
|
502
|
+
raise
|
|
503
|
+
await self._emit_activation(
|
|
504
|
+
control_event(
|
|
505
|
+
session_id=session_id,
|
|
506
|
+
event_type="control.message_claimed",
|
|
507
|
+
payload={"message_id": row["message_id"], "fencing_token": int(fencing_token)},
|
|
508
|
+
),
|
|
509
|
+
activation,
|
|
510
|
+
fencing_token,
|
|
511
|
+
)
|
|
512
|
+
return await self.load_message(row["message_id"])
|
|
513
|
+
|
|
514
|
+
async def complete_claim(self, message_id: str, *, expected_fence: int) -> None:
|
|
515
|
+
message_id = str(message_id)
|
|
516
|
+
async with self._write_lock:
|
|
517
|
+
connection = await self._begin()
|
|
518
|
+
try:
|
|
519
|
+
row = await self._fetchone(
|
|
520
|
+
connection, "SELECT * FROM kernel_inbox WHERE message_id=?", (message_id,)
|
|
521
|
+
)
|
|
522
|
+
if row is None:
|
|
523
|
+
raise InvalidCommandError(f"unknown message_id {message_id!r}")
|
|
524
|
+
activation = await self._check_fence(
|
|
525
|
+
connection, row["agent_instance_id"], row["session_id"], expected_fence
|
|
526
|
+
)
|
|
527
|
+
if (
|
|
528
|
+
row["status"] != InboxState.CLAIMED.value
|
|
529
|
+
or row["claimed_fence"] != int(expected_fence)
|
|
530
|
+
):
|
|
531
|
+
raise StaleFenceError(
|
|
532
|
+
f"message {message_id!r} is not claimed at fence {expected_fence}"
|
|
533
|
+
)
|
|
534
|
+
assert_inbox_transition(InboxState(row["status"]), InboxState.COMPLETED)
|
|
535
|
+
await connection.execute(
|
|
536
|
+
"UPDATE kernel_inbox SET status='completed' WHERE message_id=?",
|
|
537
|
+
(message_id,),
|
|
538
|
+
)
|
|
539
|
+
await connection.commit()
|
|
540
|
+
except BaseException:
|
|
541
|
+
await connection.rollback()
|
|
542
|
+
raise
|
|
543
|
+
await self._emit_activation(
|
|
544
|
+
control_event(
|
|
545
|
+
session_id=row["session_id"],
|
|
546
|
+
event_type="control.message_completed",
|
|
547
|
+
payload={"message_id": message_id, "fencing_token": int(expected_fence)},
|
|
548
|
+
),
|
|
549
|
+
activation,
|
|
550
|
+
expected_fence,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# -------------------------------------------------------------- interactions
|
|
554
|
+
|
|
555
|
+
async def _check_interaction_guard(
|
|
556
|
+
self,
|
|
557
|
+
connection: aiosqlite.Connection,
|
|
558
|
+
agent_instance_id: str,
|
|
559
|
+
session_id: str,
|
|
560
|
+
guard: ActivationWriteGuard,
|
|
561
|
+
) -> dict[str, Any]:
|
|
562
|
+
row = await self._fetchone(
|
|
563
|
+
connection,
|
|
564
|
+
"SELECT * FROM kernel_activations WHERE activation_id=?",
|
|
565
|
+
(guard.activation_id,),
|
|
566
|
+
)
|
|
567
|
+
activation = self._activation_row(row)
|
|
568
|
+
if (
|
|
569
|
+
activation is None
|
|
570
|
+
or activation["lease_expires_at"] <= time.time()
|
|
571
|
+
or activation["fencing_token"] != int(guard.fencing_token)
|
|
572
|
+
or activation["agent_instance_id"] != agent_instance_id
|
|
573
|
+
or activation["session_id"] != session_id
|
|
574
|
+
):
|
|
575
|
+
raise StaleFenceError(
|
|
576
|
+
"interaction write guard does not match the current lease",
|
|
577
|
+
details={
|
|
578
|
+
"activation_id": guard.activation_id,
|
|
579
|
+
"fencing_token": int(guard.fencing_token),
|
|
580
|
+
"session_id": session_id,
|
|
581
|
+
},
|
|
582
|
+
)
|
|
583
|
+
return activation
|
|
584
|
+
|
|
585
|
+
def _require_local_transactional_event_store(self) -> None:
|
|
586
|
+
"""Ensure an Interaction fact shares this store's SQLite transaction.
|
|
587
|
+
|
|
588
|
+
``SessionServiceEventStore(LocalSessionService)`` normally owns the
|
|
589
|
+
canonical local SessionEvent log. Giving the kernel a different file
|
|
590
|
+
would make a ledger row and its event independently committable, so it
|
|
591
|
+
is an invalid Interaction/v1 configuration rather than a best-effort
|
|
592
|
+
fallback. Other session backends remain usable for the pre-existing
|
|
593
|
+
non-transactional local control path, but not for durable interactions.
|
|
594
|
+
"""
|
|
595
|
+
|
|
596
|
+
service = (
|
|
597
|
+
self._events.session_service
|
|
598
|
+
if isinstance(self._events, SessionServiceEventStore)
|
|
599
|
+
else None
|
|
600
|
+
)
|
|
601
|
+
if not isinstance(service, LocalSessionService) or service.db_path != self.db_path.resolve():
|
|
602
|
+
raise RuntimeError(
|
|
603
|
+
"SQLite InteractionLedger requires SessionEventStore backed by the "
|
|
604
|
+
"same SQLite database"
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
async def _append_interaction_event_on(
|
|
608
|
+
self,
|
|
609
|
+
connection: aiosqlite.Connection,
|
|
610
|
+
envelope: SessionEventEnvelope,
|
|
611
|
+
guard: ActivationWriteGuard,
|
|
612
|
+
) -> SessionEventEnvelope:
|
|
613
|
+
"""Append the canonical SessionEvent in the ledger writer transaction."""
|
|
614
|
+
|
|
615
|
+
self._require_local_transactional_event_store()
|
|
616
|
+
validate_write_guard(envelope, guard)
|
|
617
|
+
packed = envelope_to_session_event(envelope)
|
|
618
|
+
storage_id = session_event_storage_id(envelope.session_id, str(envelope.event_id))
|
|
619
|
+
session_row = await self._fetchone(
|
|
620
|
+
connection,
|
|
621
|
+
f"SELECT id FROM {KSADK_SESSIONS_TABLE} WHERE id=?",
|
|
622
|
+
(envelope.session_id,),
|
|
623
|
+
)
|
|
624
|
+
if session_row is None:
|
|
625
|
+
raise InvalidCommandError(
|
|
626
|
+
f"session {envelope.session_id!r} does not exist in the shared event log"
|
|
627
|
+
)
|
|
628
|
+
existing = await self._fetchone(
|
|
629
|
+
connection,
|
|
630
|
+
f"SELECT id, author, event_type, content_json, timestamp, seq_id,"
|
|
631
|
+
f" invocation_id, metadata_json FROM {KSADK_EVENTS_TABLE}"
|
|
632
|
+
" WHERE session_id=? AND id=?",
|
|
633
|
+
(envelope.session_id, storage_id),
|
|
634
|
+
)
|
|
635
|
+
if existing is not None:
|
|
636
|
+
stored = SessionEvent(
|
|
637
|
+
id=existing["id"],
|
|
638
|
+
session_id=envelope.session_id,
|
|
639
|
+
author=existing["author"],
|
|
640
|
+
event_type=existing["event_type"],
|
|
641
|
+
content=json.loads(existing["content_json"]),
|
|
642
|
+
timestamp=float(existing["timestamp"]),
|
|
643
|
+
seq_id=int(existing["seq_id"]),
|
|
644
|
+
invocation_id=existing["invocation_id"],
|
|
645
|
+
metadata=json.loads(existing["metadata_json"]),
|
|
646
|
+
)
|
|
647
|
+
persisted = session_event_to_envelope(stored)
|
|
648
|
+
if persisted is None: # pragma: no cover - only our packed rows use this id
|
|
649
|
+
raise RuntimeError("kernel interaction event lost its envelope marker")
|
|
650
|
+
SessionServiceEventStore._assert_same_fact(persisted, envelope)
|
|
651
|
+
return persisted
|
|
652
|
+
|
|
653
|
+
next_seq_row = await self._fetchone(
|
|
654
|
+
connection,
|
|
655
|
+
f"SELECT COALESCE(MAX(seq_id), 0) + 1 AS next_seq FROM {KSADK_EVENTS_TABLE}"
|
|
656
|
+
" WHERE session_id=?",
|
|
657
|
+
(envelope.session_id,),
|
|
658
|
+
)
|
|
659
|
+
next_seq = int(next_seq_row["next_seq"])
|
|
660
|
+
packed.bind_seq_id(next_seq)
|
|
661
|
+
await connection.execute(
|
|
662
|
+
f"INSERT INTO {KSADK_EVENTS_TABLE} ("
|
|
663
|
+
"id, session_id, author, event_type, content_json, timestamp, "
|
|
664
|
+
"state_delta_json, seq_id, invocation_id, metadata_json"
|
|
665
|
+
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
666
|
+
(
|
|
667
|
+
storage_id,
|
|
668
|
+
envelope.session_id,
|
|
669
|
+
packed.author,
|
|
670
|
+
packed.event_type,
|
|
671
|
+
json.dumps(packed.content, ensure_ascii=False),
|
|
672
|
+
packed.timestamp,
|
|
673
|
+
json.dumps(packed.state_delta, ensure_ascii=False),
|
|
674
|
+
next_seq,
|
|
675
|
+
packed.invocation_id,
|
|
676
|
+
json.dumps(packed.metadata, ensure_ascii=False),
|
|
677
|
+
),
|
|
678
|
+
)
|
|
679
|
+
await connection.execute(
|
|
680
|
+
f"UPDATE {KSADK_SESSIONS_TABLE} SET updated_at=? WHERE id=?",
|
|
681
|
+
(time.time(), envelope.session_id),
|
|
682
|
+
)
|
|
683
|
+
persisted = session_event_to_envelope(packed)
|
|
684
|
+
if persisted is None: # pragma: no cover - packed by this method
|
|
685
|
+
raise RuntimeError("kernel interaction event lost its envelope marker")
|
|
686
|
+
return persisted
|
|
687
|
+
|
|
688
|
+
async def _interaction_row_for_guard(
|
|
689
|
+
self,
|
|
690
|
+
connection: aiosqlite.Connection,
|
|
691
|
+
interaction_id: str,
|
|
692
|
+
guard: ActivationWriteGuard,
|
|
693
|
+
) -> aiosqlite.Row | None:
|
|
694
|
+
"""Find a public id through the trusted activation scope, not by id alone."""
|
|
695
|
+
|
|
696
|
+
activation_row = await self._fetchone(
|
|
697
|
+
connection,
|
|
698
|
+
"SELECT * FROM kernel_activations WHERE activation_id=?",
|
|
699
|
+
(guard.activation_id,),
|
|
700
|
+
)
|
|
701
|
+
activation = self._activation_row(activation_row)
|
|
702
|
+
if (
|
|
703
|
+
activation is None
|
|
704
|
+
or activation["released"]
|
|
705
|
+
or activation["lease_expires_at"] <= time.time()
|
|
706
|
+
or activation["fencing_token"] != int(guard.fencing_token)
|
|
707
|
+
):
|
|
708
|
+
raise StaleFenceError(
|
|
709
|
+
"interaction write guard does not match the current lease",
|
|
710
|
+
details={
|
|
711
|
+
"activation_id": guard.activation_id,
|
|
712
|
+
"fencing_token": int(guard.fencing_token),
|
|
713
|
+
},
|
|
714
|
+
)
|
|
715
|
+
return await self._fetchone(
|
|
716
|
+
connection,
|
|
717
|
+
"SELECT * FROM kernel_interactions WHERE interaction_id=?"
|
|
718
|
+
" AND agent_instance_id=? AND session_id=?",
|
|
719
|
+
(
|
|
720
|
+
interaction_id,
|
|
721
|
+
activation["agent_instance_id"],
|
|
722
|
+
activation["session_id"],
|
|
723
|
+
),
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
@staticmethod
|
|
727
|
+
def _row_to_record(row: aiosqlite.Row | None) -> InteractionRecord | None:
|
|
728
|
+
if row is None:
|
|
729
|
+
return None
|
|
730
|
+
from ksadk.interaction.contracts import InteractionPresentation
|
|
731
|
+
|
|
732
|
+
presentation = None
|
|
733
|
+
if row["presentation_json"]:
|
|
734
|
+
presentation = InteractionPresentation.model_validate_json(
|
|
735
|
+
row["presentation_json"]
|
|
736
|
+
)
|
|
737
|
+
return InteractionRecord(
|
|
738
|
+
interaction_id=row["interaction_id"],
|
|
739
|
+
tenant_id=row["tenant_id"],
|
|
740
|
+
agent_instance_id=row["agent_instance_id"],
|
|
741
|
+
session_id=row["session_id"],
|
|
742
|
+
run_id=row["run_id"],
|
|
743
|
+
kind=row["kind"],
|
|
744
|
+
request_schema=json.loads(row["request_schema_json"]),
|
|
745
|
+
revision=int(row["revision"]),
|
|
746
|
+
status=row["status"],
|
|
747
|
+
created_at=row["created_at"],
|
|
748
|
+
expires_at=row["expires_at"],
|
|
749
|
+
presentation=presentation,
|
|
750
|
+
provider_id=row["provider_id"] or "",
|
|
751
|
+
native_target=(
|
|
752
|
+
json.loads(row["native_target_json"])
|
|
753
|
+
if row["native_target_json"]
|
|
754
|
+
else None
|
|
755
|
+
),
|
|
756
|
+
continuation_metadata=(
|
|
757
|
+
json.loads(row["continuation_json"])
|
|
758
|
+
if row["continuation_json"]
|
|
759
|
+
else None
|
|
760
|
+
),
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
def _record_values(self, record: InteractionRecord, *, request_digest_: str) -> tuple:
|
|
764
|
+
return (
|
|
765
|
+
record.interaction_id,
|
|
766
|
+
record.tenant_id,
|
|
767
|
+
record.agent_instance_id,
|
|
768
|
+
record.session_id,
|
|
769
|
+
record.run_id,
|
|
770
|
+
record.kind,
|
|
771
|
+
json.dumps(record.request_schema, ensure_ascii=False),
|
|
772
|
+
(
|
|
773
|
+
record.presentation.model_dump_json()
|
|
774
|
+
if record.presentation is not None
|
|
775
|
+
else None
|
|
776
|
+
),
|
|
777
|
+
record.revision,
|
|
778
|
+
record.status,
|
|
779
|
+
record.created_at,
|
|
780
|
+
record.expires_at,
|
|
781
|
+
record.provider_id,
|
|
782
|
+
(
|
|
783
|
+
json.dumps(record.native_target, ensure_ascii=False)
|
|
784
|
+
if record.native_target is not None
|
|
785
|
+
else None
|
|
786
|
+
),
|
|
787
|
+
(
|
|
788
|
+
json.dumps(record.continuation_metadata, ensure_ascii=False)
|
|
789
|
+
if record.continuation_metadata is not None
|
|
790
|
+
else None
|
|
791
|
+
),
|
|
792
|
+
request_digest_,
|
|
793
|
+
now_iso(),
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
async def request(
|
|
797
|
+
self, record: InteractionRecord, *, guard: ActivationWriteGuard
|
|
798
|
+
) -> InteractionRecord:
|
|
799
|
+
digest = request_digest(record)
|
|
800
|
+
async with self._write_lock:
|
|
801
|
+
connection = await self._begin()
|
|
802
|
+
try:
|
|
803
|
+
await self._check_interaction_guard(
|
|
804
|
+
connection, record.agent_instance_id, record.session_id, guard
|
|
805
|
+
)
|
|
806
|
+
existing = await self._fetchone(
|
|
807
|
+
connection,
|
|
808
|
+
"SELECT * FROM kernel_interactions WHERE tenant_id=? AND interaction_id=?",
|
|
809
|
+
(record.tenant_id, record.interaction_id),
|
|
810
|
+
)
|
|
811
|
+
if existing is not None:
|
|
812
|
+
if existing["request_digest"] != digest:
|
|
813
|
+
raise InvalidCommandError(
|
|
814
|
+
"interaction_id reused with a different request digest",
|
|
815
|
+
details={
|
|
816
|
+
"reason": REQUEST_CONFLICT,
|
|
817
|
+
"interaction_id": record.interaction_id,
|
|
818
|
+
},
|
|
819
|
+
)
|
|
820
|
+
await connection.commit()
|
|
821
|
+
stored = self._row_to_record(existing)
|
|
822
|
+
assert stored is not None
|
|
823
|
+
return stored
|
|
824
|
+
await connection.execute(
|
|
825
|
+
"INSERT INTO kernel_interactions (interaction_id, tenant_id,"
|
|
826
|
+
" agent_instance_id, session_id, run_id, kind, request_schema_json,"
|
|
827
|
+
" presentation_json, revision, status, created_at, expires_at,"
|
|
828
|
+
" provider_id, native_target_json, continuation_json,"
|
|
829
|
+
" request_digest, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
830
|
+
self._record_values(record, request_digest_=digest),
|
|
831
|
+
)
|
|
832
|
+
# pending 行与 canonical requested 事实共用同一 SQLite commit。
|
|
833
|
+
await self._append_interaction_event_on(
|
|
834
|
+
connection, requested_event_payload(record, now_iso()), guard
|
|
835
|
+
)
|
|
836
|
+
await connection.commit()
|
|
837
|
+
except BaseException:
|
|
838
|
+
await connection.rollback()
|
|
839
|
+
raise
|
|
840
|
+
return record
|
|
841
|
+
|
|
842
|
+
async def resolve(
|
|
843
|
+
self, submission: InteractionSubmission, *, guard: ActivationWriteGuard
|
|
844
|
+
) -> InteractionReceipt:
|
|
845
|
+
sub_digest = submission_digest(submission)
|
|
846
|
+
async with self._write_lock:
|
|
847
|
+
connection = await self._begin()
|
|
848
|
+
try:
|
|
849
|
+
row = await self._interaction_row_for_guard(
|
|
850
|
+
connection, submission.interaction_id, guard
|
|
851
|
+
)
|
|
852
|
+
if row is None:
|
|
853
|
+
raise InvalidCommandError(
|
|
854
|
+
f"unknown interaction_id {submission.interaction_id!r}"
|
|
855
|
+
)
|
|
856
|
+
await self._check_interaction_guard(
|
|
857
|
+
connection, row["agent_instance_id"], row["session_id"], guard
|
|
858
|
+
)
|
|
859
|
+
current = self._row_to_record(row)
|
|
860
|
+
assert current is not None
|
|
861
|
+
if is_terminal(current.status):
|
|
862
|
+
existing_sub = await self._fetchone(
|
|
863
|
+
connection,
|
|
864
|
+
"SELECT * FROM kernel_interaction_submissions WHERE tenant_id=?"
|
|
865
|
+
" AND interaction_id=? AND idempotency_key=?",
|
|
866
|
+
(
|
|
867
|
+
current.tenant_id,
|
|
868
|
+
current.interaction_id,
|
|
869
|
+
submission.idempotency_key,
|
|
870
|
+
),
|
|
871
|
+
)
|
|
872
|
+
if (
|
|
873
|
+
existing_sub is not None
|
|
874
|
+
and existing_sub["submission_digest"] == sub_digest
|
|
875
|
+
):
|
|
876
|
+
await connection.commit()
|
|
877
|
+
return InteractionReceipt.model_validate_json(
|
|
878
|
+
existing_sub["receipt_json"]
|
|
879
|
+
)
|
|
880
|
+
raise InvalidCommandError(
|
|
881
|
+
"interaction already reached terminal status"
|
|
882
|
+
f" {current.status!r}",
|
|
883
|
+
details={
|
|
884
|
+
"reason": ALREADY_RESOLVED,
|
|
885
|
+
"interaction_id": current.interaction_id,
|
|
886
|
+
},
|
|
887
|
+
)
|
|
888
|
+
if current.revision != submission.expected_revision:
|
|
889
|
+
raise InvalidCommandError(
|
|
890
|
+
"interaction revision does not match expected_revision",
|
|
891
|
+
details={
|
|
892
|
+
"reason": REVISION_MISMATCH,
|
|
893
|
+
"interaction_id": current.interaction_id,
|
|
894
|
+
"expected_revision": submission.expected_revision,
|
|
895
|
+
"current_revision": current.revision,
|
|
896
|
+
},
|
|
897
|
+
)
|
|
898
|
+
outcome = resolve_outcome(submission.action)
|
|
899
|
+
updated = current.model_copy(
|
|
900
|
+
update={"status": "resolved", "revision": current.revision + 1}
|
|
901
|
+
)
|
|
902
|
+
stored = await self._append_interaction_event_on(
|
|
903
|
+
connection,
|
|
904
|
+
interaction_event(
|
|
905
|
+
updated,
|
|
906
|
+
event_type="interaction.resolved",
|
|
907
|
+
timestamp=now_iso(),
|
|
908
|
+
outcome=outcome,
|
|
909
|
+
response=submission.response,
|
|
910
|
+
actor_ref="user",
|
|
911
|
+
),
|
|
912
|
+
guard,
|
|
913
|
+
)
|
|
914
|
+
receipt = InteractionReceipt(
|
|
915
|
+
interaction_id=updated.interaction_id,
|
|
916
|
+
revision=updated.revision,
|
|
917
|
+
status="resolved",
|
|
918
|
+
outcome=outcome, # type: ignore[arg-type]
|
|
919
|
+
event_id=str(stored.event_id),
|
|
920
|
+
accepted_seq=stored.seq,
|
|
921
|
+
)
|
|
922
|
+
await connection.execute(
|
|
923
|
+
"UPDATE kernel_interactions SET revision=?, status=?,"
|
|
924
|
+
" response_json=?, outcome=?, actor=?, event_id=?, accepted_seq=?,"
|
|
925
|
+
" fencing_token=?, updated_at=? WHERE tenant_id=? AND interaction_id=?",
|
|
926
|
+
(
|
|
927
|
+
updated.revision,
|
|
928
|
+
"resolved",
|
|
929
|
+
json.dumps(submission.response, ensure_ascii=False),
|
|
930
|
+
outcome,
|
|
931
|
+
"user",
|
|
932
|
+
str(stored.event_id),
|
|
933
|
+
stored.seq,
|
|
934
|
+
int(guard.fencing_token),
|
|
935
|
+
now_iso(),
|
|
936
|
+
updated.tenant_id,
|
|
937
|
+
updated.interaction_id,
|
|
938
|
+
),
|
|
939
|
+
)
|
|
940
|
+
await connection.execute(
|
|
941
|
+
"INSERT INTO kernel_interaction_submissions (tenant_id,"
|
|
942
|
+
" interaction_id, idempotency_key, submission_digest, receipt_json)"
|
|
943
|
+
" VALUES (?,?,?,?,?) ON CONFLICT DO NOTHING",
|
|
944
|
+
(
|
|
945
|
+
updated.tenant_id,
|
|
946
|
+
updated.interaction_id,
|
|
947
|
+
submission.idempotency_key,
|
|
948
|
+
sub_digest,
|
|
949
|
+
receipt.model_dump_json(),
|
|
950
|
+
),
|
|
951
|
+
)
|
|
952
|
+
await connection.commit()
|
|
953
|
+
except BaseException:
|
|
954
|
+
await connection.rollback()
|
|
955
|
+
raise
|
|
956
|
+
return receipt
|
|
957
|
+
|
|
958
|
+
async def _terminal_command(
|
|
959
|
+
self,
|
|
960
|
+
interaction_id: str,
|
|
961
|
+
expected_revision: int,
|
|
962
|
+
*,
|
|
963
|
+
guard: ActivationWriteGuard,
|
|
964
|
+
status: str,
|
|
965
|
+
reason: str,
|
|
966
|
+
) -> InteractionReceipt:
|
|
967
|
+
async with self._write_lock:
|
|
968
|
+
connection = await self._begin()
|
|
969
|
+
try:
|
|
970
|
+
row = await self._interaction_row_for_guard(
|
|
971
|
+
connection, interaction_id, guard
|
|
972
|
+
)
|
|
973
|
+
if row is None:
|
|
974
|
+
raise InvalidCommandError(
|
|
975
|
+
f"unknown interaction_id {interaction_id!r}"
|
|
976
|
+
)
|
|
977
|
+
await self._check_interaction_guard(
|
|
978
|
+
connection, row["agent_instance_id"], row["session_id"], guard
|
|
979
|
+
)
|
|
980
|
+
current = self._row_to_record(row)
|
|
981
|
+
assert current is not None
|
|
982
|
+
if is_terminal(current.status):
|
|
983
|
+
raise InvalidCommandError(
|
|
984
|
+
f"interaction already reached terminal status"
|
|
985
|
+
f" {current.status!r}",
|
|
986
|
+
details={
|
|
987
|
+
"reason": ALREADY_RESOLVED,
|
|
988
|
+
"interaction_id": current.interaction_id,
|
|
989
|
+
},
|
|
990
|
+
)
|
|
991
|
+
if current.revision != expected_revision:
|
|
992
|
+
raise InvalidCommandError(
|
|
993
|
+
"interaction revision does not match expected_revision",
|
|
994
|
+
details={
|
|
995
|
+
"reason": REVISION_MISMATCH,
|
|
996
|
+
"interaction_id": current.interaction_id,
|
|
997
|
+
"expected_revision": expected_revision,
|
|
998
|
+
"current_revision": current.revision,
|
|
999
|
+
},
|
|
1000
|
+
)
|
|
1001
|
+
updated = current.model_copy(
|
|
1002
|
+
update={"status": status, "revision": current.revision + 1}
|
|
1003
|
+
)
|
|
1004
|
+
event_type = (
|
|
1005
|
+
"interaction.cancelled"
|
|
1006
|
+
if status == "cancelled"
|
|
1007
|
+
else "interaction.expired"
|
|
1008
|
+
)
|
|
1009
|
+
stored = await self._append_interaction_event_on(
|
|
1010
|
+
connection,
|
|
1011
|
+
interaction_event(
|
|
1012
|
+
updated,
|
|
1013
|
+
event_type=event_type,
|
|
1014
|
+
timestamp=now_iso(),
|
|
1015
|
+
reason=reason,
|
|
1016
|
+
),
|
|
1017
|
+
guard,
|
|
1018
|
+
)
|
|
1019
|
+
receipt = InteractionReceipt(
|
|
1020
|
+
interaction_id=updated.interaction_id,
|
|
1021
|
+
revision=updated.revision,
|
|
1022
|
+
status=updated.status, # type: ignore[arg-type]
|
|
1023
|
+
outcome=updated.status, # type: ignore[arg-type]
|
|
1024
|
+
event_id=str(stored.event_id),
|
|
1025
|
+
accepted_seq=stored.seq,
|
|
1026
|
+
)
|
|
1027
|
+
await connection.execute(
|
|
1028
|
+
"UPDATE kernel_interactions SET revision=?, status=?, outcome=?,"
|
|
1029
|
+
" event_id=?, accepted_seq=?, fencing_token=?, updated_at=?"
|
|
1030
|
+
" WHERE tenant_id=? AND interaction_id=?",
|
|
1031
|
+
(
|
|
1032
|
+
updated.revision,
|
|
1033
|
+
status,
|
|
1034
|
+
status,
|
|
1035
|
+
str(stored.event_id),
|
|
1036
|
+
stored.seq,
|
|
1037
|
+
int(guard.fencing_token),
|
|
1038
|
+
now_iso(),
|
|
1039
|
+
updated.tenant_id,
|
|
1040
|
+
updated.interaction_id,
|
|
1041
|
+
),
|
|
1042
|
+
)
|
|
1043
|
+
await connection.commit()
|
|
1044
|
+
except BaseException:
|
|
1045
|
+
await connection.rollback()
|
|
1046
|
+
raise
|
|
1047
|
+
return receipt
|
|
1048
|
+
|
|
1049
|
+
async def cancel(
|
|
1050
|
+
self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
|
|
1051
|
+
) -> InteractionReceipt:
|
|
1052
|
+
return await self._terminal_command(
|
|
1053
|
+
interaction_id,
|
|
1054
|
+
expected_revision,
|
|
1055
|
+
guard=guard,
|
|
1056
|
+
status="cancelled",
|
|
1057
|
+
reason="cancelled by owner",
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
async def expire(
|
|
1061
|
+
self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
|
|
1062
|
+
) -> InteractionReceipt:
|
|
1063
|
+
return await self._terminal_command(
|
|
1064
|
+
interaction_id,
|
|
1065
|
+
expected_revision,
|
|
1066
|
+
guard=guard,
|
|
1067
|
+
status="expired",
|
|
1068
|
+
reason="interaction expired",
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
async def get(
|
|
1072
|
+
self,
|
|
1073
|
+
interaction_id: str,
|
|
1074
|
+
*,
|
|
1075
|
+
tenant_id: str | None = None,
|
|
1076
|
+
agent_instance_id: str | None = None,
|
|
1077
|
+
session_id: str | None = None,
|
|
1078
|
+
run_id: str | None = None,
|
|
1079
|
+
) -> InteractionRecord | None:
|
|
1080
|
+
"""Return an interaction only inside a complete trusted scope.
|
|
1081
|
+
|
|
1082
|
+
An omitted scope is retained for local compatibility but is fail-closed
|
|
1083
|
+
when the opaque id exists in more than one tenant. Partial scope is
|
|
1084
|
+
never sufficient for a security-sensitive worker lookup.
|
|
1085
|
+
"""
|
|
1086
|
+
|
|
1087
|
+
scope = (tenant_id, agent_instance_id, session_id, run_id)
|
|
1088
|
+
connection = await self._connect()
|
|
1089
|
+
if any(value is not None for value in scope):
|
|
1090
|
+
if not all(value is not None for value in scope):
|
|
1091
|
+
raise InvalidCommandError(
|
|
1092
|
+
"interaction lookup requires a complete trusted scope",
|
|
1093
|
+
details={"interaction_id": interaction_id},
|
|
1094
|
+
)
|
|
1095
|
+
row = await self._fetchone(
|
|
1096
|
+
connection,
|
|
1097
|
+
"SELECT * FROM kernel_interactions WHERE interaction_id=?"
|
|
1098
|
+
" AND tenant_id=? AND agent_instance_id=? AND session_id=? AND run_id=?",
|
|
1099
|
+
(interaction_id, tenant_id, agent_instance_id, session_id, run_id),
|
|
1100
|
+
)
|
|
1101
|
+
return self._row_to_record(row)
|
|
1102
|
+
cursor = await connection.execute(
|
|
1103
|
+
"SELECT * FROM kernel_interactions WHERE interaction_id=? LIMIT 2",
|
|
1104
|
+
(interaction_id,),
|
|
1105
|
+
)
|
|
1106
|
+
rows = await cursor.fetchall()
|
|
1107
|
+
await cursor.close()
|
|
1108
|
+
if len(rows) > 1:
|
|
1109
|
+
raise InvalidCommandError(
|
|
1110
|
+
f"interaction_id {interaction_id!r} is ambiguous without trusted scope",
|
|
1111
|
+
details={"reason": REQUEST_CONFLICT, "interaction_id": interaction_id},
|
|
1112
|
+
)
|
|
1113
|
+
return self._row_to_record(rows[0]) if rows else None
|
|
1114
|
+
|
|
1115
|
+
async def list_pending_interactions(
|
|
1116
|
+
self, tenant_id: str, session_id: str
|
|
1117
|
+
) -> list[InteractionRecord]:
|
|
1118
|
+
connection = await self._connect()
|
|
1119
|
+
cursor = await connection.execute(
|
|
1120
|
+
"SELECT * FROM kernel_interactions WHERE tenant_id=? AND session_id=?"
|
|
1121
|
+
" AND status='pending' ORDER BY created_at",
|
|
1122
|
+
(tenant_id, session_id),
|
|
1123
|
+
)
|
|
1124
|
+
rows = await cursor.fetchall()
|
|
1125
|
+
await cursor.close()
|
|
1126
|
+
records = [self._row_to_record(row) for row in rows]
|
|
1127
|
+
return [r for r in records if r is not None]
|
|
1128
|
+
|
|
1129
|
+
# ------------------------------------------------------------- activations
|
|
1130
|
+
|
|
1131
|
+
async def acquire_activation(self, request: ActivationLeaseRequest) -> ActivationLease:
|
|
1132
|
+
async with self._write_lock:
|
|
1133
|
+
connection = await self._begin()
|
|
1134
|
+
try:
|
|
1135
|
+
row = await self._fetchone(
|
|
1136
|
+
connection,
|
|
1137
|
+
"SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
|
|
1138
|
+
(request.agent_instance_id, request.session_id),
|
|
1139
|
+
)
|
|
1140
|
+
expires_at = time.time() + request.lease_ttl_seconds
|
|
1141
|
+
if row is None:
|
|
1142
|
+
token = 1
|
|
1143
|
+
elif row["released"] or row["lease_expires_at"] <= time.time():
|
|
1144
|
+
token = row["fencing_token"] + 1
|
|
1145
|
+
elif row["activation_id"] == request.activation_id:
|
|
1146
|
+
token = row["fencing_token"]
|
|
1147
|
+
else:
|
|
1148
|
+
raise InvalidCommandError(
|
|
1149
|
+
"activation lease is still held by another owner",
|
|
1150
|
+
details={
|
|
1151
|
+
"holder": row["activation_id"],
|
|
1152
|
+
"lease_expires_at": row["lease_expires_at_iso"],
|
|
1153
|
+
},
|
|
1154
|
+
)
|
|
1155
|
+
expires_iso = datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat()
|
|
1156
|
+
await connection.execute(
|
|
1157
|
+
"INSERT INTO kernel_activations (agent_instance_id, session_id,"
|
|
1158
|
+
" activation_id, fencing_token, lease_expires_at, lease_expires_at_iso,"
|
|
1159
|
+
" released, runtime_type, bundle_digest, capability_digest)"
|
|
1160
|
+
" VALUES (?,?,?,?,?,?,0,?,?,?)"
|
|
1161
|
+
" ON CONFLICT(agent_instance_id, session_id) DO UPDATE SET"
|
|
1162
|
+
" activation_id=excluded.activation_id,"
|
|
1163
|
+
" fencing_token=excluded.fencing_token,"
|
|
1164
|
+
" lease_expires_at=excluded.lease_expires_at,"
|
|
1165
|
+
" lease_expires_at_iso=excluded.lease_expires_at_iso,"
|
|
1166
|
+
" released=0, runtime_type=excluded.runtime_type,"
|
|
1167
|
+
" bundle_digest=excluded.bundle_digest,"
|
|
1168
|
+
" capability_digest=excluded.capability_digest",
|
|
1169
|
+
(
|
|
1170
|
+
request.agent_instance_id,
|
|
1171
|
+
request.session_id,
|
|
1172
|
+
request.activation_id,
|
|
1173
|
+
token,
|
|
1174
|
+
expires_at,
|
|
1175
|
+
expires_iso,
|
|
1176
|
+
request.runtime_type,
|
|
1177
|
+
request.bundle_digest,
|
|
1178
|
+
request.capability_digest,
|
|
1179
|
+
),
|
|
1180
|
+
)
|
|
1181
|
+
await connection.commit()
|
|
1182
|
+
except BaseException:
|
|
1183
|
+
await connection.rollback()
|
|
1184
|
+
raise
|
|
1185
|
+
return ActivationLease(
|
|
1186
|
+
agent_instance_id=request.agent_instance_id,
|
|
1187
|
+
activation_id=request.activation_id,
|
|
1188
|
+
fencing_token=token,
|
|
1189
|
+
lease_expires_at=expires_iso,
|
|
1190
|
+
bundle_digest=request.bundle_digest,
|
|
1191
|
+
runtime_type=request.runtime_type,
|
|
1192
|
+
capability_digest=request.capability_digest,
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
async def renew_activation(
|
|
1196
|
+
self, activation_id: str, *, expected_fence: int, lease_ttl_seconds: float
|
|
1197
|
+
) -> ActivationLease:
|
|
1198
|
+
async with self._write_lock:
|
|
1199
|
+
connection = await self._begin()
|
|
1200
|
+
try:
|
|
1201
|
+
row = await self._fetchone(
|
|
1202
|
+
connection,
|
|
1203
|
+
"SELECT * FROM kernel_activations WHERE activation_id=?",
|
|
1204
|
+
(activation_id,),
|
|
1205
|
+
)
|
|
1206
|
+
if row is None:
|
|
1207
|
+
raise InvalidCommandError(f"unknown activation_id {activation_id!r}")
|
|
1208
|
+
if (
|
|
1209
|
+
row["released"]
|
|
1210
|
+
or row["lease_expires_at"] <= time.time()
|
|
1211
|
+
or row["fencing_token"] != int(expected_fence)
|
|
1212
|
+
):
|
|
1213
|
+
raise StaleFenceError(
|
|
1214
|
+
f"cannot renew activation {activation_id!r} at fence {expected_fence}"
|
|
1215
|
+
)
|
|
1216
|
+
expires_at = time.time() + lease_ttl_seconds
|
|
1217
|
+
expires_iso = datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat()
|
|
1218
|
+
await connection.execute(
|
|
1219
|
+
"UPDATE kernel_activations SET lease_expires_at=?, lease_expires_at_iso=?"
|
|
1220
|
+
" WHERE activation_id=?",
|
|
1221
|
+
(expires_at, expires_iso, activation_id),
|
|
1222
|
+
)
|
|
1223
|
+
await connection.commit()
|
|
1224
|
+
except BaseException:
|
|
1225
|
+
await connection.rollback()
|
|
1226
|
+
raise
|
|
1227
|
+
return ActivationLease(
|
|
1228
|
+
agent_instance_id=row["agent_instance_id"],
|
|
1229
|
+
activation_id=activation_id,
|
|
1230
|
+
fencing_token=row["fencing_token"],
|
|
1231
|
+
lease_expires_at=expires_iso,
|
|
1232
|
+
bundle_digest=row["bundle_digest"],
|
|
1233
|
+
runtime_type=row["runtime_type"],
|
|
1234
|
+
capability_digest=row["capability_digest"],
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
async def release_activation(self, activation_id: str, *, expected_fence: int) -> None:
|
|
1238
|
+
async with self._write_lock:
|
|
1239
|
+
connection = await self._begin()
|
|
1240
|
+
try:
|
|
1241
|
+
row = await self._fetchone(
|
|
1242
|
+
connection,
|
|
1243
|
+
"SELECT * FROM kernel_activations WHERE activation_id=?",
|
|
1244
|
+
(activation_id,),
|
|
1245
|
+
)
|
|
1246
|
+
if row is None:
|
|
1247
|
+
raise InvalidCommandError(f"unknown activation_id {activation_id!r}")
|
|
1248
|
+
if row["released"] or row["fencing_token"] != int(expected_fence):
|
|
1249
|
+
raise StaleFenceError(
|
|
1250
|
+
f"cannot release activation {activation_id!r} at fence {expected_fence}"
|
|
1251
|
+
)
|
|
1252
|
+
await connection.execute(
|
|
1253
|
+
"UPDATE kernel_activations SET released=1, lease_expires_at=?"
|
|
1254
|
+
" WHERE activation_id=?",
|
|
1255
|
+
(time.time(), activation_id),
|
|
1256
|
+
)
|
|
1257
|
+
await connection.commit()
|
|
1258
|
+
except BaseException:
|
|
1259
|
+
await connection.rollback()
|
|
1260
|
+
raise
|
|
1261
|
+
|
|
1262
|
+
# ------------------------------------------------------------------ events
|
|
1263
|
+
|
|
1264
|
+
async def append_event(
|
|
1265
|
+
self,
|
|
1266
|
+
envelope: SessionEventEnvelope,
|
|
1267
|
+
*,
|
|
1268
|
+
expected_fence: int,
|
|
1269
|
+
agent_instance_id: str | None = None,
|
|
1270
|
+
) -> SessionEventEnvelope:
|
|
1271
|
+
activation = await self._resolve_activation(envelope.session_id, agent_instance_id)
|
|
1272
|
+
await self._check_fence(
|
|
1273
|
+
await self._connect(),
|
|
1274
|
+
activation["agent_instance_id"],
|
|
1275
|
+
envelope.session_id,
|
|
1276
|
+
expected_fence,
|
|
1277
|
+
)
|
|
1278
|
+
return await self._events.append(
|
|
1279
|
+
envelope,
|
|
1280
|
+
guard=ActivationWriteGuard(
|
|
1281
|
+
activation_id=activation["activation_id"],
|
|
1282
|
+
fencing_token=int(expected_fence),
|
|
1283
|
+
),
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
async def _resolve_activation(
|
|
1287
|
+
self, session_id: str, agent_instance_id: str | None
|
|
1288
|
+
) -> dict[str, Any]:
|
|
1289
|
+
connection = await self._connect()
|
|
1290
|
+
if agent_instance_id is not None:
|
|
1291
|
+
row = self._activation_row(await self._fetchone(
|
|
1292
|
+
connection,
|
|
1293
|
+
"SELECT * FROM kernel_activations WHERE agent_instance_id=? AND session_id=?",
|
|
1294
|
+
(agent_instance_id, session_id),
|
|
1295
|
+
))
|
|
1296
|
+
if row is None:
|
|
1297
|
+
raise StaleFenceError(
|
|
1298
|
+
"no active activation lease",
|
|
1299
|
+
details={"agent_instance_id": agent_instance_id, "session_id": session_id},
|
|
1300
|
+
)
|
|
1301
|
+
return row
|
|
1302
|
+
cursor = await connection.execute(
|
|
1303
|
+
"SELECT * FROM kernel_activations WHERE session_id=? AND released=0",
|
|
1304
|
+
(session_id,),
|
|
1305
|
+
)
|
|
1306
|
+
rows = [self._activation_row(row) for row in await cursor.fetchall()]
|
|
1307
|
+
await cursor.close()
|
|
1308
|
+
rows = [row for row in rows if row is not None]
|
|
1309
|
+
if len(rows) != 1:
|
|
1310
|
+
raise StaleFenceError(
|
|
1311
|
+
"cannot resolve a single activation lease for session",
|
|
1312
|
+
details={"session_id": session_id, "matches": len(rows)},
|
|
1313
|
+
)
|
|
1314
|
+
return rows[0]
|
|
1315
|
+
|
|
1316
|
+
# -------------------------------------------------------------------- runs
|
|
1317
|
+
|
|
1318
|
+
async def load_run(self, run_id: str) -> RunRecord | None:
|
|
1319
|
+
row = await self._fetchone(
|
|
1320
|
+
await self._connect(), "SELECT * FROM kernel_runs WHERE run_id=?", (run_id,)
|
|
1321
|
+
)
|
|
1322
|
+
if row is None:
|
|
1323
|
+
return None
|
|
1324
|
+
return RunRecord(
|
|
1325
|
+
run_id=row["run_id"],
|
|
1326
|
+
agent_instance_id=row["agent_instance_id"],
|
|
1327
|
+
session_id=row["session_id"],
|
|
1328
|
+
state=row["state"],
|
|
1329
|
+
activation_fence=row["activation_fence"],
|
|
1330
|
+
created_at=row["created_at"],
|
|
1331
|
+
updated_at=row["updated_at"],
|
|
1332
|
+
metadata=json.loads(row["metadata_json"]),
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
async def save_run_transition(
|
|
1336
|
+
self, run: RunRecord, *, expected_fence: int
|
|
1337
|
+
) -> RunRecord:
|
|
1338
|
+
async with self._write_lock:
|
|
1339
|
+
connection = await self._begin()
|
|
1340
|
+
try:
|
|
1341
|
+
activation = await self._check_fence(
|
|
1342
|
+
connection, run.agent_instance_id, run.session_id, expected_fence
|
|
1343
|
+
)
|
|
1344
|
+
existing = await self.load_run(run.run_id)
|
|
1345
|
+
assert_run_transition(existing.state if existing else None, run.state)
|
|
1346
|
+
if is_active_run(run.state):
|
|
1347
|
+
cursor = await connection.execute(
|
|
1348
|
+
"SELECT run_id FROM kernel_runs WHERE session_id=? AND run_id != ?"
|
|
1349
|
+
" AND state IN ('running','paused','waiting')",
|
|
1350
|
+
(run.session_id, run.run_id),
|
|
1351
|
+
)
|
|
1352
|
+
clash = await cursor.fetchone()
|
|
1353
|
+
await cursor.close()
|
|
1354
|
+
if clash is not None:
|
|
1355
|
+
raise InvalidCommandError(
|
|
1356
|
+
"session already has an active run",
|
|
1357
|
+
details={
|
|
1358
|
+
"session_id": run.session_id,
|
|
1359
|
+
"active_run_id": clash["run_id"],
|
|
1360
|
+
},
|
|
1361
|
+
)
|
|
1362
|
+
timestamp = now_iso()
|
|
1363
|
+
stored = run.model_copy(
|
|
1364
|
+
update={
|
|
1365
|
+
"activation_fence": int(expected_fence),
|
|
1366
|
+
"created_at": existing.created_at if existing else timestamp,
|
|
1367
|
+
"updated_at": timestamp,
|
|
1368
|
+
}
|
|
1369
|
+
)
|
|
1370
|
+
await connection.execute(
|
|
1371
|
+
"INSERT INTO kernel_runs (run_id, agent_instance_id, session_id, state,"
|
|
1372
|
+
" activation_fence, created_at, updated_at, metadata_json)"
|
|
1373
|
+
" VALUES (?,?,?,?,?,?,?,?)"
|
|
1374
|
+
" ON CONFLICT(run_id) DO UPDATE SET state=excluded.state,"
|
|
1375
|
+
" activation_fence=excluded.activation_fence,"
|
|
1376
|
+
" updated_at=excluded.updated_at,"
|
|
1377
|
+
" metadata_json=excluded.metadata_json",
|
|
1378
|
+
(
|
|
1379
|
+
stored.run_id,
|
|
1380
|
+
stored.agent_instance_id,
|
|
1381
|
+
stored.session_id,
|
|
1382
|
+
stored.state.value,
|
|
1383
|
+
stored.activation_fence,
|
|
1384
|
+
stored.created_at,
|
|
1385
|
+
stored.updated_at,
|
|
1386
|
+
json.dumps(stored.metadata, ensure_ascii=False),
|
|
1387
|
+
),
|
|
1388
|
+
)
|
|
1389
|
+
await connection.commit()
|
|
1390
|
+
except BaseException:
|
|
1391
|
+
await connection.rollback()
|
|
1392
|
+
raise
|
|
1393
|
+
await self._emit_activation(
|
|
1394
|
+
control_event(
|
|
1395
|
+
session_id=run.session_id,
|
|
1396
|
+
event_type="control.run_transition",
|
|
1397
|
+
payload={
|
|
1398
|
+
"run_id": run.run_id,
|
|
1399
|
+
"state": run.state.value,
|
|
1400
|
+
"fencing_token": int(expected_fence),
|
|
1401
|
+
},
|
|
1402
|
+
run_id=run.run_id,
|
|
1403
|
+
),
|
|
1404
|
+
activation,
|
|
1405
|
+
expected_fence,
|
|
1406
|
+
)
|
|
1407
|
+
return stored
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
__all__ = ["SQLiteAgentKernelStore"]
|