agentengine-sdk-python 0.8.1__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.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +22 -2
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/RECORD +356 -222
- ksadk/a2a/_space_client_events.py +381 -0
- ksadk/a2a/event_adapter.py +156 -64
- ksadk/a2a/executor.py +179 -63
- ksadk/a2a/space_client.py +6 -333
- ksadk/a2ui/core.py +222 -53
- ksadk/agui/_agent_helpers.py +243 -0
- ksadk/agui/a2ui_projection.py +12 -5
- ksadk/agui/agent.py +443 -259
- ksadk/api/client.py +525 -27
- ksadk/builders/code_builder.py +284 -8
- ksadk/builders/container_builder.py +8 -1
- ksadk/builders/managed_runtime_builder.py +39 -35
- ksadk/cli/__init__.py +17 -0
- ksadk/cli/cmd_dashboard.py +15 -1
- ksadk/cli/cmd_deploy.py +10 -2
- ksadk/cli/cmd_eval.py +66 -9
- ksadk/cli/cmd_evalset.py +284 -0
- ksadk/cli/cmd_files.py +5 -1
- ksadk/cli/cmd_hermes.py +1 -126
- ksadk/cli/cmd_invoke.py +28 -38
- ksadk/cli/cmd_managed_runtime.py +95 -0
- ksadk/cli/cmd_observe.py +85 -0
- ksadk/cli/cmd_openclaw.py +1 -435
- ksadk/cli/cmd_replay.py +28 -4
- ksadk/cli/cmd_studio.py +99 -8
- ksadk/cli/cmd_web.py +10 -2
- ksadk/cli/hermes_env.py +151 -0
- ksadk/cli/invoke_payload.py +49 -0
- ksadk/cli/openclaw_env.py +458 -0
- ksadk/codex/client.py +180 -30
- ksadk/codex/runtime.py +428 -326
- ksadk/configs/env_registry.py +66 -12
- ksadk/configs/env_registry_pcm.py +181 -0
- ksadk/configs/env_var_spec.py +18 -0
- ksadk/configs/global_config.py +5 -0
- 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/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 +184 -2
- ksadk/conversations/runtime_invocation.py +81 -3
- ksadk/conversations/runtime_observability.py +141 -0
- ksadk/conversations/runtime_payloads.py +42 -0
- ksadk/conversations/runtime_preparation.py +272 -2
- ksadk/conversations/runtime_resume.py +20 -1
- ksadk/conversations/runtime_stream_events.py +94 -9
- 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 +5 -0
- ksadk/deployment/providers/serverless.py +143 -69
- ksadk/evaluation/__init__.py +32 -0
- ksadk/evaluation/a2a_adapter.py +9 -1
- ksadk/evaluation/adapters.py +18 -5
- 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 +73 -4
- ksadk/evaluation/evaluators.py +183 -17
- ksadk/evaluation/evidence.py +258 -0
- ksadk/evaluation/executor.py +31 -6
- ksadk/evaluation/local_adapter.py +550 -0
- ksadk/evaluation/service_env.py +27 -0
- ksadk/evaluation/studio_build_adapter.py +253 -0
- ksadk/evaluation/target.py +15 -6
- 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 -265
- ksadk/events/session_event.py +324 -0
- ksadk/events/store.py +3 -316
- ksadk/events/v1_compat.py +41 -0
- ksadk/harness/runtime.py +160 -57
- 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 +7 -0
- ksadk/knowledge_base/service.py +32 -5
- 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 +384 -2
- ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
- 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 +188 -6
- ksadk/model_proxy/bootstrap.py +16 -9
- ksadk/model_proxy/detect.py +202 -9
- ksadk/model_proxy/namespace.py +39 -15
- ksadk/model_proxy/server.py +24 -5
- ksadk/model_proxy/transform.py +60 -2
- 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 +5 -5
- ksadk/runners/base_runner.py +16 -0
- ksadk/runners/factory.py +1 -2
- ksadk/runners/langgraph_runner.py +198 -437
- ksadk/runtime/_runner_adapter/__init__.py +1 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
- ksadk/runtime/adapter.py +118 -2
- ksadk/runtime/conversation_execution.py +316 -182
- ksadk/runtime/executor.py +144 -3
- ksadk/runtime/factory.py +193 -7
- ksadk/runtime/hosted_finalizer.py +268 -0
- ksadk/runtime/launch.py +14 -1
- ksadk/runtime/preprocessing.py +25 -3
- ksadk/runtime/runner_adapter.py +288 -508
- ksadk/runtime/usage.py +33 -0
- ksadk/sandbox/backends/e2b.py +35 -2
- ksadk/server/composition.py +8 -0
- ksadk/server/factory.py +69 -6
- ksadk/server/routes/kernel_ingress.py +258 -0
- ksadk/server/routes/openai_compat.py +88 -8
- ksadk/server/routes/projection.py +35 -20
- ksadk/server/routes/run.py +94 -0
- ksadk/server/routes/sessions.py +2 -0
- ksadk/server/routes/streaming.py +5 -2
- ksadk/server/static/assets/{ArtifactsPanel-CakCqxEN.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-CynLknyO.js → CodeBlock-DOH6MVcn.js} +4 -4
- ksadk/server/static/assets/{MathMessageMarkdown-3sQdOysS.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-B_fH2YuH.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-D6rFLaVV.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
- ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
- ksadk/server/static/assets/{chunk-4BX2VUAB-B9drTgOW.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-ChNs_Mdk.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-Dli7qW-B.js → dist-8hqcx0sU.js} +1 -1
- ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
- ksadk/server/static/assets/{dist-C1sz5Eo3.js → dist-BE7bu-DL.js} +1 -1
- ksadk/server/static/assets/{dist-fhOuWds5.js → dist-BVDffZ0U.js} +1 -1
- ksadk/server/static/assets/{dist-BjOdlDjj.js → dist-BX8z72IC.js} +1 -1
- ksadk/server/static/assets/{dist-DsHPdk3W.js → dist-Bf2M8m3N.js} +1 -1
- ksadk/server/static/assets/{dist-o2OVmKvt.js → dist-BjU6y-A2.js} +1 -1
- ksadk/server/static/assets/{dist-DWKByxWV.js → dist-BytlxIDT.js} +1 -1
- ksadk/server/static/assets/{dist-DJN_6Y39.js → dist-CSh_DE14.js} +1 -1
- ksadk/server/static/assets/{dist-DQU9EWw-.js → dist-CXYYBw3_.js} +1 -1
- ksadk/server/static/assets/{dist-4N2NMVYg.js → dist-C_wsv-Qd.js} +1 -1
- ksadk/server/static/assets/{dist-gLnrGsoU.js → dist-ClxGviKw.js} +1 -1
- ksadk/server/static/assets/{dist-j6Dxmf_j.js → dist-CttYUqzS.js} +1 -1
- ksadk/server/static/assets/{dist-D6MpCZtz.js → dist-DNp6rLRA.js} +1 -1
- ksadk/server/static/assets/{dist-BB3-OwTQ.js → dist-DUX-id_F.js} +1 -1
- ksadk/server/static/assets/{dist-_pHPTyZ-.js → dist-DmldrHd_.js} +1 -1
- ksadk/server/static/assets/{dist-CWV4lDr1.js → dist-DnfXs8Vn.js} +2 -2
- ksadk/server/static/assets/{dist-B5oS1zcj.js → dist-Dq9gLD7L.js} +1 -1
- ksadk/server/static/assets/{dist-B-jNR6Vn.js → dist-W-TIVntx.js} +1 -1
- ksadk/server/static/assets/{dist-BIlKoW0k.js → dist-dDdADKFA.js} +1 -1
- ksadk/server/static/assets/{dist-X4TnqwOK.js → dist-dydu68Fl.js} +1 -1
- ksadk/server/static/assets/{dist-CmZHzlwZ.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-K0OOfVna.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-8LeOJaZp.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-C6uCgZrW.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
- ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
- ksadk/server/static/assets/{linear-hdBcYzM8.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-Bjy_2v4_.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-BmkRbwCg.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-l6iXmU65.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-Cq6UBeQB.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-DEz24vV0.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/studio/api.py +577 -25
- ksadk/studio/api_contracts.py +114 -5
- ksadk/studio/api_memory_routes.py +82 -0
- ksadk/studio/authoring.py +252 -27
- ksadk/studio/authoring_coordinator.py +799 -67
- ksadk/studio/builder.py +88 -11
- ksadk/studio/capabilities.py +17 -0
- ksadk/studio/cloud.py +1916 -95
- ksadk/studio/codex_agent_service.py +125 -14
- ksadk/studio/codex_authoring.py +469 -0
- ksadk/studio/codex_builder.py +138 -25
- ksadk/studio/codex_manifest.py +51 -22
- ksadk/studio/codex_run.py +46 -3
- ksadk/studio/compiler.py +1 -0
- ksadk/studio/contracts.py +114 -3
- ksadk/studio/event_store.py +25 -95
- ksadk/studio/framework_run.py +231 -3
- ksadk/studio/hosted_kernel.py +317 -0
- ksadk/studio/manifest_resolver.py +131 -0
- ksadk/studio/model_client.py +95 -9
- ksadk/studio/model_profile_service.py +1 -1
- ksadk/studio/operations.py +37 -5
- ksadk/studio/otel_trace.py +348 -42
- ksadk/studio/pcm_memory.py +85 -0
- ksadk/studio/repository.py +23 -2
- ksadk/studio/resource_catalog.py +43 -0
- ksadk/studio/run_service.py +563 -137
- ksadk/studio/runtime_source.py +42 -16
- ksadk/studio/service.py +1180 -72
- ksadk/studio/shared_web.py +32 -22
- ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
- ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
- ksadk/studio/static/index.html +2 -2
- ksadk/studio/templates.py +100 -8
- ksadk/studio/validator.py +19 -0
- ksadk/studio/workspace.py +30 -32
- ksadk/tracing/setup.py +304 -3
- ksadk/version.py +1 -1
- ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
- ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
- ksadk/events/parser.py +0 -191
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
- ksadk/server/static/assets/MermaidBlock-NERD_ef7.js +0 -303
- ksadk/server/static/assets/NativeTerminalPanel-CiHWf82q.js +0 -1
- ksadk/server/static/assets/arc-DrgjcKhl.js +0 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-CpZLRCcY.js +0 -1
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-BXoVRbQ6.js +0 -36
- ksadk/server/static/assets/blockDiagram-GPEHLZMM-BnaB3Ywv.js +0 -132
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-Dw4weZcp.js +0 -10
- ksadk/server/static/assets/channel-Dt3rQTFI.js +0 -1
- ksadk/server/static/assets/chunk-2J33WTMH-ChjpLK7j.js +0 -1
- ksadk/server/static/assets/chunk-55IACEB6-CK4ROATy.js +0 -1
- ksadk/server/static/assets/chunk-727SXJPM-B9KtTTgw.js +0 -206
- ksadk/server/static/assets/chunk-AQP2D5EJ-1O1mki3m.js +0 -231
- ksadk/server/static/assets/chunk-QZHKN3VN-iXEZtKQc.js +0 -1
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-DIveV17Q.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-DIveV17Q.js +0 -1
- ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
- ksadk/server/static/assets/dagre-BM42HDAG-D0QXg_MQ.js +0 -4
- ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
- ksadk/server/static/assets/diagram-2AECGRRQ-DBDDKwBQ.js +0 -43
- ksadk/server/static/assets/diagram-5GNKFQAL-BkwDOxmO.js +0 -10
- ksadk/server/static/assets/diagram-KO2AKTUF-Bv8waYvY.js +0 -3
- ksadk/server/static/assets/diagram-LMA3HP47-e1VECDdu.js +0 -24
- ksadk/server/static/assets/diagram-OG6HWLK6-Ds9adfRl.js +0 -24
- ksadk/server/static/assets/dist-B5J2uhNk.js +0 -1
- ksadk/server/static/assets/erDiagram-TEJ5UH35-BPItFKH6.js +0 -85
- ksadk/server/static/assets/eventmodeling-FCH6USID-DYFSxbqr.js +0 -1
- ksadk/server/static/assets/flowDiagram-I6XJVG4X-BbYB9w5Y.js +0 -162
- ksadk/server/static/assets/ganttDiagram-6RSMTGT7-TQXa0KXv.js +0 -292
- ksadk/server/static/assets/gitGraph-WXDBUCRP-DyNYp6BN.js +0 -1
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-bKblCnpa.js +0 -106
- ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
- ksadk/server/static/assets/index-R52pJJvg.js +0 -177
- ksadk/server/static/assets/index-jJr3_955.css +0 -2
- ksadk/server/static/assets/info-J43DQDTF-WUx7qTde.js +0 -1
- ksadk/server/static/assets/infoDiagram-5YYISTIA-BGVT7J_i.js +0 -2
- ksadk/server/static/assets/katex-CYWseY5E.js +0 -265
- ksadk/server/static/assets/mermaid-parser.core-BWrDjSZM.js +0 -161
- ksadk/server/static/assets/packet-YPE3B663-BN13y_xl.js +0 -1
- ksadk/server/static/assets/pie-LRSECV5Y-ftSLXhhc.js +0 -1
- ksadk/server/static/assets/pieDiagram-4H26LBE5-GmmqNiVH.js +0 -30
- ksadk/server/static/assets/radar-GUYGQ44K-jaICkIeO.js +0 -1
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DSWFqL7c.js +0 -84
- ksadk/server/static/assets/stateDiagram-AJRCARHV-qzTQpg3Y.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-BYCoYGAN.js +0 -1
- ksadk/server/static/assets/treeView-BLDUP644-Cpib_vlz.js +0 -1
- ksadk/server/static/assets/treemap-LRROVOQU-DQxfmVHK.js +0 -1
- ksadk/server/static/assets/vennDiagram-CIIHVFJN-CMCixki2.js +0 -34
- ksadk/server/static/assets/wardley-L42UT6IY-BQYHnnkU.js +0 -1
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-CVzB8F9K.js +0 -78
- ksadk/server/static/assets/xychartDiagram-2RQKCTM6-BFBqBLc1.js +0 -7
- ksadk/studio/evaluation.py +0 -192
- ksadk/studio/static/assets/index-CaEN0eTR.css +0 -1
- ksadk/studio/static/assets/index-DI2eY_NH.js +0 -252
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
ksadk/kernel/worker.py
ADDED
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""per-session FIFO worker(Phase 1 Task 6 Step 6)。
|
|
3
|
+
|
|
4
|
+
- 持有 activation lease(fencing token 通过 Store 的 CAS 校验)才能 claim。
|
|
5
|
+
- 按 per-session accepted_seq 保序;active Run 存在时普通 enqueue 保持排队,
|
|
6
|
+
只执行允许作用于该 Run 的控制命令(interrupt/pause/steer/...)。
|
|
7
|
+
- 异常分类:retryable kernel error(消息保持 claimed)、typed runtime
|
|
8
|
+
rejection(discarded + control.command_rejected)、terminal failure
|
|
9
|
+
(不 ack,消息保持 claimed 等待 takeover reclaim)。
|
|
10
|
+
|
|
11
|
+
Task 6:Activation 通过 :class:`ActiveExecution` 拥有 Adapter/RunHandle/
|
|
12
|
+
InteractionProvider——控制命令与 Interaction 回包永远作用于同一 live
|
|
13
|
+
execution(同一 client 实例);control lookup 永远按 durable run id。
|
|
14
|
+
``submit_interaction`` 不再是静态 ``adapter.submit`` 映射:Worker 载入权威
|
|
15
|
+
``InteractionRecord``,调用其绑定 provider 送达回包,provider 接受后才写
|
|
16
|
+
``InteractionResolved``,同 fence 恢复 stream 消费。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from collections.abc import Callable, Mapping
|
|
25
|
+
from dataclasses import dataclass, replace
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from typing import Literal
|
|
28
|
+
|
|
29
|
+
from ksadk.interaction.contracts import (
|
|
30
|
+
InteractionSubmission,
|
|
31
|
+
)
|
|
32
|
+
from ksadk.interaction.provider import (
|
|
33
|
+
RUNTIME_INTERACTION_UNAVAILABLE,
|
|
34
|
+
InteractionProvider,
|
|
35
|
+
InteractionResolveContext,
|
|
36
|
+
UnavailableInteractionProvider,
|
|
37
|
+
)
|
|
38
|
+
from ksadk.interaction.providers import default_interaction_providers
|
|
39
|
+
from ksadk.kernel.contracts import (
|
|
40
|
+
ActivationLease,
|
|
41
|
+
ActivationWriteGuard,
|
|
42
|
+
AgentControlCommand,
|
|
43
|
+
SubmitInteractionPayload,
|
|
44
|
+
)
|
|
45
|
+
from ksadk.kernel.contracts import (
|
|
46
|
+
InjectPayload as ContractInjectPayload,
|
|
47
|
+
)
|
|
48
|
+
from ksadk.kernel.contracts import (
|
|
49
|
+
SteerPayload as ContractSteerPayload,
|
|
50
|
+
)
|
|
51
|
+
from ksadk.kernel.errors import (
|
|
52
|
+
AgentKernelError,
|
|
53
|
+
InvalidCommandError,
|
|
54
|
+
StaleFenceError,
|
|
55
|
+
UnsupportedControlError,
|
|
56
|
+
)
|
|
57
|
+
from ksadk.kernel.mapping import COMMAND_HANDLERS, RESUME_TARGET_KINDS
|
|
58
|
+
from ksadk.kernel.state import RunState
|
|
59
|
+
from ksadk.kernel.store import (
|
|
60
|
+
AgentKernelStore,
|
|
61
|
+
RunRecord,
|
|
62
|
+
control_event,
|
|
63
|
+
new_message_id,
|
|
64
|
+
)
|
|
65
|
+
from ksadk.runtime.adapter import (
|
|
66
|
+
CancelResult,
|
|
67
|
+
PauseResult,
|
|
68
|
+
RunHandle,
|
|
69
|
+
RuntimeAdapter,
|
|
70
|
+
StartRequest,
|
|
71
|
+
)
|
|
72
|
+
from ksadk.runtime.adapter import (
|
|
73
|
+
ResumePayload as AdapterResumePayload,
|
|
74
|
+
)
|
|
75
|
+
from ksadk.runtime.adapter import (
|
|
76
|
+
ResumeTarget as AdapterResumeTarget,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
logger = logging.getLogger(__name__)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
WorkOutcome = Literal["idle", "claimed", "completed", "retryable_failure", "terminal_failure"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class ActiveExecution:
|
|
87
|
+
"""一个 activation 拥有的 live execution(Task 6 Step 4)。
|
|
88
|
+
|
|
89
|
+
owner 真相仍在 Store 的 RunRecord(durable run id);本结构只是当前
|
|
90
|
+
进程持有 lease 期间的运行期句柄集合——adapter(含框架 client)、
|
|
91
|
+
live handle 与回包送达 provider 必须同源,否则回包会打到另一个
|
|
92
|
+
client 实例上静默丢失。
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
durable_run_id: str
|
|
96
|
+
runtime_run_id: str
|
|
97
|
+
adapter: RuntimeAdapter
|
|
98
|
+
handle: RunHandle
|
|
99
|
+
interaction_provider: InteractionProvider
|
|
100
|
+
stream_task: asyncio.Task[None] | None = None
|
|
101
|
+
stream_guard: ActivationWriteGuard | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class WorkResult:
|
|
106
|
+
"""进程内调度结果,不是公网协议。idle 时后三项可为空。"""
|
|
107
|
+
|
|
108
|
+
outcome: WorkOutcome
|
|
109
|
+
message_id: str | None = None
|
|
110
|
+
run_id: str | None = None
|
|
111
|
+
last_seq: int | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AgentKernelWorker:
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
store: AgentKernelStore,
|
|
118
|
+
*,
|
|
119
|
+
adapter_factory: Callable[[], RuntimeAdapter],
|
|
120
|
+
session_events: object | None = None,
|
|
121
|
+
interaction_providers: Mapping[str, InteractionProvider] | None = None,
|
|
122
|
+
start_request_defaults: Mapping[str, object] | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
self._store = store
|
|
125
|
+
self._adapter_factory = adapter_factory
|
|
126
|
+
# SessionEventStore(typed RuntimeEventStore 的 envelope 写路径)。
|
|
127
|
+
# 缺省时不落 runtime 事件,仅保证 stream 被消费到自然结束。
|
|
128
|
+
self._session_events = session_events
|
|
129
|
+
# Deployment-owned defaults (model, prompt and sandbox) come from the
|
|
130
|
+
# admitted immutable manifest. Server may attach a bounded per-turn
|
|
131
|
+
# model/approval selector to the signed command; the worker validates
|
|
132
|
+
# the model allow-list and never lets that selector replace sandbox.
|
|
133
|
+
self._start_request_defaults = dict(start_request_defaults or {})
|
|
134
|
+
# Task 6:activation 拥有 Adapter/RunHandle/Provider 的 live 表。
|
|
135
|
+
# key 永远是 durable run id;cache miss 不能等价于 Run 不存在
|
|
136
|
+
# (只能说明本进程未 attach,takeover 后由 adopt_execution 重建)。
|
|
137
|
+
self._executions: dict[str, ActiveExecution] = {}
|
|
138
|
+
self._providers: dict[str, InteractionProvider] = (
|
|
139
|
+
dict(interaction_providers)
|
|
140
|
+
if interaction_providers is not None
|
|
141
|
+
else default_interaction_providers()
|
|
142
|
+
)
|
|
143
|
+
# A stream may fail after its enqueue has been durably completed. Keep
|
|
144
|
+
# the exception observable to diagnostics without leaving an unhandled
|
|
145
|
+
# Task warning; the durable run remains open for recovery/takeover.
|
|
146
|
+
self._background_stream_errors: dict[str, Exception] = {}
|
|
147
|
+
# ``ActivationLease`` is a frozen wire contract and intentionally does
|
|
148
|
+
# not carry ``session_id``. Production composition roots therefore
|
|
149
|
+
# pass the session scope to ``run_once`` explicitly. Serialize that
|
|
150
|
+
# scope in-process as well: two scheduler ticks for the same Session
|
|
151
|
+
# must never both list/claim the Inbox head before either claim has
|
|
152
|
+
# completed. The durable activation/fence remains the cross-process
|
|
153
|
+
# authority; this lock closes the same-owner re-entrancy window.
|
|
154
|
+
self._session_locks: dict[tuple[str, str], asyncio.Lock] = {}
|
|
155
|
+
|
|
156
|
+
def attach_handle(
|
|
157
|
+
self,
|
|
158
|
+
run_id: str,
|
|
159
|
+
handle: RunHandle,
|
|
160
|
+
*,
|
|
161
|
+
adapter: RuntimeAdapter | None = None,
|
|
162
|
+
provider: InteractionProvider | None = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""把 live handle 注册为当前 activation 的 ActiveExecution。"""
|
|
165
|
+
|
|
166
|
+
self.adopt_execution(
|
|
167
|
+
durable_run_id=run_id,
|
|
168
|
+
runtime_run_id=handle.run_id,
|
|
169
|
+
adapter=adapter if adapter is not None else self._adapter_factory(),
|
|
170
|
+
handle=handle,
|
|
171
|
+
provider=provider,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def adopt_execution(
|
|
175
|
+
self,
|
|
176
|
+
*,
|
|
177
|
+
durable_run_id: str,
|
|
178
|
+
runtime_run_id: str,
|
|
179
|
+
adapter: RuntimeAdapter,
|
|
180
|
+
handle: RunHandle,
|
|
181
|
+
provider: InteractionProvider | None = None,
|
|
182
|
+
) -> ActiveExecution:
|
|
183
|
+
"""takeover 后重建 ActiveExecution(仅在 lease 获取 + attach/resume
|
|
184
|
+
成功后由 RecoveryCoordinator 调用;provider 按 runtime_type 解析)。"""
|
|
185
|
+
|
|
186
|
+
if provider is None:
|
|
187
|
+
provider = self._providers.get(handle.runtime_type, UnavailableInteractionProvider())
|
|
188
|
+
execution = ActiveExecution(
|
|
189
|
+
durable_run_id=durable_run_id,
|
|
190
|
+
runtime_run_id=runtime_run_id,
|
|
191
|
+
adapter=adapter,
|
|
192
|
+
handle=handle,
|
|
193
|
+
interaction_provider=provider,
|
|
194
|
+
)
|
|
195
|
+
self._executions[durable_run_id] = execution
|
|
196
|
+
return execution
|
|
197
|
+
|
|
198
|
+
def execution_for(self, durable_run_id: str) -> ActiveExecution | None:
|
|
199
|
+
"""control lookup 入口:永远按 durable run id 查 live execution。"""
|
|
200
|
+
|
|
201
|
+
return self._executions.get(durable_run_id)
|
|
202
|
+
|
|
203
|
+
def active_session_ids(self) -> set[str]:
|
|
204
|
+
"""Sessions whose activation must stay alive after Inbox ack.
|
|
205
|
+
|
|
206
|
+
An enqueue is acknowledged once ``adapter.start`` returns, while its
|
|
207
|
+
RuntimeEvent stream may continue for minutes. The composition root
|
|
208
|
+
uses this set to renew the lease during that interval; relying only on
|
|
209
|
+
accepted/claimed Inbox messages opens a stale-fence window mid-stream.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
return {execution.handle.session_id for execution in self._executions.values()}
|
|
213
|
+
|
|
214
|
+
async def run_once(
|
|
215
|
+
self,
|
|
216
|
+
agent_instance_id: str,
|
|
217
|
+
activation: ActivationLease,
|
|
218
|
+
*,
|
|
219
|
+
session_id: str | None = None,
|
|
220
|
+
) -> WorkResult:
|
|
221
|
+
if session_id is not None:
|
|
222
|
+
key = (agent_instance_id, session_id)
|
|
223
|
+
lock = self._session_locks.setdefault(key, asyncio.Lock())
|
|
224
|
+
async with lock:
|
|
225
|
+
return await self._run_once(agent_instance_id, activation, session_id=session_id)
|
|
226
|
+
# Compatibility for direct/test callers written before the internal
|
|
227
|
+
# scheduler API became session-scoped. Production callers below all
|
|
228
|
+
# provide ``session_id``; the frozen ActivationLease JSON is unchanged.
|
|
229
|
+
return await self._run_once(agent_instance_id, activation, session_id=None)
|
|
230
|
+
|
|
231
|
+
async def _run_once(
|
|
232
|
+
self,
|
|
233
|
+
agent_instance_id: str,
|
|
234
|
+
activation: ActivationLease,
|
|
235
|
+
*,
|
|
236
|
+
session_id: str | None,
|
|
237
|
+
) -> WorkResult:
|
|
238
|
+
fence = activation.fencing_token
|
|
239
|
+
pending = await self._store.list_pending(
|
|
240
|
+
agent_instance_id, session_id=session_id, fencing_token=fence
|
|
241
|
+
)
|
|
242
|
+
if not pending:
|
|
243
|
+
return WorkResult(outcome="idle")
|
|
244
|
+
|
|
245
|
+
# per-session FIFO:通常按 accepted_seq 执行;但 active Run 会挡住
|
|
246
|
+
# enqueue,此时其后的 interrupt/pause/steer 等控制命令必须能越过
|
|
247
|
+
# 该 enqueue 作用于 active Run。只处理当前 activation 持有 lease 的
|
|
248
|
+
# session,避免跨 session 抢占。
|
|
249
|
+
eligible = None
|
|
250
|
+
for message in sorted(pending, key=lambda m: m.accepted_seq):
|
|
251
|
+
if session_id is not None and message.session_id != session_id:
|
|
252
|
+
continue
|
|
253
|
+
lease = await self._store.current_lease(agent_instance_id, message.session_id)
|
|
254
|
+
if lease is None or lease.activation_id != activation.activation_id:
|
|
255
|
+
continue # 该 session 归其它 activation(或无人)持有
|
|
256
|
+
if message.command is None: # pragma: no cover - defensive
|
|
257
|
+
continue
|
|
258
|
+
if message.command.command_type == "enqueue":
|
|
259
|
+
active = await self._store.find_active_run(agent_instance_id, message.session_id)
|
|
260
|
+
if active is not None:
|
|
261
|
+
continue # enqueue 保持排队
|
|
262
|
+
eligible = message
|
|
263
|
+
break
|
|
264
|
+
if eligible is None:
|
|
265
|
+
return WorkResult(outcome="idle")
|
|
266
|
+
|
|
267
|
+
claimed = await self._store.claim_message(eligible.message_id, fence)
|
|
268
|
+
result = await self._execute_claim(claimed.command, activation)
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
# ------------------------------------------------------------- execution
|
|
272
|
+
|
|
273
|
+
async def _execute_claim(
|
|
274
|
+
self, command: AgentControlCommand, activation: ActivationLease
|
|
275
|
+
) -> WorkResult:
|
|
276
|
+
fence = activation.fencing_token
|
|
277
|
+
message_id = await self._message_id_for(command)
|
|
278
|
+
try:
|
|
279
|
+
run_id = await self._dispatch(command, activation)
|
|
280
|
+
except (UnsupportedControlError, InvalidCommandError) as error:
|
|
281
|
+
# typed rejection:确定性收口,不重试。
|
|
282
|
+
await self._store.append_event(
|
|
283
|
+
control_event(
|
|
284
|
+
session_id=command.session_id,
|
|
285
|
+
event_type="control.command_rejected",
|
|
286
|
+
payload={
|
|
287
|
+
"command_id": str(command.command_id),
|
|
288
|
+
"status": "rejected",
|
|
289
|
+
"reason": getattr(error, "code", "unsupported"),
|
|
290
|
+
},
|
|
291
|
+
),
|
|
292
|
+
expected_fence=fence,
|
|
293
|
+
agent_instance_id=command.agent_instance_id,
|
|
294
|
+
)
|
|
295
|
+
await self._store.discard_claim(message_id, expected_fence=fence)
|
|
296
|
+
return WorkResult(outcome="completed", message_id=message_id)
|
|
297
|
+
except StaleFenceError:
|
|
298
|
+
return WorkResult(outcome="terminal_failure", message_id=message_id)
|
|
299
|
+
except AgentKernelError as error:
|
|
300
|
+
if error.code == RUNTIME_INTERACTION_UNAVAILABLE:
|
|
301
|
+
# typed rejection:provider 诚实声明无法原生送达回包,
|
|
302
|
+
# Interaction 绝不标 resolved。
|
|
303
|
+
await self._store.append_event(
|
|
304
|
+
control_event(
|
|
305
|
+
session_id=command.session_id,
|
|
306
|
+
event_type="control.command_rejected",
|
|
307
|
+
payload={
|
|
308
|
+
"command_id": str(command.command_id),
|
|
309
|
+
"status": "rejected",
|
|
310
|
+
"reason": error.code,
|
|
311
|
+
},
|
|
312
|
+
),
|
|
313
|
+
expected_fence=fence,
|
|
314
|
+
agent_instance_id=command.agent_instance_id,
|
|
315
|
+
)
|
|
316
|
+
await self._store.discard_claim(message_id, expected_fence=fence)
|
|
317
|
+
return WorkResult(outcome="completed", message_id=message_id)
|
|
318
|
+
if error.retryable:
|
|
319
|
+
return WorkResult(outcome="retryable_failure", message_id=message_id)
|
|
320
|
+
return WorkResult(outcome="terminal_failure", message_id=message_id)
|
|
321
|
+
except Exception:
|
|
322
|
+
# 未知异常绝不 ack 为成功:消息保持 claimed。
|
|
323
|
+
return WorkResult(outcome="terminal_failure", message_id=message_id)
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
await self._store.complete_claim(message_id, expected_fence=fence)
|
|
327
|
+
except StaleFenceError:
|
|
328
|
+
return WorkResult(outcome="terminal_failure", message_id=message_id)
|
|
329
|
+
return WorkResult(outcome="completed", message_id=message_id, run_id=run_id)
|
|
330
|
+
|
|
331
|
+
async def _message_id_for(self, command: AgentControlCommand) -> str:
|
|
332
|
+
message = await self._store.load_by_idempotency(command.session_id, command.idempotency_key)
|
|
333
|
+
assert message is not None # claim 刚发生
|
|
334
|
+
return message.message_id
|
|
335
|
+
|
|
336
|
+
async def _dispatch(
|
|
337
|
+
self, command: AgentControlCommand, activation: ActivationLease
|
|
338
|
+
) -> str | None:
|
|
339
|
+
handler = COMMAND_HANDLERS[command.command_type]
|
|
340
|
+
if handler == "start":
|
|
341
|
+
return await self._start_run(command, activation)
|
|
342
|
+
if handler == "submit_interaction":
|
|
343
|
+
return await self._control_active_run(command, activation)
|
|
344
|
+
return await self._control_active_run(command, activation)
|
|
345
|
+
|
|
346
|
+
# enqueue -> adapter.start,仅在没有 active Run 时到达这里。
|
|
347
|
+
async def _start_run(self, command: AgentControlCommand, activation: ActivationLease) -> str:
|
|
348
|
+
from ksadk.runtime.executor import handle_digest
|
|
349
|
+
|
|
350
|
+
fence = activation.fencing_token
|
|
351
|
+
guard = ActivationWriteGuard(activation_id=activation.activation_id, fencing_token=fence)
|
|
352
|
+
run_id = new_message_id()
|
|
353
|
+
adapter = self._adapter_factory()
|
|
354
|
+
pending = RunRecord(
|
|
355
|
+
run_id=run_id,
|
|
356
|
+
agent_instance_id=command.agent_instance_id,
|
|
357
|
+
session_id=command.session_id,
|
|
358
|
+
state=RunState.PENDING,
|
|
359
|
+
)
|
|
360
|
+
created = await self._store.save_run_transition(pending, expected_fence=fence)
|
|
361
|
+
continuation_metadata = await self._session_continuation_metadata(command.session_id)
|
|
362
|
+
defaults = self._start_request_defaults
|
|
363
|
+
runtime_options = command.payload.get("runtime_options")
|
|
364
|
+
if not isinstance(runtime_options, Mapping):
|
|
365
|
+
runtime_options = {}
|
|
366
|
+
default_model = str(defaults["model"]) if defaults.get("model") is not None else None
|
|
367
|
+
requested_model = str(runtime_options.get("model") or "").strip()
|
|
368
|
+
allowed_models = {
|
|
369
|
+
str(item).strip()
|
|
370
|
+
for item in (defaults.get("allowed_models") or [])
|
|
371
|
+
if str(item).strip()
|
|
372
|
+
}
|
|
373
|
+
# 显式 allowed_models 是收紧边界(不在名单的请求回落默认);
|
|
374
|
+
# 未声明名单 = 未设限制,run 级 model 覆盖直接生效(RunAgent Model 透传)。
|
|
375
|
+
selected_model = (
|
|
376
|
+
requested_model
|
|
377
|
+
if requested_model and (not allowed_models or requested_model in allowed_models)
|
|
378
|
+
else default_model
|
|
379
|
+
)
|
|
380
|
+
request_config = dict(defaults.get("config") or {})
|
|
381
|
+
approval_mode = str(runtime_options.get("tool_approval_mode") or "").strip().lower()
|
|
382
|
+
approval_overrides = {
|
|
383
|
+
"ask": "manual",
|
|
384
|
+
"risk": "auto_review",
|
|
385
|
+
}
|
|
386
|
+
if approval_mode in approval_overrides:
|
|
387
|
+
request_config["approval_mode"] = approval_overrides[approval_mode]
|
|
388
|
+
handle = await adapter.start(
|
|
389
|
+
StartRequest(
|
|
390
|
+
input=command.payload.get("content"),
|
|
391
|
+
user_id=str(command.tenant_id or "agent-kernel"),
|
|
392
|
+
session_id=command.session_id,
|
|
393
|
+
agent_id=str(defaults.get("agent_id") or command.agent_instance_id),
|
|
394
|
+
model=selected_model,
|
|
395
|
+
config=request_config,
|
|
396
|
+
# durable run_id 优先传给 adapter;adapter 不认时以
|
|
397
|
+
# runtime_run_id 映射显式记录两个 ID 的对应关系。
|
|
398
|
+
metadata={
|
|
399
|
+
"command_id": str(command.command_id),
|
|
400
|
+
"run_id": run_id,
|
|
401
|
+
**continuation_metadata,
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
running_update: dict = {
|
|
406
|
+
"state": RunState.RUNNING,
|
|
407
|
+
"handle": handle.model_dump(mode="json"),
|
|
408
|
+
"handle_digest": handle_digest(handle),
|
|
409
|
+
"tenant_id": command.tenant_id,
|
|
410
|
+
}
|
|
411
|
+
if handle.run_id != run_id:
|
|
412
|
+
running_update["runtime_run_id"] = handle.run_id
|
|
413
|
+
running = created.model_copy(update=running_update)
|
|
414
|
+
await self._store.save_run_transition(running, expected_fence=fence)
|
|
415
|
+
# 控制面始终用 durable RunRecord.run_id 查询;adapter 可以拒绝调用方
|
|
416
|
+
# 指定的 run id,因此绝不能以 runtime 私有 id 作为 cache key。
|
|
417
|
+
# Task 6:Activation 拥有 adapter + handle + provider。
|
|
418
|
+
execution = self.adopt_execution(
|
|
419
|
+
durable_run_id=run_id,
|
|
420
|
+
runtime_run_id=handle.run_id,
|
|
421
|
+
adapter=adapter,
|
|
422
|
+
handle=handle,
|
|
423
|
+
)
|
|
424
|
+
execution = self._start_stream(execution, running, guard)
|
|
425
|
+
# Keep the historical synchronous result for immediately exhausted
|
|
426
|
+
# streams (including deterministic test adapters), while a genuinely
|
|
427
|
+
# live stream runs in the background so it cannot block Inbox polling.
|
|
428
|
+
await asyncio.sleep(0)
|
|
429
|
+
if execution.stream_task is not None and execution.stream_task.done():
|
|
430
|
+
execution.stream_task.result()
|
|
431
|
+
return run_id
|
|
432
|
+
|
|
433
|
+
async def _session_continuation_metadata(self, session_id: str) -> dict[str, str]:
|
|
434
|
+
"""Recover the latest native thread identity for a follow-up turn.
|
|
435
|
+
|
|
436
|
+
A durable Session owns multiple terminal Runs. Starting each enqueue
|
|
437
|
+
without the previous ``thread_resume`` continuation silently creates a
|
|
438
|
+
fresh provider conversation, so the UI appears multi-turn while the
|
|
439
|
+
model has no prior context. The canonical SessionEvent log is the
|
|
440
|
+
authority for this mapping and survives worker/process replacement.
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
if self._session_events is None:
|
|
444
|
+
return {}
|
|
445
|
+
from ksadk.events.canonical import ContinuationCreated, ContinuationResumed
|
|
446
|
+
from ksadk.events.canonical_store import RuntimeEventStore
|
|
447
|
+
|
|
448
|
+
events = await RuntimeEventStore(self._session_events).list(session_id, limit=256)
|
|
449
|
+
for event in reversed(events):
|
|
450
|
+
if not isinstance(event, (ContinuationCreated, ContinuationResumed)):
|
|
451
|
+
continue
|
|
452
|
+
if event.continuation_kind != "thread_resume":
|
|
453
|
+
continue
|
|
454
|
+
ref = getattr(event, "ref", None)
|
|
455
|
+
thread_id = str(ref.get("thread_id") or "").strip() if isinstance(ref, dict) else ""
|
|
456
|
+
if not thread_id:
|
|
457
|
+
thread_id = str(event.source.metadata.get("thread_id") or "").strip()
|
|
458
|
+
if thread_id:
|
|
459
|
+
return {"thread_id": thread_id}
|
|
460
|
+
return {}
|
|
461
|
+
|
|
462
|
+
def _start_stream(
|
|
463
|
+
self,
|
|
464
|
+
execution: ActiveExecution,
|
|
465
|
+
run: RunRecord,
|
|
466
|
+
guard: ActivationWriteGuard,
|
|
467
|
+
) -> ActiveExecution:
|
|
468
|
+
"""Start one non-blocking stream task owned by the current activation."""
|
|
469
|
+
|
|
470
|
+
current = self._executions.get(execution.durable_run_id)
|
|
471
|
+
if current is not None and current.stream_task is not None:
|
|
472
|
+
if not current.stream_task.done():
|
|
473
|
+
return current
|
|
474
|
+
execution = replace(current, stream_task=None, stream_guard=None)
|
|
475
|
+
task = asyncio.create_task(self._consume_stream(execution, run, guard))
|
|
476
|
+
updated = replace(execution, stream_task=task, stream_guard=guard)
|
|
477
|
+
self._executions[updated.durable_run_id] = updated
|
|
478
|
+
task.add_done_callback(
|
|
479
|
+
lambda done, run_id=updated.durable_run_id: self._observe_stream_task(run_id, done)
|
|
480
|
+
)
|
|
481
|
+
return updated
|
|
482
|
+
|
|
483
|
+
def _observe_stream_task(self, run_id: str, task: asyncio.Task[None]) -> None:
|
|
484
|
+
if task.cancelled():
|
|
485
|
+
return
|
|
486
|
+
try:
|
|
487
|
+
error = task.exception()
|
|
488
|
+
except asyncio.CancelledError: # pragma: no cover - defensive
|
|
489
|
+
return
|
|
490
|
+
if error is not None:
|
|
491
|
+
logger.error(
|
|
492
|
+
"background stream for run %s failed: %s: %s",
|
|
493
|
+
run_id,
|
|
494
|
+
type(error).__name__,
|
|
495
|
+
error,
|
|
496
|
+
)
|
|
497
|
+
self._background_stream_errors[run_id] = error
|
|
498
|
+
# A background stream has already left the Inbox claim path. Do
|
|
499
|
+
# not turn its failure into an invisible hung UI; keep the full
|
|
500
|
+
# traceback in workload logs while recovery turns the durable run
|
|
501
|
+
# into a terminal fact.
|
|
502
|
+
logger.exception(
|
|
503
|
+
"agent-kernel runtime stream failed for durable run %s (details=%s)",
|
|
504
|
+
run_id,
|
|
505
|
+
getattr(error, "details", {}),
|
|
506
|
+
exc_info=error,
|
|
507
|
+
)
|
|
508
|
+
# The failure happened after the Inbox claim had already been
|
|
509
|
+
# acknowledged, so no foreground owner remains to close this
|
|
510
|
+
# adapter. Leaving it in the live table leaks provider processes
|
|
511
|
+
# (notably one Codex app-server per failed turn) and makes later
|
|
512
|
+
# sessions stall behind stale transports. Preserve the durable
|
|
513
|
+
# open Run for recovery, but release this failed process-local
|
|
514
|
+
# attachment immediately.
|
|
515
|
+
execution = self._executions.get(run_id)
|
|
516
|
+
if execution is not None and execution.stream_task is task:
|
|
517
|
+
self._executions.pop(run_id, None)
|
|
518
|
+
cleanup = asyncio.create_task(
|
|
519
|
+
self._close_failed_execution(execution),
|
|
520
|
+
name=f"kernel-stream-cleanup:{run_id}",
|
|
521
|
+
)
|
|
522
|
+
cleanup.add_done_callback(self._observe_cleanup_task)
|
|
523
|
+
|
|
524
|
+
async def _close_failed_execution(self, execution: ActiveExecution) -> None:
|
|
525
|
+
try:
|
|
526
|
+
await execution.adapter.close(execution.handle)
|
|
527
|
+
except Exception: # noqa: BLE001
|
|
528
|
+
logger.exception(
|
|
529
|
+
"failed to close adapter after runtime stream error for durable run %s",
|
|
530
|
+
execution.durable_run_id,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
@staticmethod
|
|
534
|
+
def _observe_cleanup_task(task: asyncio.Task[None]) -> None:
|
|
535
|
+
if task.cancelled():
|
|
536
|
+
return
|
|
537
|
+
try:
|
|
538
|
+
task.exception()
|
|
539
|
+
except asyncio.CancelledError: # pragma: no cover - defensive
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
async def _consume_stream(
|
|
543
|
+
self,
|
|
544
|
+
execution: ActiveExecution,
|
|
545
|
+
run: RunRecord,
|
|
546
|
+
guard: ActivationWriteGuard,
|
|
547
|
+
) -> None:
|
|
548
|
+
"""消费 run 的事件流并把每个事实落为 family=runtime/v2 事件。
|
|
549
|
+
|
|
550
|
+
事件 run_id 统一改写为 durable run_id(adapter 私有 run_id 通过
|
|
551
|
+
RunRecord.metadata.runtime_run_id 记录映射)。stream 自然结束后
|
|
552
|
+
才把 run 收口为 COMPLETED;任何异常原样上抛。
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
from ksadk.events.canonical import (
|
|
556
|
+
InteractionRequested,
|
|
557
|
+
InteractionResolved,
|
|
558
|
+
RunCompleted,
|
|
559
|
+
RunInterrupted,
|
|
560
|
+
SourceRef,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
runtime_store = None
|
|
564
|
+
if self._session_events is not None:
|
|
565
|
+
# 延迟导入:ksadk.events 反向依赖 kernel.contracts,避免模块环。
|
|
566
|
+
from ksadk.events.canonical_store import RuntimeEventStore
|
|
567
|
+
|
|
568
|
+
runtime_store = RuntimeEventStore(self._session_events, session_id=run.session_id)
|
|
569
|
+
current_run = run
|
|
570
|
+
terminal_state: RunState | None = None
|
|
571
|
+
last_source: SourceRef | None = None
|
|
572
|
+
async for event in execution.adapter.stream(execution.handle):
|
|
573
|
+
if isinstance(event, InteractionRequested):
|
|
574
|
+
current_run = await self._record_interaction_request(
|
|
575
|
+
execution, current_run, event, guard
|
|
576
|
+
)
|
|
577
|
+
# The ledger's interaction/v1 SessionEvent is the single fact
|
|
578
|
+
# for this request. Do not append a second runtime/v2 copy.
|
|
579
|
+
continue
|
|
580
|
+
if isinstance(event, InteractionResolved):
|
|
581
|
+
# The submitted command's ledger transition is first-wins and
|
|
582
|
+
# already emitted interaction.resolved; ignore framework echo.
|
|
583
|
+
continue
|
|
584
|
+
if (
|
|
585
|
+
isinstance(event, RunInterrupted)
|
|
586
|
+
and event.interaction_id
|
|
587
|
+
and current_run.state is RunState.WAITING
|
|
588
|
+
):
|
|
589
|
+
# Codex emits this immediately after InteractionRequested to
|
|
590
|
+
# describe a *temporarily blocked native turn*. The durable
|
|
591
|
+
# Kernel state for that condition is WAITING and the
|
|
592
|
+
# Interaction/v1 ledger is its authority. Treating the
|
|
593
|
+
# companion run.interrupted event as a terminal fact closes the
|
|
594
|
+
# process-local adapter before a human can answer, so the later
|
|
595
|
+
# SubmitInteraction receipt can never resolve. Do not publish
|
|
596
|
+
# a contradictory terminal runtime event; preserve the live
|
|
597
|
+
# execution until InteractionResolved resumes the same stream.
|
|
598
|
+
continue
|
|
599
|
+
if runtime_store is None:
|
|
600
|
+
continue
|
|
601
|
+
if event.run_id != current_run.run_id:
|
|
602
|
+
update: dict = {"run_id": run.run_id}
|
|
603
|
+
if getattr(event, "scope_id", None) == f"run:{execution.handle.run_id}":
|
|
604
|
+
update["scope_id"] = f"run:{run.run_id}"
|
|
605
|
+
event = event.model_copy(update=update)
|
|
606
|
+
last_source = event.source
|
|
607
|
+
await runtime_store.append(event, guard=guard)
|
|
608
|
+
terminal_state = {
|
|
609
|
+
"run.completed": RunState.COMPLETED,
|
|
610
|
+
"run.failed": RunState.FAILED,
|
|
611
|
+
"run.canceled": RunState.CANCELLED,
|
|
612
|
+
"run.interrupted": RunState.INTERRUPTED,
|
|
613
|
+
}.get(event.event_type)
|
|
614
|
+
if terminal_state is not None:
|
|
615
|
+
# App-server style providers keep their notification channel
|
|
616
|
+
# open across turns. A canonical terminal RuntimeEvent closes
|
|
617
|
+
# this run even when the transport itself does not produce
|
|
618
|
+
# EOF; waiting for EOF here leaves the durable run RUNNING and
|
|
619
|
+
# every later FIFO command queued forever.
|
|
620
|
+
break
|
|
621
|
+
|
|
622
|
+
# ``submit_interaction`` may resolve a live provider while this task is
|
|
623
|
+
# blocked in the framework stream. It transitions the durable run
|
|
624
|
+
# WAITING -> RUNNING, but ``current_run`` above is deliberately a
|
|
625
|
+
# local snapshot used to preserve event ordering. Refresh it before
|
|
626
|
+
# deciding whether natural stream exhaustion can settle the run;
|
|
627
|
+
# otherwise a Codex approval continuation finishes successfully but
|
|
628
|
+
# remains permanently WAITING because this task still sees its stale
|
|
629
|
+
# pre-response snapshot.
|
|
630
|
+
latest_run = await self._store.find_active_run(run.agent_instance_id, run.session_id)
|
|
631
|
+
if latest_run is not None and latest_run.run_id == current_run.run_id:
|
|
632
|
+
current_run = latest_run
|
|
633
|
+
|
|
634
|
+
# ``RuntimeAdapter`` is expected to emit a terminal RuntimeEvent, but
|
|
635
|
+
# a number of framework streams naturally exhaust after their last
|
|
636
|
+
# progress/item event. The Kernel is the lifecycle owner, so it must
|
|
637
|
+
# publish a fenced ``run.completed`` fact before recording COMPLETED.
|
|
638
|
+
# Otherwise foreground callers and Studio SSE wait forever even though
|
|
639
|
+
# the durable RunRecord says completion succeeded.
|
|
640
|
+
if terminal_state is None and current_run.state is not RunState.WAITING:
|
|
641
|
+
terminal_state = RunState.COMPLETED
|
|
642
|
+
if runtime_store is not None:
|
|
643
|
+
source = last_source or SourceRef(
|
|
644
|
+
framework="ksadk",
|
|
645
|
+
native_run_id=execution.runtime_run_id,
|
|
646
|
+
)
|
|
647
|
+
await runtime_store.append(
|
|
648
|
+
RunCompleted(
|
|
649
|
+
schema_version=2,
|
|
650
|
+
event_id=f"{current_run.run_id}:kernel-completed",
|
|
651
|
+
seq=0,
|
|
652
|
+
timestamp=datetime.now(timezone.utc).timestamp(),
|
|
653
|
+
run_id=current_run.run_id,
|
|
654
|
+
scope_id=f"run:{current_run.run_id}",
|
|
655
|
+
source=source,
|
|
656
|
+
status="completed",
|
|
657
|
+
output_refs=(),
|
|
658
|
+
),
|
|
659
|
+
guard=guard,
|
|
660
|
+
)
|
|
661
|
+
if terminal_state is not None:
|
|
662
|
+
current_run = await self._store.save_run_transition(
|
|
663
|
+
current_run.model_copy(update={"state": terminal_state}),
|
|
664
|
+
expected_fence=guard.fencing_token,
|
|
665
|
+
)
|
|
666
|
+
current = self._executions.get(execution.durable_run_id)
|
|
667
|
+
if (
|
|
668
|
+
terminal_state is not None
|
|
669
|
+
and current is not None
|
|
670
|
+
and current.handle == execution.handle
|
|
671
|
+
):
|
|
672
|
+
self._executions.pop(execution.durable_run_id, None)
|
|
673
|
+
if terminal_state is not None:
|
|
674
|
+
# Each enqueue owns the adapter instance created in ``_start_run``.
|
|
675
|
+
# Once its stream is terminal there is no live interaction left to
|
|
676
|
+
# preserve, so release the provider transport as part of that same
|
|
677
|
+
# lifecycle. Codex otherwise leaves one app-server child alive per
|
|
678
|
+
# turn; a later process trying to resume the persisted thread can
|
|
679
|
+
# then block behind the stale owner indefinitely.
|
|
680
|
+
try:
|
|
681
|
+
await execution.adapter.close(execution.handle)
|
|
682
|
+
except Exception: # noqa: BLE001
|
|
683
|
+
# The durable terminal event and RunRecord are already fenced
|
|
684
|
+
# and committed. A transport cleanup failure is observable but
|
|
685
|
+
# must not rewrite a successful run into a retryable command.
|
|
686
|
+
logger.exception(
|
|
687
|
+
"failed to close terminal runtime transport for durable run %s",
|
|
688
|
+
execution.durable_run_id,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
async def _record_interaction_request(
|
|
692
|
+
self,
|
|
693
|
+
execution: ActiveExecution,
|
|
694
|
+
run: RunRecord,
|
|
695
|
+
event: object,
|
|
696
|
+
guard: ActivationWriteGuard,
|
|
697
|
+
) -> RunRecord:
|
|
698
|
+
"""Persist one framework interaction as the durable ledger authority."""
|
|
699
|
+
|
|
700
|
+
from ksadk.events.canonical import ApprovalRequest, InteractionRequested
|
|
701
|
+
from ksadk.interaction.contracts import InteractionPresentation, InteractionRecord
|
|
702
|
+
|
|
703
|
+
assert isinstance(event, InteractionRequested)
|
|
704
|
+
presentation = None
|
|
705
|
+
if isinstance(event.request, ApprovalRequest):
|
|
706
|
+
request_schema = {
|
|
707
|
+
"type": "object",
|
|
708
|
+
"properties": {
|
|
709
|
+
"decision": {
|
|
710
|
+
"type": "string",
|
|
711
|
+
"enum": ["approve", "reject"],
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
"required": ["decision"],
|
|
715
|
+
}
|
|
716
|
+
native_target = {"call_id": event.request.call_id or event.interaction_id}
|
|
717
|
+
detail = event.request.detail if isinstance(event.request.detail, Mapping) else {}
|
|
718
|
+
visible_arguments = {
|
|
719
|
+
key: detail[key]
|
|
720
|
+
for key in ("command", "cwd", "reason", "grantRoot", "proposedExecpolicyAmendment")
|
|
721
|
+
if key in detail and detail[key] is not None
|
|
722
|
+
}
|
|
723
|
+
presentation = InteractionPresentation(
|
|
724
|
+
title={
|
|
725
|
+
"command_execution": "run_command",
|
|
726
|
+
"file_change": "apply_patch",
|
|
727
|
+
"permissions": "request_permission",
|
|
728
|
+
"dynamic_tool_call": "tool_call",
|
|
729
|
+
}.get(event.request.kind, event.request.kind),
|
|
730
|
+
description=json.dumps(
|
|
731
|
+
{"arguments": visible_arguments},
|
|
732
|
+
ensure_ascii=False,
|
|
733
|
+
separators=(",", ":"),
|
|
734
|
+
),
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
request_schema = dict(event.request.schema_)
|
|
738
|
+
native_target = {"call_id": event.interaction_id}
|
|
739
|
+
for key in ("checkpoint_id", "thread_id"):
|
|
740
|
+
value = execution.handle.native_ref.get(key)
|
|
741
|
+
if value is not None:
|
|
742
|
+
native_target[key] = str(value)
|
|
743
|
+
provider_id = execution.interaction_provider.provider_id or execution.handle.runtime_type
|
|
744
|
+
record = InteractionRecord(
|
|
745
|
+
interaction_id=event.interaction_id,
|
|
746
|
+
tenant_id=str(run.metadata.get("tenant_id") or ""),
|
|
747
|
+
agent_instance_id=run.agent_instance_id,
|
|
748
|
+
session_id=run.session_id,
|
|
749
|
+
run_id=run.run_id,
|
|
750
|
+
kind=event.interaction_kind,
|
|
751
|
+
request_schema=request_schema,
|
|
752
|
+
created_at=datetime.fromtimestamp(event.timestamp, timezone.utc).isoformat(),
|
|
753
|
+
presentation=presentation,
|
|
754
|
+
provider_id=provider_id,
|
|
755
|
+
native_target=native_target,
|
|
756
|
+
continuation_metadata={"runtime_run_id": execution.runtime_run_id},
|
|
757
|
+
)
|
|
758
|
+
await self._store.request(record, guard=guard) # type: ignore[attr-defined]
|
|
759
|
+
if run.state is RunState.WAITING:
|
|
760
|
+
return run
|
|
761
|
+
return await self._store.save_run_transition(
|
|
762
|
+
run.model_copy(update={"state": RunState.WAITING}),
|
|
763
|
+
expected_fence=guard.fencing_token,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
# 控制命令必须作用于 active Run 且本进程持有 live execution。
|
|
767
|
+
async def _control_active_run(
|
|
768
|
+
self, command: AgentControlCommand, activation: ActivationLease
|
|
769
|
+
) -> str | None:
|
|
770
|
+
fence = activation.fencing_token
|
|
771
|
+
active = await self._store.find_active_run(command.agent_instance_id, command.session_id)
|
|
772
|
+
if active is None:
|
|
773
|
+
raise UnsupportedControlError(
|
|
774
|
+
"runtime_no_active_run",
|
|
775
|
+
details={"command_type": command.command_type},
|
|
776
|
+
)
|
|
777
|
+
execution = self._executions.get(active.run_id)
|
|
778
|
+
if execution is None:
|
|
779
|
+
raise UnsupportedControlError(
|
|
780
|
+
"runtime_not_attached",
|
|
781
|
+
details={"run_id": active.run_id},
|
|
782
|
+
)
|
|
783
|
+
# Task 6:控制命令作用在 activation 拥有的同一 adapter/handle 上,
|
|
784
|
+
# 绝不新建 adapter(那会把命令打到没有 live 状态的实例上)。
|
|
785
|
+
adapter = execution.adapter
|
|
786
|
+
handle = execution.handle
|
|
787
|
+
verb = COMMAND_HANDLERS[command.command_type]
|
|
788
|
+
|
|
789
|
+
if verb == "cancel":
|
|
790
|
+
result = await adapter.cancel(handle)
|
|
791
|
+
if result == CancelResult.INTERRUPTED_ACTIVE_TURN:
|
|
792
|
+
await self._transition_run(active, RunState.CANCELLED, fence)
|
|
793
|
+
self._executions.pop(active.run_id, None)
|
|
794
|
+
elif verb == "pause":
|
|
795
|
+
result = await adapter.pause(handle)
|
|
796
|
+
if result == PauseResult.PAUSED_ACTIVE_TURN:
|
|
797
|
+
await self._transition_run(active, RunState.PAUSED, fence)
|
|
798
|
+
elif verb == "resume":
|
|
799
|
+
target_dict = dict(command.payload.get("target") or {})
|
|
800
|
+
target = AdapterResumeTarget(
|
|
801
|
+
kind=RESUME_TARGET_KINDS[target_dict["kind"]],
|
|
802
|
+
id=str(target_dict["id"]),
|
|
803
|
+
)
|
|
804
|
+
resumed = await adapter.resume(handle, target, AdapterResumePayload(kind="free_text"))
|
|
805
|
+
execution = self._replace_handle(execution, resumed)
|
|
806
|
+
self._start_stream(
|
|
807
|
+
execution,
|
|
808
|
+
active,
|
|
809
|
+
ActivationWriteGuard(
|
|
810
|
+
activation_id=activation.activation_id,
|
|
811
|
+
fencing_token=fence,
|
|
812
|
+
),
|
|
813
|
+
)
|
|
814
|
+
elif verb == "submit_interaction":
|
|
815
|
+
await self._submit_interaction(command, activation, active, execution)
|
|
816
|
+
elif verb == "steer":
|
|
817
|
+
await adapter.steer(handle, ContractSteerPayload.model_validate(dict(command.payload)))
|
|
818
|
+
elif verb == "inject":
|
|
819
|
+
await adapter.inject(
|
|
820
|
+
handle, ContractInjectPayload.model_validate(dict(command.payload))
|
|
821
|
+
)
|
|
822
|
+
else: # pragma: no cover - mapping 冻结
|
|
823
|
+
raise UnsupportedControlError(f"unknown handler {verb!r}")
|
|
824
|
+
return active.run_id
|
|
825
|
+
|
|
826
|
+
# ------------------------------------------------- Task 6: interaction 回包
|
|
827
|
+
|
|
828
|
+
async def _submit_interaction(
|
|
829
|
+
self,
|
|
830
|
+
command: AgentControlCommand,
|
|
831
|
+
activation: ActivationLease,
|
|
832
|
+
active: RunRecord,
|
|
833
|
+
execution: ActiveExecution,
|
|
834
|
+
) -> None:
|
|
835
|
+
"""权威 record -> 绑定 provider -> provider 接受后才写 resolved。
|
|
836
|
+
|
|
837
|
+
顺序是合同:provider 拒绝(含 unavailable)时 Interaction 保持
|
|
838
|
+
pending,绝不提前标 resolved;ledger ``resolve`` 本身做 revision CAS
|
|
839
|
+
first-wins。
|
|
840
|
+
"""
|
|
841
|
+
|
|
842
|
+
fence = activation.fencing_token
|
|
843
|
+
payload = SubmitInteractionPayload.model_validate(dict(command.payload))
|
|
844
|
+
record = await self._store.get( # type: ignore[attr-defined]
|
|
845
|
+
payload.interaction_id,
|
|
846
|
+
tenant_id=command.tenant_id,
|
|
847
|
+
agent_instance_id=command.agent_instance_id,
|
|
848
|
+
session_id=command.session_id,
|
|
849
|
+
run_id=active.run_id,
|
|
850
|
+
)
|
|
851
|
+
if record is None:
|
|
852
|
+
raise InvalidCommandError(
|
|
853
|
+
f"unknown interaction_id {payload.interaction_id!r}",
|
|
854
|
+
details={"interaction_id": payload.interaction_id},
|
|
855
|
+
)
|
|
856
|
+
if record.run_id != active.run_id:
|
|
857
|
+
raise InvalidCommandError(
|
|
858
|
+
"interaction does not belong to the active run",
|
|
859
|
+
details={
|
|
860
|
+
"interaction_id": record.interaction_id,
|
|
861
|
+
"interaction_run_id": record.run_id,
|
|
862
|
+
"active_run_id": active.run_id,
|
|
863
|
+
},
|
|
864
|
+
)
|
|
865
|
+
# ``ActiveExecution`` owns the adapter, live handle *and* provider for
|
|
866
|
+
# this activation. The durable record tells us what was requested,
|
|
867
|
+
# but it must not redirect a response into another framework provider:
|
|
868
|
+
# e.g. calling LangGraph checkpoint resume with a Codex live handle
|
|
869
|
+
# would acknowledge a response that can never reach the original run.
|
|
870
|
+
provider = execution.interaction_provider
|
|
871
|
+
if (
|
|
872
|
+
provider.provider_id != record.provider_id
|
|
873
|
+
or provider.mode == "unavailable"
|
|
874
|
+
):
|
|
875
|
+
raise AgentKernelError(
|
|
876
|
+
RUNTIME_INTERACTION_UNAVAILABLE,
|
|
877
|
+
f"interaction provider {record.provider_id!r} cannot deliver "
|
|
878
|
+
"the response through the active execution's native framework "
|
|
879
|
+
"identity",
|
|
880
|
+
retryable=False,
|
|
881
|
+
details={
|
|
882
|
+
"provider_id": record.provider_id,
|
|
883
|
+
"active_provider_id": provider.provider_id,
|
|
884
|
+
"mode": provider.mode,
|
|
885
|
+
"interaction_id": record.interaction_id,
|
|
886
|
+
},
|
|
887
|
+
)
|
|
888
|
+
submission = InteractionSubmission(
|
|
889
|
+
interaction_id=record.interaction_id,
|
|
890
|
+
expected_revision=int(
|
|
891
|
+
payload.expected_revision
|
|
892
|
+
if payload.expected_revision is not None
|
|
893
|
+
else record.revision
|
|
894
|
+
),
|
|
895
|
+
action=payload.action or "submit", # type: ignore[arg-type]
|
|
896
|
+
response=payload.response,
|
|
897
|
+
idempotency_key=payload.idempotency_key or command.idempotency_key,
|
|
898
|
+
)
|
|
899
|
+
context = InteractionResolveContext(
|
|
900
|
+
adapter=execution.adapter,
|
|
901
|
+
handle=execution.handle,
|
|
902
|
+
activation_id=activation.activation_id,
|
|
903
|
+
fencing_token=fence,
|
|
904
|
+
)
|
|
905
|
+
# provider 接受(typed 异常原样上抛 -> command_rejected,不标 resolved)。
|
|
906
|
+
resumed = await provider.resolve(context, record, submission)
|
|
907
|
+
execution = self._replace_handle(execution, resumed)
|
|
908
|
+
# provider 已接受,才在 ledger 收口 InteractionResolved(同一 fence)。
|
|
909
|
+
await self._store.resolve( # type: ignore[attr-defined]
|
|
910
|
+
submission,
|
|
911
|
+
guard=ActivationWriteGuard(activation_id=activation.activation_id, fencing_token=fence),
|
|
912
|
+
)
|
|
913
|
+
# A durable response returns a waiting run to execution. The old live
|
|
914
|
+
# stream usually remains open (Codex); checkpoint providers normally
|
|
915
|
+
# returned a fresh handle and need a new background stream. RUNNING is
|
|
916
|
+
# already the active-execution state (RUNNING -> RUNNING is not a legal
|
|
917
|
+
# transition), so only WAITING/PAUSED runs move back to RUNNING.
|
|
918
|
+
resumed_run = active
|
|
919
|
+
if active.state != RunState.RUNNING:
|
|
920
|
+
resumed_run = await self._transition_run(active, RunState.RUNNING, fence)
|
|
921
|
+
task = execution.stream_task
|
|
922
|
+
if task is None or task.done():
|
|
923
|
+
self._start_stream(
|
|
924
|
+
execution,
|
|
925
|
+
resumed_run,
|
|
926
|
+
ActivationWriteGuard(
|
|
927
|
+
activation_id=activation.activation_id,
|
|
928
|
+
fencing_token=fence,
|
|
929
|
+
),
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
def _replace_handle(self, execution: ActiveExecution, handle: RunHandle) -> ActiveExecution:
|
|
933
|
+
if handle is execution.handle or handle == execution.handle:
|
|
934
|
+
return execution
|
|
935
|
+
if execution.stream_task is not None and not execution.stream_task.done():
|
|
936
|
+
execution.stream_task.cancel()
|
|
937
|
+
updated = replace(
|
|
938
|
+
execution,
|
|
939
|
+
handle=handle,
|
|
940
|
+
runtime_run_id=handle.run_id,
|
|
941
|
+
stream_task=None,
|
|
942
|
+
stream_guard=None,
|
|
943
|
+
)
|
|
944
|
+
self._executions[execution.durable_run_id] = updated
|
|
945
|
+
return updated
|
|
946
|
+
|
|
947
|
+
async def _transition_run(self, run: RunRecord, state: RunState, fence: int) -> RunRecord:
|
|
948
|
+
return await self._store.save_run_transition(
|
|
949
|
+
run.model_copy(update={"state": state}), expected_fence=fence
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
__all__ = ["ActiveExecution", "AgentKernelWorker", "WorkResult", "WorkOutcome"]
|