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,1133 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""InMemory ``AgentKernelStore``(Phase 1 Task 3 Step 4)。
|
|
3
|
+
|
|
4
|
+
只用于单进程开发与 conformance 测试,不宣称跨进程 durable。
|
|
5
|
+
以 per-(agent, session) asyncio lock 保证 accept/claim/transition 的原子语义;
|
|
6
|
+
所有 mutation 与对应 ``ControlEvent/v1`` 的追加在同一个临界区内完成。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Any
|
|
15
|
+
from uuid import uuid4
|
|
16
|
+
|
|
17
|
+
from ksadk.events.session_event import SessionEventStore
|
|
18
|
+
from ksadk.interaction.contracts import (
|
|
19
|
+
InteractionRecord,
|
|
20
|
+
InteractionReceipt,
|
|
21
|
+
InteractionSubmission,
|
|
22
|
+
is_terminal,
|
|
23
|
+
)
|
|
24
|
+
from ksadk.interaction.ledger import (
|
|
25
|
+
ALREADY_RESOLVED,
|
|
26
|
+
REVISION_MISMATCH,
|
|
27
|
+
REQUEST_CONFLICT,
|
|
28
|
+
interaction_event,
|
|
29
|
+
request_digest,
|
|
30
|
+
requested_event_payload,
|
|
31
|
+
resolve_outcome,
|
|
32
|
+
submission_digest,
|
|
33
|
+
)
|
|
34
|
+
from ksadk.kernel.contracts import (
|
|
35
|
+
ActivationLease,
|
|
36
|
+
ActivationWriteGuard,
|
|
37
|
+
AdmissionWriteGuard,
|
|
38
|
+
AgentControlCommand,
|
|
39
|
+
AgentControlReceipt,
|
|
40
|
+
ControlError,
|
|
41
|
+
SessionEventEnvelope,
|
|
42
|
+
)
|
|
43
|
+
from ksadk.kernel.errors import InvalidCommandError, StaleFenceError
|
|
44
|
+
from ksadk.kernel.state import (
|
|
45
|
+
InboxState,
|
|
46
|
+
assert_inbox_transition,
|
|
47
|
+
assert_run_transition,
|
|
48
|
+
is_active_run,
|
|
49
|
+
)
|
|
50
|
+
from ksadk.kernel.store import (
|
|
51
|
+
ActivationLeaseRequest,
|
|
52
|
+
InboxMessage,
|
|
53
|
+
RunRecord,
|
|
54
|
+
command_digest,
|
|
55
|
+
control_event,
|
|
56
|
+
new_message_id,
|
|
57
|
+
now_iso,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class InMemoryAgentKernelStore:
|
|
62
|
+
def __init__(self, session_event_store: SessionEventStore) -> None:
|
|
63
|
+
self._events = session_event_store
|
|
64
|
+
self._locks: dict[tuple[str, str], asyncio.Lock] = {}
|
|
65
|
+
self._messages: dict[str, dict[str, Any]] = {}
|
|
66
|
+
self._idempotency: dict[tuple[str, str], str] = {} # (session, key) -> message_id
|
|
67
|
+
self._accepted_seq: dict[str, int] = {}
|
|
68
|
+
self._activations: dict[tuple[str, str], dict[str, Any]] = {}
|
|
69
|
+
self._runs: dict[str, RunRecord] = {}
|
|
70
|
+
# Interaction ledger(Task 5):(tenant_id, interaction_id) -> row。
|
|
71
|
+
self._interactions: dict[tuple[str, str], dict[str, Any]] = {}
|
|
72
|
+
self._interaction_submissions: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------ locks
|
|
75
|
+
|
|
76
|
+
def _lock(self, agent_instance_id: str, session_id: str) -> asyncio.Lock:
|
|
77
|
+
key = (agent_instance_id, session_id)
|
|
78
|
+
return self._locks.setdefault(key, asyncio.Lock())
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------- helpers
|
|
81
|
+
|
|
82
|
+
def _activation_row(
|
|
83
|
+
self, agent_instance_id: str, session_id: str
|
|
84
|
+
) -> dict[str, Any] | None:
|
|
85
|
+
row = self._activations.get((agent_instance_id, session_id))
|
|
86
|
+
if row is None or row["released"]:
|
|
87
|
+
return None
|
|
88
|
+
return row
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _lease_expired(row: dict[str, Any]) -> bool:
|
|
92
|
+
return row["lease_expires_at"] <= time.time()
|
|
93
|
+
|
|
94
|
+
def _check_fence(self, agent_instance_id: str, session_id: str, expected_fence: int) -> dict[str, Any]:
|
|
95
|
+
row = self._activation_row(agent_instance_id, session_id)
|
|
96
|
+
if (
|
|
97
|
+
row is None
|
|
98
|
+
or self._lease_expired(row)
|
|
99
|
+
or row["fencing_token"] != int(expected_fence)
|
|
100
|
+
):
|
|
101
|
+
raise StaleFenceError(
|
|
102
|
+
"activation lease does not match expected fence",
|
|
103
|
+
details={
|
|
104
|
+
"agent_instance_id": agent_instance_id,
|
|
105
|
+
"session_id": session_id,
|
|
106
|
+
"expected_fence": int(expected_fence),
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
return row
|
|
110
|
+
|
|
111
|
+
async def _emit(
|
|
112
|
+
self,
|
|
113
|
+
envelope: SessionEventEnvelope,
|
|
114
|
+
*,
|
|
115
|
+
activation_row: dict[str, Any] | None,
|
|
116
|
+
admission_command: AgentControlCommand | None = None,
|
|
117
|
+
) -> SessionEventEnvelope:
|
|
118
|
+
if activation_row is not None:
|
|
119
|
+
guard = ActivationWriteGuard(
|
|
120
|
+
activation_id=activation_row["activation_id"],
|
|
121
|
+
fencing_token=activation_row["fencing_token"],
|
|
122
|
+
)
|
|
123
|
+
elif admission_command is not None:
|
|
124
|
+
# admission 事实的 guard 绑定提交方 permit 引用与 command_id。
|
|
125
|
+
guard = AdmissionWriteGuard(
|
|
126
|
+
authorization_ref=admission_command.authorization_ref,
|
|
127
|
+
command_id=admission_command.command_id,
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
guard = AdmissionWriteGuard(
|
|
131
|
+
authorization_ref="agent-kernel", command_id=uuid4()
|
|
132
|
+
)
|
|
133
|
+
return await self._events.append(envelope, guard=guard)
|
|
134
|
+
|
|
135
|
+
async def _emit_admission(
|
|
136
|
+
self, envelope: SessionEventEnvelope, command: AgentControlCommand
|
|
137
|
+
) -> None:
|
|
138
|
+
# admission 事实的 guard 绑定提交方 permit 引用与 command_id。
|
|
139
|
+
await self._events.append(
|
|
140
|
+
envelope,
|
|
141
|
+
guard=AdmissionWriteGuard(
|
|
142
|
+
authorization_ref=command.authorization_ref,
|
|
143
|
+
command_id=command.command_id,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _receipt(
|
|
149
|
+
command: AgentControlCommand,
|
|
150
|
+
status: str,
|
|
151
|
+
*,
|
|
152
|
+
message_id: str | None = None,
|
|
153
|
+
accepted_seq: int | None = None,
|
|
154
|
+
error: ControlError | None = None,
|
|
155
|
+
) -> AgentControlReceipt:
|
|
156
|
+
return AgentControlReceipt(
|
|
157
|
+
command_id=command.command_id,
|
|
158
|
+
status=status, # type: ignore[arg-type]
|
|
159
|
+
message_id=message_id,
|
|
160
|
+
accepted_seq=accepted_seq,
|
|
161
|
+
error=error,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------- commands
|
|
165
|
+
|
|
166
|
+
async def accept_command(
|
|
167
|
+
self, command: AgentControlCommand, *, queue_limit: int
|
|
168
|
+
) -> AgentControlReceipt:
|
|
169
|
+
if queue_limit < 1:
|
|
170
|
+
raise InvalidCommandError("queue_limit must be positive")
|
|
171
|
+
async with self._lock(command.agent_instance_id, command.session_id):
|
|
172
|
+
existing_id = self._idempotency.get(
|
|
173
|
+
(command.session_id, command.idempotency_key)
|
|
174
|
+
)
|
|
175
|
+
if existing_id is not None:
|
|
176
|
+
existing = self._messages[existing_id]
|
|
177
|
+
if existing["request_digest"] != command_digest(command):
|
|
178
|
+
await self._emit_admission(
|
|
179
|
+
control_event(
|
|
180
|
+
session_id=command.session_id,
|
|
181
|
+
event_type="control.command_rejected",
|
|
182
|
+
payload={
|
|
183
|
+
"command_id": str(command.command_id),
|
|
184
|
+
"status": "rejected",
|
|
185
|
+
"reason": "idempotency_conflict",
|
|
186
|
+
},
|
|
187
|
+
causation_id=str(command.command_id),
|
|
188
|
+
),
|
|
189
|
+
command,
|
|
190
|
+
)
|
|
191
|
+
return self._receipt(
|
|
192
|
+
command,
|
|
193
|
+
"rejected",
|
|
194
|
+
error=ControlError(
|
|
195
|
+
code="idempotency_conflict",
|
|
196
|
+
message="idempotency key reused with a different request digest",
|
|
197
|
+
retryable=False,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
return self._receipt(
|
|
201
|
+
command,
|
|
202
|
+
"duplicate",
|
|
203
|
+
message_id=existing["message_id"],
|
|
204
|
+
accepted_seq=existing["accepted_seq"],
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
depth = sum(
|
|
208
|
+
1
|
|
209
|
+
for row in self._messages.values()
|
|
210
|
+
if row["agent_instance_id"] == command.agent_instance_id
|
|
211
|
+
and row["session_id"] == command.session_id
|
|
212
|
+
and row["status"] == InboxState.ACCEPTED
|
|
213
|
+
)
|
|
214
|
+
if depth >= queue_limit:
|
|
215
|
+
await self._emit_admission(
|
|
216
|
+
control_event(
|
|
217
|
+
session_id=command.session_id,
|
|
218
|
+
event_type="control.command_rejected",
|
|
219
|
+
payload={
|
|
220
|
+
"command_id": str(command.command_id),
|
|
221
|
+
"status": "queue_full",
|
|
222
|
+
"queue_limit": queue_limit,
|
|
223
|
+
},
|
|
224
|
+
causation_id=str(command.command_id),
|
|
225
|
+
),
|
|
226
|
+
command,
|
|
227
|
+
)
|
|
228
|
+
return self._receipt(
|
|
229
|
+
command,
|
|
230
|
+
"queue_full",
|
|
231
|
+
error=ControlError(
|
|
232
|
+
code="queue_full",
|
|
233
|
+
message=f"inbox reached queue_limit={queue_limit}",
|
|
234
|
+
retryable=True,
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
accepted_seq = self._accepted_seq.get(command.session_id, 0) + 1
|
|
239
|
+
self._accepted_seq[command.session_id] = accepted_seq
|
|
240
|
+
message_id = new_message_id()
|
|
241
|
+
self._messages[message_id] = {
|
|
242
|
+
"message_id": message_id,
|
|
243
|
+
"agent_instance_id": command.agent_instance_id,
|
|
244
|
+
"session_id": command.session_id,
|
|
245
|
+
"idempotency_key": command.idempotency_key,
|
|
246
|
+
"request_digest": command_digest(command),
|
|
247
|
+
"accepted_seq": accepted_seq,
|
|
248
|
+
"status": InboxState.ACCEPTED,
|
|
249
|
+
"claimed_fence": None,
|
|
250
|
+
"command": command,
|
|
251
|
+
}
|
|
252
|
+
self._idempotency[(command.session_id, command.idempotency_key)] = message_id
|
|
253
|
+
await self._emit_admission(
|
|
254
|
+
control_event(
|
|
255
|
+
session_id=command.session_id,
|
|
256
|
+
event_type="control.command_accepted",
|
|
257
|
+
payload={
|
|
258
|
+
"command_id": str(command.command_id),
|
|
259
|
+
"status": "accepted",
|
|
260
|
+
"message_id": message_id,
|
|
261
|
+
"accepted_seq": accepted_seq,
|
|
262
|
+
"command_type": command.command_type,
|
|
263
|
+
},
|
|
264
|
+
causation_id=str(command.command_id),
|
|
265
|
+
),
|
|
266
|
+
command,
|
|
267
|
+
)
|
|
268
|
+
return self._receipt(
|
|
269
|
+
command,
|
|
270
|
+
"accepted",
|
|
271
|
+
message_id=message_id,
|
|
272
|
+
accepted_seq=accepted_seq,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def load_message(self, message_id: str) -> InboxMessage | None:
|
|
276
|
+
row = self._messages.get(str(message_id))
|
|
277
|
+
return self._to_message(row) if row is not None else None
|
|
278
|
+
|
|
279
|
+
async def load_by_idempotency(
|
|
280
|
+
self, session_id: str, idempotency_key: str
|
|
281
|
+
) -> InboxMessage | None:
|
|
282
|
+
message_id = self._idempotency.get((session_id, idempotency_key))
|
|
283
|
+
if message_id is None:
|
|
284
|
+
return None
|
|
285
|
+
return await self.load_message(message_id)
|
|
286
|
+
|
|
287
|
+
async def reject_command(
|
|
288
|
+
self,
|
|
289
|
+
command: AgentControlCommand,
|
|
290
|
+
*,
|
|
291
|
+
status: str,
|
|
292
|
+
code: str,
|
|
293
|
+
message: str,
|
|
294
|
+
retryable: bool = False,
|
|
295
|
+
) -> AgentControlReceipt:
|
|
296
|
+
"""admission 拒绝(invalid_permit / unsupported / ...)的脱敏审计 + receipt。
|
|
297
|
+
|
|
298
|
+
只在 SessionEventStore 里追加 ``control.command_rejected`` 事实,
|
|
299
|
+
不写 Inbox 行;payload 仅含 command_id/status/reason。
|
|
300
|
+
"""
|
|
301
|
+
await self._emit_admission(
|
|
302
|
+
control_event(
|
|
303
|
+
session_id=command.session_id,
|
|
304
|
+
event_type="control.command_rejected",
|
|
305
|
+
payload={
|
|
306
|
+
"command_id": str(command.command_id),
|
|
307
|
+
"status": status,
|
|
308
|
+
"reason": code,
|
|
309
|
+
},
|
|
310
|
+
causation_id=str(command.command_id),
|
|
311
|
+
),
|
|
312
|
+
command,
|
|
313
|
+
)
|
|
314
|
+
return self._receipt(
|
|
315
|
+
command,
|
|
316
|
+
status,
|
|
317
|
+
error=ControlError(code=code, message=message, retryable=retryable),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def _session_rows(
|
|
321
|
+
self, agent_instance_id: str, session_id: str | None
|
|
322
|
+
) -> list[dict[str, Any]]:
|
|
323
|
+
return [
|
|
324
|
+
row
|
|
325
|
+
for row in self._messages.values()
|
|
326
|
+
if row["agent_instance_id"] == agent_instance_id
|
|
327
|
+
and (session_id is None or row["session_id"] == session_id)
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
async def list_messages(
|
|
331
|
+
self, agent_instance_id: str, session_id: str | None = None
|
|
332
|
+
) -> list[InboxMessage]:
|
|
333
|
+
"""全部状态的 Inbox 行(审计/测试视角),按 accepted_seq 排序。"""
|
|
334
|
+
rows = sorted(
|
|
335
|
+
self._session_rows(agent_instance_id, session_id),
|
|
336
|
+
key=lambda row: row["accepted_seq"],
|
|
337
|
+
)
|
|
338
|
+
return [self._to_message(row) for row in rows]
|
|
339
|
+
|
|
340
|
+
async def list_pending(
|
|
341
|
+
self,
|
|
342
|
+
agent_instance_id: str,
|
|
343
|
+
session_id: str | None = None,
|
|
344
|
+
*,
|
|
345
|
+
fencing_token: int | None = None,
|
|
346
|
+
) -> list[InboxMessage]:
|
|
347
|
+
"""按 accepted_seq 排序的待处理消息。
|
|
348
|
+
|
|
349
|
+
ACCEPTED 总是 pending;CLAIMED 只在传入相同 fencing_token(本 owner
|
|
350
|
+
自我重试视角)时可见,用于 retryable failure 后的恢复。
|
|
351
|
+
"""
|
|
352
|
+
rows = []
|
|
353
|
+
for row in self._session_rows(agent_instance_id, session_id):
|
|
354
|
+
if row["status"] == InboxState.ACCEPTED:
|
|
355
|
+
rows.append(row)
|
|
356
|
+
elif (
|
|
357
|
+
fencing_token is not None
|
|
358
|
+
and row["status"] == InboxState.CLAIMED
|
|
359
|
+
and row["claimed_fence"] == int(fencing_token)
|
|
360
|
+
):
|
|
361
|
+
rows.append(row)
|
|
362
|
+
rows.sort(key=lambda row: row["accepted_seq"])
|
|
363
|
+
return [self._to_message(row) for row in rows]
|
|
364
|
+
|
|
365
|
+
async def claim_message(
|
|
366
|
+
self, message_id: str, fencing_token: int
|
|
367
|
+
) -> InboxMessage:
|
|
368
|
+
"""按 message_id 认领(worker 选择性 FIFO 使用)。同 fence 重复认领幂等。"""
|
|
369
|
+
row = self._messages.get(str(message_id))
|
|
370
|
+
if row is None:
|
|
371
|
+
raise InvalidCommandError(f"unknown message_id {message_id!r}")
|
|
372
|
+
async with self._lock(row["agent_instance_id"], row["session_id"]):
|
|
373
|
+
fresh = self._messages[str(message_id)]
|
|
374
|
+
if (
|
|
375
|
+
fresh["status"] == InboxState.CLAIMED
|
|
376
|
+
and fresh["claimed_fence"] == int(fencing_token)
|
|
377
|
+
):
|
|
378
|
+
return self._to_message(fresh)
|
|
379
|
+
activation = self._check_fence(
|
|
380
|
+
fresh["agent_instance_id"], fresh["session_id"], fencing_token
|
|
381
|
+
)
|
|
382
|
+
if fresh["status"] != InboxState.ACCEPTED:
|
|
383
|
+
raise InvalidCommandError(
|
|
384
|
+
f"message {message_id!r} is not claimable at status {fresh['status']}"
|
|
385
|
+
)
|
|
386
|
+
assert_inbox_transition(InboxState(fresh["status"]), InboxState.CLAIMED)
|
|
387
|
+
fresh["status"] = InboxState.CLAIMED
|
|
388
|
+
fresh["claimed_fence"] = int(fencing_token)
|
|
389
|
+
await self._emit(
|
|
390
|
+
control_event(
|
|
391
|
+
session_id=fresh["session_id"],
|
|
392
|
+
event_type="control.message_claimed",
|
|
393
|
+
payload={
|
|
394
|
+
"message_id": fresh["message_id"],
|
|
395
|
+
"fencing_token": int(fencing_token),
|
|
396
|
+
},
|
|
397
|
+
),
|
|
398
|
+
activation_row=activation,
|
|
399
|
+
)
|
|
400
|
+
return self._to_message(fresh)
|
|
401
|
+
|
|
402
|
+
async def discard_claim(self, message_id: str, *, expected_fence: int) -> None:
|
|
403
|
+
"""typed rejection 的确定性收口:CLAIMED -> DISCARDED。"""
|
|
404
|
+
message_id = str(message_id)
|
|
405
|
+
row = self._messages.get(message_id)
|
|
406
|
+
if row is None:
|
|
407
|
+
raise InvalidCommandError(f"unknown message_id {message_id!r}")
|
|
408
|
+
async with self._lock(row["agent_instance_id"], row["session_id"]):
|
|
409
|
+
fresh = self._messages[message_id]
|
|
410
|
+
activation = self._check_fence(
|
|
411
|
+
fresh["agent_instance_id"], fresh["session_id"], expected_fence
|
|
412
|
+
)
|
|
413
|
+
if (
|
|
414
|
+
fresh["status"] != InboxState.CLAIMED
|
|
415
|
+
or fresh["claimed_fence"] != int(expected_fence)
|
|
416
|
+
):
|
|
417
|
+
raise StaleFenceError(
|
|
418
|
+
f"message {message_id!r} is not claimed at fence {expected_fence}"
|
|
419
|
+
)
|
|
420
|
+
assert_inbox_transition(InboxState(fresh["status"]), InboxState.DISCARDED)
|
|
421
|
+
fresh["status"] = InboxState.DISCARDED
|
|
422
|
+
await self._emit(
|
|
423
|
+
control_event(
|
|
424
|
+
session_id=fresh["session_id"],
|
|
425
|
+
event_type="control.message_discarded",
|
|
426
|
+
payload={
|
|
427
|
+
"message_id": message_id,
|
|
428
|
+
"fencing_token": int(expected_fence),
|
|
429
|
+
},
|
|
430
|
+
),
|
|
431
|
+
activation_row=activation,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
async def inbox_depth(
|
|
435
|
+
self, agent_instance_id: str, session_id: str | None = None
|
|
436
|
+
) -> int:
|
|
437
|
+
return sum(
|
|
438
|
+
1
|
|
439
|
+
for row in self._session_rows(agent_instance_id, session_id)
|
|
440
|
+
if row["status"] == InboxState.ACCEPTED
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
async def find_active_run(
|
|
444
|
+
self, agent_instance_id: str, session_id: str | None = None
|
|
445
|
+
) -> RunRecord | None:
|
|
446
|
+
for run in self._runs.values():
|
|
447
|
+
if run.agent_instance_id != agent_instance_id:
|
|
448
|
+
continue
|
|
449
|
+
if session_id is not None and run.session_id != session_id:
|
|
450
|
+
continue
|
|
451
|
+
if is_active_run(run.state):
|
|
452
|
+
return run
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
async def current_lease(
|
|
456
|
+
self, agent_instance_id: str, session_id: str | None = None
|
|
457
|
+
) -> ActivationLease | None:
|
|
458
|
+
for (agent, session), row in self._activations.items():
|
|
459
|
+
if agent != agent_instance_id:
|
|
460
|
+
continue
|
|
461
|
+
if session_id is not None and session != session_id:
|
|
462
|
+
continue
|
|
463
|
+
if row.get("released") or self._lease_expired(row):
|
|
464
|
+
continue
|
|
465
|
+
request = ActivationLeaseRequest(
|
|
466
|
+
agent_instance_id=agent,
|
|
467
|
+
session_id=session,
|
|
468
|
+
activation_id=row["activation_id"],
|
|
469
|
+
runtime_type=row["runtime_type"],
|
|
470
|
+
bundle_digest=row["bundle_digest"],
|
|
471
|
+
capability_digest=row["capability_digest"],
|
|
472
|
+
)
|
|
473
|
+
return self._lease(request, row)
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
@staticmethod
|
|
477
|
+
def _to_message(row: dict[str, Any]) -> InboxMessage:
|
|
478
|
+
return InboxMessage(
|
|
479
|
+
message_id=row["message_id"],
|
|
480
|
+
agent_instance_id=row["agent_instance_id"],
|
|
481
|
+
session_id=row["session_id"],
|
|
482
|
+
idempotency_key=row["idempotency_key"],
|
|
483
|
+
request_digest=row["request_digest"],
|
|
484
|
+
accepted_seq=row["accepted_seq"],
|
|
485
|
+
status=InboxState(row["status"]),
|
|
486
|
+
claimed_fence=row["claimed_fence"],
|
|
487
|
+
command=row.get("command"),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
async def claim_next(
|
|
491
|
+
self, agent_instance_id: str, session_id: str, fencing_token: int
|
|
492
|
+
) -> InboxMessage | None:
|
|
493
|
+
async with self._lock(agent_instance_id, session_id):
|
|
494
|
+
activation = self._check_fence(agent_instance_id, session_id, fencing_token)
|
|
495
|
+
def _claimable(row: dict[str, Any]) -> bool:
|
|
496
|
+
if row["status"] == InboxState.ACCEPTED:
|
|
497
|
+
return True
|
|
498
|
+
# 过期/被 takeover 的 claim 只能被更高 fence 的 owner reclaim。
|
|
499
|
+
return row["status"] == InboxState.CLAIMED and row[
|
|
500
|
+
"claimed_fence"
|
|
501
|
+
] != int(fencing_token)
|
|
502
|
+
|
|
503
|
+
candidates = sorted(
|
|
504
|
+
(
|
|
505
|
+
row
|
|
506
|
+
for row in self._messages.values()
|
|
507
|
+
if row["agent_instance_id"] == agent_instance_id
|
|
508
|
+
and row["session_id"] == session_id
|
|
509
|
+
and _claimable(row)
|
|
510
|
+
),
|
|
511
|
+
key=lambda row: row["accepted_seq"],
|
|
512
|
+
)
|
|
513
|
+
if not candidates:
|
|
514
|
+
return None
|
|
515
|
+
row = candidates[0]
|
|
516
|
+
if row["status"] == InboxState.ACCEPTED:
|
|
517
|
+
assert_inbox_transition(InboxState(row["status"]), InboxState.CLAIMED)
|
|
518
|
+
row["status"] = InboxState.CLAIMED
|
|
519
|
+
row["claimed_fence"] = int(fencing_token)
|
|
520
|
+
await self._emit(
|
|
521
|
+
control_event(
|
|
522
|
+
session_id=session_id,
|
|
523
|
+
event_type="control.message_claimed",
|
|
524
|
+
payload={
|
|
525
|
+
"message_id": row["message_id"],
|
|
526
|
+
"fencing_token": int(fencing_token),
|
|
527
|
+
},
|
|
528
|
+
),
|
|
529
|
+
activation_row=activation,
|
|
530
|
+
)
|
|
531
|
+
return self._to_message(row)
|
|
532
|
+
|
|
533
|
+
async def complete_claim(self, message_id: str, *, expected_fence: int) -> None:
|
|
534
|
+
message_id = str(message_id)
|
|
535
|
+
row = self._messages.get(message_id)
|
|
536
|
+
if row is None:
|
|
537
|
+
raise InvalidCommandError(f"unknown message_id {message_id!r}")
|
|
538
|
+
async with self._lock(row["agent_instance_id"], row["session_id"]):
|
|
539
|
+
fresh = self._messages[message_id]
|
|
540
|
+
activation = self._check_fence(
|
|
541
|
+
fresh["agent_instance_id"], fresh["session_id"], expected_fence
|
|
542
|
+
)
|
|
543
|
+
if (
|
|
544
|
+
fresh["status"] != InboxState.CLAIMED
|
|
545
|
+
or fresh["claimed_fence"] != int(expected_fence)
|
|
546
|
+
):
|
|
547
|
+
raise StaleFenceError(
|
|
548
|
+
f"message {message_id!r} is not claimed at fence {expected_fence}"
|
|
549
|
+
)
|
|
550
|
+
assert_inbox_transition(InboxState(fresh["status"]), InboxState.COMPLETED)
|
|
551
|
+
fresh["status"] = InboxState.COMPLETED
|
|
552
|
+
await self._emit(
|
|
553
|
+
control_event(
|
|
554
|
+
session_id=fresh["session_id"],
|
|
555
|
+
event_type="control.message_completed",
|
|
556
|
+
payload={"message_id": message_id, "fencing_token": int(expected_fence)},
|
|
557
|
+
),
|
|
558
|
+
activation_row=activation,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# -------------------------------------------------------------- interactions
|
|
562
|
+
|
|
563
|
+
def _check_interaction_guard(
|
|
564
|
+
self, agent_instance_id: str, session_id: str, guard: ActivationWriteGuard
|
|
565
|
+
) -> dict[str, Any]:
|
|
566
|
+
"""Interaction 台账的 fence CAS:guard 必须命中当前未过期 lease。"""
|
|
567
|
+
|
|
568
|
+
row = next(
|
|
569
|
+
(
|
|
570
|
+
candidate
|
|
571
|
+
for candidate in self._activations.values()
|
|
572
|
+
if candidate["activation_id"] == guard.activation_id
|
|
573
|
+
and candidate["agent_instance_id"] == agent_instance_id
|
|
574
|
+
and candidate["session_id"] == session_id
|
|
575
|
+
),
|
|
576
|
+
None,
|
|
577
|
+
)
|
|
578
|
+
if (
|
|
579
|
+
row is None
|
|
580
|
+
or row.get("released")
|
|
581
|
+
or self._lease_expired(row)
|
|
582
|
+
or row["fencing_token"] != int(guard.fencing_token)
|
|
583
|
+
or row["agent_instance_id"] != agent_instance_id
|
|
584
|
+
or row["session_id"] != session_id
|
|
585
|
+
):
|
|
586
|
+
raise StaleFenceError(
|
|
587
|
+
"interaction write guard does not match the current lease",
|
|
588
|
+
details={
|
|
589
|
+
"activation_id": guard.activation_id,
|
|
590
|
+
"fencing_token": int(guard.fencing_token),
|
|
591
|
+
"session_id": session_id,
|
|
592
|
+
},
|
|
593
|
+
)
|
|
594
|
+
return row
|
|
595
|
+
|
|
596
|
+
def _find_interaction(
|
|
597
|
+
self,
|
|
598
|
+
interaction_id: str,
|
|
599
|
+
*,
|
|
600
|
+
agent_instance_id: str | None = None,
|
|
601
|
+
session_id: str | None = None,
|
|
602
|
+
) -> dict[str, Any] | None:
|
|
603
|
+
"""Resolve an opaque public id only inside an activation-owned scope.
|
|
604
|
+
|
|
605
|
+
``kernel_interactions`` is tenant-keyed, while a public submission
|
|
606
|
+
intentionally does not carry a tenant. The trusted activation guard
|
|
607
|
+
is consequently the lookup boundary for mutations. An unscoped read
|
|
608
|
+
is permitted only when the id is globally unambiguous; it must never
|
|
609
|
+
return an arbitrary tenant's row.
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
matches = [
|
|
613
|
+
row
|
|
614
|
+
for (_, key), row in self._interactions.items()
|
|
615
|
+
if key == interaction_id
|
|
616
|
+
and (agent_instance_id is None or row["record"].agent_instance_id == agent_instance_id)
|
|
617
|
+
and (session_id is None or row["record"].session_id == session_id)
|
|
618
|
+
]
|
|
619
|
+
if len(matches) > 1:
|
|
620
|
+
raise InvalidCommandError(
|
|
621
|
+
f"interaction_id {interaction_id!r} is ambiguous without trusted scope",
|
|
622
|
+
details={"reason": REQUEST_CONFLICT, "interaction_id": interaction_id},
|
|
623
|
+
)
|
|
624
|
+
return matches[0] if matches else None
|
|
625
|
+
|
|
626
|
+
def _interaction_scope_for_guard(
|
|
627
|
+
self, guard: ActivationWriteGuard
|
|
628
|
+
) -> tuple[str, str]:
|
|
629
|
+
row = next(
|
|
630
|
+
(
|
|
631
|
+
candidate
|
|
632
|
+
for candidate in self._activations.values()
|
|
633
|
+
if candidate["activation_id"] == guard.activation_id
|
|
634
|
+
),
|
|
635
|
+
None,
|
|
636
|
+
)
|
|
637
|
+
if (
|
|
638
|
+
row is None
|
|
639
|
+
or row.get("released")
|
|
640
|
+
or self._lease_expired(row)
|
|
641
|
+
or row["fencing_token"] != int(guard.fencing_token)
|
|
642
|
+
):
|
|
643
|
+
raise StaleFenceError(
|
|
644
|
+
"interaction write guard does not match the current lease",
|
|
645
|
+
details={
|
|
646
|
+
"activation_id": guard.activation_id,
|
|
647
|
+
"fencing_token": int(guard.fencing_token),
|
|
648
|
+
},
|
|
649
|
+
)
|
|
650
|
+
return str(row["agent_instance_id"]), str(row["session_id"])
|
|
651
|
+
|
|
652
|
+
@staticmethod
|
|
653
|
+
def _terminal_conflict(interaction_id: str, status: str) -> InvalidCommandError:
|
|
654
|
+
return InvalidCommandError(
|
|
655
|
+
f"interaction {interaction_id!r} already reached terminal status {status!r}",
|
|
656
|
+
details={"reason": ALREADY_RESOLVED, "interaction_id": interaction_id},
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
async def request(
|
|
660
|
+
self, record: InteractionRecord, *, guard: ActivationWriteGuard
|
|
661
|
+
) -> InteractionRecord:
|
|
662
|
+
async with self._lock(record.agent_instance_id, record.session_id):
|
|
663
|
+
self._check_interaction_guard(
|
|
664
|
+
record.agent_instance_id, record.session_id, guard
|
|
665
|
+
)
|
|
666
|
+
key = (record.tenant_id, record.interaction_id)
|
|
667
|
+
existing = self._interactions.get(key)
|
|
668
|
+
digest = request_digest(record)
|
|
669
|
+
if existing is not None:
|
|
670
|
+
if existing["request_digest"] != digest:
|
|
671
|
+
raise InvalidCommandError(
|
|
672
|
+
"interaction_id reused with a different request digest",
|
|
673
|
+
details={
|
|
674
|
+
"reason": REQUEST_CONFLICT,
|
|
675
|
+
"interaction_id": record.interaction_id,
|
|
676
|
+
},
|
|
677
|
+
)
|
|
678
|
+
return existing["record"]
|
|
679
|
+
# persist-before-ack:事件追加失败时不留下 pending 行。
|
|
680
|
+
await self._events.append(
|
|
681
|
+
requested_event_payload(record, now_iso()), guard=guard
|
|
682
|
+
)
|
|
683
|
+
self._interactions[key] = {"record": record, "request_digest": digest}
|
|
684
|
+
return record
|
|
685
|
+
|
|
686
|
+
async def resolve(
|
|
687
|
+
self, submission: InteractionSubmission, *, guard: ActivationWriteGuard
|
|
688
|
+
) -> InteractionReceipt:
|
|
689
|
+
agent_instance_id, session_id = self._interaction_scope_for_guard(guard)
|
|
690
|
+
row = self._find_interaction(
|
|
691
|
+
submission.interaction_id,
|
|
692
|
+
agent_instance_id=agent_instance_id,
|
|
693
|
+
session_id=session_id,
|
|
694
|
+
)
|
|
695
|
+
if row is None:
|
|
696
|
+
raise InvalidCommandError(
|
|
697
|
+
f"unknown interaction_id {submission.interaction_id!r}"
|
|
698
|
+
)
|
|
699
|
+
record = row["record"]
|
|
700
|
+
async with self._lock(record.agent_instance_id, record.session_id):
|
|
701
|
+
self._check_interaction_guard(
|
|
702
|
+
record.agent_instance_id, record.session_id, guard
|
|
703
|
+
)
|
|
704
|
+
fresh = self._interactions[(record.tenant_id, record.interaction_id)]
|
|
705
|
+
current: InteractionRecord = fresh["record"]
|
|
706
|
+
if is_terminal(current.status):
|
|
707
|
+
sub_key = (
|
|
708
|
+
current.tenant_id,
|
|
709
|
+
current.interaction_id,
|
|
710
|
+
submission.idempotency_key,
|
|
711
|
+
)
|
|
712
|
+
existing_sub = self._interaction_submissions.get(sub_key)
|
|
713
|
+
if (
|
|
714
|
+
existing_sub is not None
|
|
715
|
+
and existing_sub["digest"] == submission_digest(submission)
|
|
716
|
+
):
|
|
717
|
+
return existing_sub["receipt"]
|
|
718
|
+
raise self._terminal_conflict(
|
|
719
|
+
current.interaction_id, current.status
|
|
720
|
+
)
|
|
721
|
+
if current.revision != submission.expected_revision:
|
|
722
|
+
raise InvalidCommandError(
|
|
723
|
+
"interaction revision does not match expected_revision",
|
|
724
|
+
details={
|
|
725
|
+
"reason": REVISION_MISMATCH,
|
|
726
|
+
"interaction_id": current.interaction_id,
|
|
727
|
+
"expected_revision": submission.expected_revision,
|
|
728
|
+
"current_revision": current.revision,
|
|
729
|
+
},
|
|
730
|
+
)
|
|
731
|
+
outcome = resolve_outcome(submission.action)
|
|
732
|
+
updated = current.model_copy(
|
|
733
|
+
update={
|
|
734
|
+
"status": "resolved",
|
|
735
|
+
"revision": current.revision + 1,
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
stored = await self._events.append(
|
|
739
|
+
interaction_event(
|
|
740
|
+
updated,
|
|
741
|
+
event_type="interaction.resolved",
|
|
742
|
+
timestamp=now_iso(),
|
|
743
|
+
outcome=outcome,
|
|
744
|
+
response=submission.response,
|
|
745
|
+
actor_ref="user",
|
|
746
|
+
),
|
|
747
|
+
guard=guard,
|
|
748
|
+
)
|
|
749
|
+
receipt = InteractionReceipt(
|
|
750
|
+
interaction_id=updated.interaction_id,
|
|
751
|
+
revision=updated.revision,
|
|
752
|
+
status="resolved",
|
|
753
|
+
outcome=outcome, # type: ignore[arg-type]
|
|
754
|
+
event_id=stored.event_id,
|
|
755
|
+
accepted_seq=stored.seq,
|
|
756
|
+
)
|
|
757
|
+
fresh["record"] = updated
|
|
758
|
+
self._interaction_submissions[
|
|
759
|
+
(updated.tenant_id, updated.interaction_id, submission.idempotency_key)
|
|
760
|
+
] = {"digest": submission_digest(submission), "receipt": receipt}
|
|
761
|
+
return receipt
|
|
762
|
+
|
|
763
|
+
async def _terminal_command(
|
|
764
|
+
self,
|
|
765
|
+
interaction_id: str,
|
|
766
|
+
expected_revision: int,
|
|
767
|
+
*,
|
|
768
|
+
guard: ActivationWriteGuard,
|
|
769
|
+
status: str,
|
|
770
|
+
reason: str,
|
|
771
|
+
) -> InteractionReceipt:
|
|
772
|
+
agent_instance_id, session_id = self._interaction_scope_for_guard(guard)
|
|
773
|
+
row = self._find_interaction(
|
|
774
|
+
interaction_id,
|
|
775
|
+
agent_instance_id=agent_instance_id,
|
|
776
|
+
session_id=session_id,
|
|
777
|
+
)
|
|
778
|
+
if row is None:
|
|
779
|
+
raise InvalidCommandError(f"unknown interaction_id {interaction_id!r}")
|
|
780
|
+
record = row["record"]
|
|
781
|
+
async with self._lock(record.agent_instance_id, record.session_id):
|
|
782
|
+
self._check_interaction_guard(
|
|
783
|
+
record.agent_instance_id, record.session_id, guard
|
|
784
|
+
)
|
|
785
|
+
fresh = self._interactions[(record.tenant_id, record.interaction_id)]
|
|
786
|
+
current: InteractionRecord = fresh["record"]
|
|
787
|
+
if is_terminal(current.status):
|
|
788
|
+
raise self._terminal_conflict(current.interaction_id, current.status)
|
|
789
|
+
if current.revision != expected_revision:
|
|
790
|
+
raise InvalidCommandError(
|
|
791
|
+
"interaction revision does not match expected_revision",
|
|
792
|
+
details={
|
|
793
|
+
"reason": REVISION_MISMATCH,
|
|
794
|
+
"interaction_id": current.interaction_id,
|
|
795
|
+
"expected_revision": expected_revision,
|
|
796
|
+
"current_revision": current.revision,
|
|
797
|
+
},
|
|
798
|
+
)
|
|
799
|
+
updated = current.model_copy(
|
|
800
|
+
update={"status": status, "revision": current.revision + 1}
|
|
801
|
+
)
|
|
802
|
+
event_type = (
|
|
803
|
+
"interaction.cancelled" if status == "cancelled" else "interaction.expired"
|
|
804
|
+
)
|
|
805
|
+
stored = await self._events.append(
|
|
806
|
+
interaction_event(
|
|
807
|
+
updated,
|
|
808
|
+
event_type=event_type,
|
|
809
|
+
timestamp=now_iso(),
|
|
810
|
+
reason=reason,
|
|
811
|
+
),
|
|
812
|
+
guard=guard,
|
|
813
|
+
)
|
|
814
|
+
receipt = InteractionReceipt(
|
|
815
|
+
interaction_id=updated.interaction_id,
|
|
816
|
+
revision=updated.revision,
|
|
817
|
+
status=updated.status, # type: ignore[arg-type]
|
|
818
|
+
outcome=updated.status, # type: ignore[arg-type]
|
|
819
|
+
event_id=stored.event_id,
|
|
820
|
+
accepted_seq=stored.seq,
|
|
821
|
+
)
|
|
822
|
+
fresh["record"] = updated
|
|
823
|
+
return receipt
|
|
824
|
+
|
|
825
|
+
async def cancel(
|
|
826
|
+
self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
|
|
827
|
+
) -> InteractionReceipt:
|
|
828
|
+
return await self._terminal_command(
|
|
829
|
+
interaction_id,
|
|
830
|
+
expected_revision,
|
|
831
|
+
guard=guard,
|
|
832
|
+
status="cancelled",
|
|
833
|
+
reason="cancelled by owner",
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
async def expire(
|
|
837
|
+
self, interaction_id: str, expected_revision: int, *, guard: ActivationWriteGuard
|
|
838
|
+
) -> InteractionReceipt:
|
|
839
|
+
return await self._terminal_command(
|
|
840
|
+
interaction_id,
|
|
841
|
+
expected_revision,
|
|
842
|
+
guard=guard,
|
|
843
|
+
status="expired",
|
|
844
|
+
reason="interaction expired",
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
async def get(
|
|
848
|
+
self,
|
|
849
|
+
interaction_id: str,
|
|
850
|
+
*,
|
|
851
|
+
tenant_id: str | None = None,
|
|
852
|
+
agent_instance_id: str | None = None,
|
|
853
|
+
session_id: str | None = None,
|
|
854
|
+
run_id: str | None = None,
|
|
855
|
+
) -> InteractionRecord | None:
|
|
856
|
+
"""Read an opaque id only when it is unique or fully trusted-scoped.
|
|
857
|
+
|
|
858
|
+
Public interaction ids are not tenant grants. The worker always has
|
|
859
|
+
the Server-admitted command scope and must pass all four dimensions;
|
|
860
|
+
legacy local callers may omit all dimensions only while the id is
|
|
861
|
+
globally unambiguous.
|
|
862
|
+
"""
|
|
863
|
+
|
|
864
|
+
scope = (tenant_id, agent_instance_id, session_id, run_id)
|
|
865
|
+
if any(value is not None for value in scope):
|
|
866
|
+
if not all(value is not None for value in scope):
|
|
867
|
+
raise InvalidCommandError(
|
|
868
|
+
"interaction lookup requires a complete trusted scope",
|
|
869
|
+
details={"interaction_id": interaction_id},
|
|
870
|
+
)
|
|
871
|
+
matches = [
|
|
872
|
+
row
|
|
873
|
+
for (_, key), row in self._interactions.items()
|
|
874
|
+
if key == interaction_id
|
|
875
|
+
and row["record"].tenant_id == tenant_id
|
|
876
|
+
and row["record"].agent_instance_id == agent_instance_id
|
|
877
|
+
and row["record"].session_id == session_id
|
|
878
|
+
and row["record"].run_id == run_id
|
|
879
|
+
]
|
|
880
|
+
if len(matches) > 1: # pragma: no cover - backend key prevents it
|
|
881
|
+
raise InvalidCommandError(
|
|
882
|
+
f"interaction_id {interaction_id!r} is ambiguous in trusted scope",
|
|
883
|
+
details={"reason": REQUEST_CONFLICT, "interaction_id": interaction_id},
|
|
884
|
+
)
|
|
885
|
+
return matches[0]["record"] if matches else None
|
|
886
|
+
row = self._find_interaction(interaction_id)
|
|
887
|
+
return row["record"] if row is not None else None
|
|
888
|
+
|
|
889
|
+
async def list_pending_interactions(
|
|
890
|
+
self, tenant_id: str, session_id: str
|
|
891
|
+
) -> list[InteractionRecord]:
|
|
892
|
+
return [
|
|
893
|
+
row["record"]
|
|
894
|
+
for (tenant, _), row in sorted(self._interactions.items())
|
|
895
|
+
if tenant == tenant_id
|
|
896
|
+
and row["record"].session_id == session_id
|
|
897
|
+
and row["record"].status == "pending"
|
|
898
|
+
]
|
|
899
|
+
|
|
900
|
+
# ------------------------------------------------------------- activations
|
|
901
|
+
|
|
902
|
+
async def acquire_activation(self, request: ActivationLeaseRequest) -> ActivationLease:
|
|
903
|
+
key = (request.agent_instance_id, request.session_id)
|
|
904
|
+
async with self._lock(*key):
|
|
905
|
+
row = self._activations.get(key)
|
|
906
|
+
expires_at = time.time() + request.lease_ttl_seconds
|
|
907
|
+
if row is None:
|
|
908
|
+
token = 1
|
|
909
|
+
elif row["released"] or self._lease_expired(row):
|
|
910
|
+
token = row["fencing_token"] + 1
|
|
911
|
+
elif row["activation_id"] == request.activation_id:
|
|
912
|
+
token = row["fencing_token"]
|
|
913
|
+
else:
|
|
914
|
+
raise InvalidCommandError(
|
|
915
|
+
"activation lease is still held by another owner",
|
|
916
|
+
details={
|
|
917
|
+
"holder": row["activation_id"],
|
|
918
|
+
"lease_expires_at": row["lease_expires_at_iso"],
|
|
919
|
+
},
|
|
920
|
+
)
|
|
921
|
+
new_row = {
|
|
922
|
+
"agent_instance_id": request.agent_instance_id,
|
|
923
|
+
"session_id": request.session_id,
|
|
924
|
+
"activation_id": request.activation_id,
|
|
925
|
+
"fencing_token": token,
|
|
926
|
+
"lease_expires_at": expires_at,
|
|
927
|
+
"lease_expires_at_iso": datetime.fromtimestamp(
|
|
928
|
+
expires_at, tz=timezone.utc
|
|
929
|
+
).isoformat(),
|
|
930
|
+
"released": False,
|
|
931
|
+
"runtime_type": request.runtime_type,
|
|
932
|
+
"bundle_digest": request.bundle_digest,
|
|
933
|
+
"capability_digest": request.capability_digest,
|
|
934
|
+
}
|
|
935
|
+
self._activations[key] = new_row
|
|
936
|
+
return self._lease(request, new_row)
|
|
937
|
+
|
|
938
|
+
@staticmethod
|
|
939
|
+
def _lease(request: ActivationLeaseRequest, row: dict[str, Any]) -> ActivationLease:
|
|
940
|
+
return ActivationLease(
|
|
941
|
+
agent_instance_id=request.agent_instance_id,
|
|
942
|
+
activation_id=row["activation_id"],
|
|
943
|
+
fencing_token=row["fencing_token"],
|
|
944
|
+
lease_expires_at=row["lease_expires_at_iso"],
|
|
945
|
+
bundle_digest=row["bundle_digest"],
|
|
946
|
+
runtime_type=row["runtime_type"],
|
|
947
|
+
capability_digest=row["capability_digest"],
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
def _find_activation(self, activation_id: str) -> dict[str, Any]:
|
|
951
|
+
for row in self._activations.values():
|
|
952
|
+
if row["activation_id"] == activation_id:
|
|
953
|
+
return row
|
|
954
|
+
raise InvalidCommandError(f"unknown activation_id {activation_id!r}")
|
|
955
|
+
|
|
956
|
+
async def renew_activation(
|
|
957
|
+
self, activation_id: str, *, expected_fence: int, lease_ttl_seconds: float
|
|
958
|
+
) -> ActivationLease:
|
|
959
|
+
row = self._find_activation(activation_id)
|
|
960
|
+
key = self._activation_key(row)
|
|
961
|
+
async with self._lock(*key):
|
|
962
|
+
fresh = self._find_activation(activation_id)
|
|
963
|
+
if (
|
|
964
|
+
fresh["released"]
|
|
965
|
+
or self._lease_expired(fresh)
|
|
966
|
+
or fresh["fencing_token"] != int(expected_fence)
|
|
967
|
+
):
|
|
968
|
+
raise StaleFenceError(
|
|
969
|
+
f"cannot renew activation {activation_id!r} at fence {expected_fence}"
|
|
970
|
+
)
|
|
971
|
+
fresh["lease_expires_at"] = time.time() + lease_ttl_seconds
|
|
972
|
+
fresh["lease_expires_at_iso"] = now_iso()
|
|
973
|
+
request = ActivationLeaseRequest(
|
|
974
|
+
agent_instance_id=key[0],
|
|
975
|
+
session_id=key[1],
|
|
976
|
+
activation_id=fresh["activation_id"],
|
|
977
|
+
runtime_type=fresh["runtime_type"],
|
|
978
|
+
bundle_digest=fresh["bundle_digest"],
|
|
979
|
+
capability_digest=fresh["capability_digest"],
|
|
980
|
+
)
|
|
981
|
+
return self._lease(request, fresh)
|
|
982
|
+
|
|
983
|
+
async def release_activation(self, activation_id: str, *, expected_fence: int) -> None:
|
|
984
|
+
row = self._find_activation(activation_id)
|
|
985
|
+
key = self._activation_key(row)
|
|
986
|
+
async with self._lock(*key):
|
|
987
|
+
fresh = self._find_activation(activation_id)
|
|
988
|
+
if (
|
|
989
|
+
fresh["released"]
|
|
990
|
+
or fresh["fencing_token"] != int(expected_fence)
|
|
991
|
+
):
|
|
992
|
+
raise StaleFenceError(
|
|
993
|
+
f"cannot release activation {activation_id!r} at fence {expected_fence}"
|
|
994
|
+
)
|
|
995
|
+
fresh["released"] = True
|
|
996
|
+
fresh["lease_expires_at"] = time.time()
|
|
997
|
+
|
|
998
|
+
@staticmethod
|
|
999
|
+
def _activation_key(row: dict[str, Any]) -> tuple[str, str]:
|
|
1000
|
+
return (row["agent_instance_id"], row["session_id"])
|
|
1001
|
+
|
|
1002
|
+
# ------------------------------------------------------------------ events
|
|
1003
|
+
|
|
1004
|
+
async def validate_write_fence(
|
|
1005
|
+
self,
|
|
1006
|
+
envelope: SessionEventEnvelope,
|
|
1007
|
+
guard: ActivationWriteGuard,
|
|
1008
|
+
) -> None:
|
|
1009
|
+
"""SessionEventStore fence seam:guard 必须是当前未过期 lease 的 owner。
|
|
1010
|
+
|
|
1011
|
+
被 takeover(activation 行被替换/释放)或 token 滞后的旧 owner 得到
|
|
1012
|
+
:class:`StaleFenceError`;不做任何写入。
|
|
1013
|
+
"""
|
|
1014
|
+
|
|
1015
|
+
row = next(
|
|
1016
|
+
(
|
|
1017
|
+
candidate
|
|
1018
|
+
for candidate in self._activations.values()
|
|
1019
|
+
if candidate["activation_id"] == guard.activation_id
|
|
1020
|
+
),
|
|
1021
|
+
None,
|
|
1022
|
+
)
|
|
1023
|
+
if (
|
|
1024
|
+
row is None
|
|
1025
|
+
or row.get("released")
|
|
1026
|
+
or self._lease_expired(row)
|
|
1027
|
+
or row["fencing_token"] != int(guard.fencing_token)
|
|
1028
|
+
):
|
|
1029
|
+
raise StaleFenceError(
|
|
1030
|
+
"activation write guard does not match the current lease",
|
|
1031
|
+
details={
|
|
1032
|
+
"activation_id": guard.activation_id,
|
|
1033
|
+
"fencing_token": int(guard.fencing_token),
|
|
1034
|
+
"session_id": envelope.session_id,
|
|
1035
|
+
},
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
async def append_event(
|
|
1039
|
+
self,
|
|
1040
|
+
envelope: SessionEventEnvelope,
|
|
1041
|
+
*,
|
|
1042
|
+
expected_fence: int,
|
|
1043
|
+
agent_instance_id: str | None = None,
|
|
1044
|
+
) -> SessionEventEnvelope:
|
|
1045
|
+
row = self._resolve_activation(envelope.session_id, agent_instance_id)
|
|
1046
|
+
async with self._lock(row["agent_instance_id"], envelope.session_id):
|
|
1047
|
+
fresh = self._resolve_activation(envelope.session_id, agent_instance_id)
|
|
1048
|
+
activation = self._check_fence(
|
|
1049
|
+
fresh["agent_instance_id"], envelope.session_id, expected_fence
|
|
1050
|
+
)
|
|
1051
|
+
return await self._events.append(
|
|
1052
|
+
envelope, guard=ActivationWriteGuard(
|
|
1053
|
+
activation_id=activation["activation_id"],
|
|
1054
|
+
fencing_token=int(expected_fence),
|
|
1055
|
+
)
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
def _resolve_activation(
|
|
1059
|
+
self, session_id: str, agent_instance_id: str | None
|
|
1060
|
+
) -> dict[str, Any]:
|
|
1061
|
+
if agent_instance_id is not None:
|
|
1062
|
+
row = self._activation_row(agent_instance_id, session_id)
|
|
1063
|
+
if row is None:
|
|
1064
|
+
raise StaleFenceError(
|
|
1065
|
+
"no active activation lease",
|
|
1066
|
+
details={"agent_instance_id": agent_instance_id, "session_id": session_id},
|
|
1067
|
+
)
|
|
1068
|
+
return row
|
|
1069
|
+
matches = [
|
|
1070
|
+
row
|
|
1071
|
+
for (agent, session), row in self._activations.items()
|
|
1072
|
+
if session == session_id and row.get("released") is not True
|
|
1073
|
+
]
|
|
1074
|
+
if len(matches) != 1:
|
|
1075
|
+
raise StaleFenceError(
|
|
1076
|
+
"cannot resolve a single activation lease for session",
|
|
1077
|
+
details={"session_id": session_id, "matches": len(matches)},
|
|
1078
|
+
)
|
|
1079
|
+
return matches[0]
|
|
1080
|
+
|
|
1081
|
+
# -------------------------------------------------------------------- runs
|
|
1082
|
+
|
|
1083
|
+
async def load_run(self, run_id: str) -> RunRecord | None:
|
|
1084
|
+
return self._runs.get(run_id)
|
|
1085
|
+
|
|
1086
|
+
async def save_run_transition(
|
|
1087
|
+
self, run: RunRecord, *, expected_fence: int
|
|
1088
|
+
) -> RunRecord:
|
|
1089
|
+
async with self._lock(run.agent_instance_id, run.session_id):
|
|
1090
|
+
activation = self._check_fence(
|
|
1091
|
+
run.agent_instance_id, run.session_id, expected_fence
|
|
1092
|
+
)
|
|
1093
|
+
existing = self._runs.get(run.run_id)
|
|
1094
|
+
assert_run_transition(existing.state if existing else None, run.state)
|
|
1095
|
+
if is_active_run(run.state):
|
|
1096
|
+
for other in self._runs.values():
|
|
1097
|
+
if (
|
|
1098
|
+
other.run_id != run.run_id
|
|
1099
|
+
and other.session_id == run.session_id
|
|
1100
|
+
and is_active_run(other.state)
|
|
1101
|
+
):
|
|
1102
|
+
raise InvalidCommandError(
|
|
1103
|
+
"session already has an active run",
|
|
1104
|
+
details={
|
|
1105
|
+
"session_id": run.session_id,
|
|
1106
|
+
"active_run_id": other.run_id,
|
|
1107
|
+
},
|
|
1108
|
+
)
|
|
1109
|
+
stored = run.model_copy(
|
|
1110
|
+
update={
|
|
1111
|
+
"activation_fence": int(expected_fence),
|
|
1112
|
+
"created_at": existing.created_at if existing else now_iso(),
|
|
1113
|
+
"updated_at": now_iso(),
|
|
1114
|
+
}
|
|
1115
|
+
)
|
|
1116
|
+
self._runs[run.run_id] = stored
|
|
1117
|
+
await self._emit(
|
|
1118
|
+
control_event(
|
|
1119
|
+
session_id=run.session_id,
|
|
1120
|
+
event_type="control.run_transition",
|
|
1121
|
+
payload={
|
|
1122
|
+
"run_id": run.run_id,
|
|
1123
|
+
"state": run.state.value,
|
|
1124
|
+
"fencing_token": int(expected_fence),
|
|
1125
|
+
},
|
|
1126
|
+
run_id=run.run_id,
|
|
1127
|
+
),
|
|
1128
|
+
activation_row=activation,
|
|
1129
|
+
)
|
|
1130
|
+
return stored
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
__all__ = ["InMemoryAgentKernelStore"]
|