agentengine-sdk-python 0.8.2__py3-none-any.whl → 0.8.3__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.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/METADATA +18 -25
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +224 -169
- ksadk/_build_provenance.json +1 -0
- ksadk/agui/_agent_helpers.py +4 -0
- ksadk/api/client.py +3 -0
- ksadk/builders/managed_runtime_builder.py +3 -0
- ksadk/cli/__init__.py +6 -0
- ksadk/cli/cmd_plugin.py +657 -0
- ksadk/codex/runtime.py +50 -8
- ksadk/configs/env_registry.py +31 -3
- ksadk/configs/settings.py +9 -0
- ksadk/context_engine/contributors.py +73 -8
- ksadk/conversations/__init__.py +19 -0
- ksadk/conversations/context.py +104 -24
- ksadk/conversations/contracts.py +266 -0
- ksadk/conversations/message_projection.py +19 -1
- ksadk/conversations/projector.py +616 -0
- ksadk/conversations/reducer.py +98 -0
- ksadk/events/_v1_compat/projection.py +2 -0
- ksadk/events/adapters/codex.py +190 -0
- ksadk/harness/__init__.py +45 -35
- ksadk/harness/runtime.py +60 -1
- ksadk/kernel/contract_fingerprints.py +5 -4
- ksadk/kernel/contracts.py +8 -0
- ksadk/kernel/control.py +1 -0
- ksadk/kernel/ingress.py +21 -0
- ksadk/kernel/memory_store.py +1 -0
- ksadk/kernel/postgres_store.py +1 -0
- ksadk/kernel/sqlite_store.py +1 -0
- ksadk/kernel/worker.py +11 -0
- ksadk/memory/providers/local_sqlite.py +7 -0
- ksadk/plugins/__init__.py +167 -0
- ksadk/plugins/bridges/__init__.py +45 -0
- ksadk/plugins/bridges/codex.py +552 -0
- ksadk/plugins/bridges/dsh.py +1116 -0
- ksadk/plugins/builtins.py +747 -0
- ksadk/plugins/bundle.py +246 -0
- ksadk/plugins/bundle_security.py +167 -0
- ksadk/plugins/composition.py +529 -0
- ksadk/plugins/context_contributor.py +437 -0
- ksadk/plugins/contracts.py +451 -0
- ksadk/plugins/dsh_toolchain.py +935 -0
- ksadk/plugins/ecosystem_bridge.py +1105 -0
- ksadk/plugins/ecosystem_probe.py +191 -0
- ksadk/plugins/host.py +724 -0
- ksadk/plugins/providers/__init__.py +85 -0
- ksadk/plugins/providers/bundles/ksadk-codex/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +18 -0
- ksadk/plugins/providers/bundles/ksadk-codex/package.json +19 -0
- ksadk/plugins/providers/bundles/ksadk-harness/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +19 -0
- ksadk/plugins/providers/bundles/ksadk-harness/package.json +19 -0
- ksadk/plugins/providers/codex.py +722 -0
- ksadk/plugins/providers/codex_dsh.py +83 -0
- ksadk/plugins/providers/dsh.py +951 -0
- ksadk/plugins/providers/dsh_descriptor_host.py +226 -0
- ksadk/plugins/providers/harness.py +627 -0
- ksadk/plugins/providers/harness_dsh.py +91 -0
- ksadk/plugins/providers/legacy.py +215 -0
- ksadk/plugins/providers/legacy_catalog.py +100 -0
- ksadk/plugins/providers/shipped_dsh.py +370 -0
- ksadk/plugins/resolver.py +331 -0
- ksadk/plugins/subagent_providers/__init__.py +12 -0
- ksadk/plugins/subagent_providers/codex.py +443 -0
- ksadk/plugins/subagents.py +333 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +51 -42
- ksadk/runtime/adapter.py +1 -0
- ksadk/runtime/factory.py +76 -2
- ksadk/runtime/framework_adapters.py +18 -0
- ksadk/runtime/runner_adapter.py +5 -0
- ksadk/sandbox/__init__.py +2 -1
- ksadk/sandbox/factory.py +72 -0
- ksadk/scheduler/__init__.py +18 -0
- ksadk/scheduler/calendar.py +102 -0
- ksadk/scheduler/contracts.py +254 -0
- ksadk/scheduler/dispatcher.py +129 -0
- ksadk/scheduler/engine.py +416 -0
- ksadk/scheduler/sqlite_store.py +563 -0
- ksadk/server/static/assets/{ArtifactsPanel-DSU1sWD_.js → ArtifactsPanel-10GPXIJ6.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-DOH6MVcn.js → CodeBlock-BXk1l-G1.js} +1 -1
- ksadk/server/static/assets/{MathMessageMarkdown-BL8my1R2.js → MathMessageMarkdown-D9uB55Oh.js} +1 -1
- ksadk/server/static/assets/{MermaidBlock-Dz4IP-Tx.js → MermaidBlock--OEYoXIJ.js} +6 -6
- ksadk/server/static/assets/{NativeTerminalPanel-DUrK0JpZ.js → NativeTerminalPanel-DrEe1syq.js} +1 -1
- ksadk/server/static/assets/{abnfDiagram-VCTEODGH-g20pFzNV.js → abnfDiagram-VCTEODGH-eeTFJLvo.js} +1 -1
- ksadk/server/static/assets/{arc-C4FzinUA.js → arc-S3ZNbIAd.js} +1 -1
- ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-DpIqL5h4.js → architectureDiagram-5GKGNRK7-CRjgEJDX.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-NRAW4CY4-BjkljTNz.js → blockDiagram-NRAW4CY4-C7bsBoNH.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-Dk_ieq2X.js → c4Diagram-UCG6FXSJ-BX3c33Ob.js} +1 -1
- ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
- ksadk/server/static/assets/{chunk-5VM5RSS4-BVHAchFb.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
- ksadk/server/static/assets/{chunk-F27PBJKO-C-ipQzuS.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
- ksadk/server/static/assets/{chunk-G27WJ6UU-C2EyJbIK.js → chunk-G27WJ6UU-CZwF3oUP.js} +1 -1
- ksadk/server/static/assets/{chunk-JWPE2WC7-vYvVJb_M.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
- ksadk/server/static/assets/{chunk-LCL6LL3I-BcI6ysPT.js → chunk-LCL6LL3I-CnorGV5q.js} +1 -1
- ksadk/server/static/assets/{chunk-POPQ4Y6H-C030x_Z1.js → chunk-POPQ4Y6H-CexntQA-.js} +1 -1
- ksadk/server/static/assets/{chunk-RHFEMEQ7-yyGpsz4n.js → chunk-RHFEMEQ7-CHTC7UxA.js} +1 -1
- ksadk/server/static/assets/{chunk-SVP7TREG-BLTlmMU7.js → chunk-SVP7TREG-BC1EWt_-.js} +1 -1
- ksadk/server/static/assets/{chunk-XXDRQBXY-C_32ArgP.js → chunk-XXDRQBXY-BTG24xN0.js} +1 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Dd0nucl9.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-JH36ORCC-9YQYwdl0.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
- ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-ErLF5N13.js → cynefinDiagram-5FMLGOSQ-DEOCBe15.js} +1 -1
- ksadk/server/static/assets/{dagre-3AP2YEHR-DFiQF-6f.js → dagre-3AP2YEHR-DBThPuLa.js} +1 -1
- ksadk/server/static/assets/{dagre-BuhGnRI1.js → dagre-ClTPrTht.js} +1 -1
- ksadk/server/static/assets/{diagram-S7CK7UJ4-Bt1v8-GC.js → diagram-S7CK7UJ4-DsZHrnpJ.js} +1 -1
- ksadk/server/static/assets/{diagram-UQ7AKVKN-DSCxJdBK.js → diagram-UQ7AKVKN-BJR4nhVr.js} +1 -1
- ksadk/server/static/assets/{diagram-VSXAHHWV-DHfYwp_9.js → diagram-VSXAHHWV-xzcERX0a.js} +1 -1
- ksadk/server/static/assets/{diagram-VX7I27RA-DdzD-4Le.js → diagram-VX7I27RA-tn2xM1xu.js} +1 -1
- ksadk/server/static/assets/{diagram-Z3DM3KII-Du-nAJ9F.js → diagram-Z3DM3KII-BZPGBkw5.js} +1 -1
- ksadk/server/static/assets/{dist-ClxGviKw.js → dist-4zN1_tq5.js} +1 -1
- ksadk/server/static/assets/{dist-BE7bu-DL.js → dist-8VWI-2bZ.js} +1 -1
- ksadk/server/static/assets/{dist-BX8z72IC.js → dist-B-46q3Hk.js} +1 -1
- ksadk/server/static/assets/{dist-BjU6y-A2.js → dist-B0gdk4rl.js} +1 -1
- ksadk/server/static/assets/{dist-C_wsv-Qd.js → dist-B1oWRmrH.js} +1 -1
- ksadk/server/static/assets/{dist-ngtN0DJB.js → dist-BI1qQH4n.js} +1 -1
- ksadk/server/static/assets/{dist-dydu68Fl.js → dist-BJLUwH6W.js} +1 -1
- ksadk/server/static/assets/{dist-Dq9gLD7L.js → dist-Bbwqa8TG.js} +1 -1
- ksadk/server/static/assets/{dist-CttYUqzS.js → dist-Btyo4vX4.js} +1 -1
- ksadk/server/static/assets/{dist-CXYYBw3_.js → dist-C71CsujO.js} +1 -1
- ksadk/server/static/assets/{dist-dDdADKFA.js → dist-CXpH_Rdd.js} +1 -1
- ksadk/server/static/assets/{dist-CSh_DE14.js → dist-Cq4OL6nO.js} +1 -1
- ksadk/server/static/assets/{dist-8hqcx0sU.js → dist-D1V3f1B4.js} +1 -1
- ksadk/server/static/assets/{dist-BVDffZ0U.js → dist-DJmCPRVB.js} +1 -1
- ksadk/server/static/assets/{dist-DnfXs8Vn.js → dist-DQAB0qn_.js} +1 -1
- ksadk/server/static/assets/{dist-DNp6rLRA.js → dist-DUwKa_l6.js} +1 -1
- ksadk/server/static/assets/{dist-B7seoj3d.js → dist-DdRBIceA.js} +1 -1
- ksadk/server/static/assets/{dist-BytlxIDT.js → dist-DfGFrb0v.js} +1 -1
- ksadk/server/static/assets/{dist-Bf2M8m3N.js → dist-DjhwGUgZ.js} +1 -1
- ksadk/server/static/assets/{dist-W-TIVntx.js → dist-DyqV4fL4.js} +1 -1
- ksadk/server/static/assets/{dist-DUX-id_F.js → dist-M67Hwu0n.js} +1 -1
- ksadk/server/static/assets/{dist-DmldrHd_.js → dist-u8tVH5Ak.js} +1 -1
- ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Da9C9NO1.js → ebnfDiagram-PWID7BFC-DVfMsLge.js} +1 -1
- ksadk/server/static/assets/{erDiagram-SSCWMZ5O-EAGqqR79.js → erDiagram-SSCWMZ5O-CWEZSYaw.js} +1 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-Dr5okd8V.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-VEO-cVcO.js +1 -0
- ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-DqJsKb59.js → ganttDiagram-EL5Y4UJY-TWIxoyYL.js} +1 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-B6g4dtDi.js → gitGraphDiagram-WWUBYQGX-DDh2hd2S.js} +1 -1
- ksadk/server/static/assets/index-B2k_urY8.js +241 -0
- ksadk/server/static/assets/index-ILPt6IWF.css +2 -0
- ksadk/server/static/assets/info-A6RAGUB7-ubA4Nm_p.js +1 -0
- ksadk/server/static/assets/{infoDiagram-RXCK75RN-Djm_nbd5.js → infoDiagram-RXCK75RN-CgoyWiDR.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-DWya9SG2.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-EYS64GPL-DutxhSNI.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-3QL26DDD-DJdOF8Fm.js → kanban-definition-3QL26DDD-CNP3ui66.js} +1 -1
- ksadk/server/static/assets/{katex-vFWytM5c.js → katex-DWEzQy1d.js} +1 -1
- ksadk/server/static/assets/{linear-BHjG8b-J.js → linear-C-qHp7d2.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-KGSy4jWT.js → mermaid-parser.core-Cl-K943T.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-9-HC88D4.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +1 -1
- ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
- ksadk/server/static/assets/{pegDiagram-XKGWAZYB-DrEZd4fD.js → pegDiagram-XKGWAZYB-BQVc9ia_.js} +1 -1
- ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
- ksadk/server/static/assets/{pieDiagram-E7YTZNPT-DpjqOFFp.js → pieDiagram-E7YTZNPT-B6UCAvtY.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BiNhgOXX.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +1 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-l5noef9X.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-Baea6ZvM.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-OKn8zZnU.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-DdKAKC4l.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-Bds8udlG.js +1 -0
- ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-_Y3Ojg6G.js → railroadDiagram-O6MQD6OU-Dt3FFDmK.js} +1 -1
- ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-DeCFdg9K.js → requirementDiagram-EFPCY7ZU-Cz_vQtgF.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-CPH75rhw.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-OEpQQdr4.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +1 -1
- ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-heUErzhI.js → sizeCapture-X5ZJPWSS-DXX54iyO.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DvA3jSMB.js → stateDiagram-HBIQ2CUA-Dp_lw9II.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
- ksadk/server/static/assets/{swimlanes-XN3QIQJK-CR-dfN_t.js → swimlanes-XN3QIQJK-D8O5UlLG.js} +1 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
- ksadk/server/static/assets/{timeline-definition-24CTP7MA-DAVJf1TX.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +1 -1
- ksadk/server/static/assets/treeView-Q6P3EWNA-CK5CSrSO.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-Ch2BKgAM.js +1 -0
- ksadk/server/static/assets/{vennDiagram-4TSXK5OY-DwM8eDkh.js → vennDiagram-4TSXK5OY-EnNl0DE7.js} +1 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-DZL-Zz2t.js → wardleyDiagram-VM6X3IG4-DBu5fbYr.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js → xychartDiagram-S5SC5T6Z-BSVXs4IY.js} +1 -1
- ksadk/server/static/index.html +2 -2
- ksadk/skills/service_client.py +48 -0
- ksadk/studio/agent_lifecycle.py +26 -20
- ksadk/studio/api.py +513 -71
- ksadk/studio/api_catalog_routes.py +5 -0
- ksadk/studio/api_contracts.py +34 -1
- ksadk/studio/api_plugin_routes.py +480 -0
- ksadk/studio/attachment_store.py +187 -0
- ksadk/studio/authoring.py +10 -4
- ksadk/studio/authoring_coordinator.py +12 -2
- ksadk/studio/builder.py +123 -43
- ksadk/studio/cloud.py +4 -0
- ksadk/studio/codex_agent_service.py +12 -1
- ksadk/studio/codex_builder.py +54 -15
- ksadk/studio/codex_manifest.py +21 -1
- ksadk/studio/codex_run.py +16 -4
- ksadk/studio/compatibility_report.py +445 -0
- ksadk/studio/compiler.py +6 -1
- ksadk/studio/contracts.py +123 -4
- ksadk/studio/dsh_provider_registration.py +768 -0
- ksadk/studio/event_store.py +54 -6
- ksadk/studio/framework_run.py +37 -4
- ksadk/studio/hosted_kernel.py +1 -1
- ksadk/studio/operations.py +12 -2
- ksadk/studio/plugin_composition.py +301 -0
- ksadk/studio/plugin_kernel_adapter.py +128 -0
- ksadk/studio/plugin_runtime.py +652 -0
- ksadk/studio/resource_catalog.py +6 -1
- ksadk/studio/run_service.py +713 -75
- ksadk/studio/runtime_source.py +9 -1
- ksadk/studio/scheduler_runtime.py +374 -0
- ksadk/studio/scheduler_service.py +211 -0
- ksadk/studio/service.py +672 -82
- ksadk/studio/shared_web.py +167 -28
- ksadk/studio/soul.py +52 -0
- ksadk/studio/static/assets/index-DXs1Eq4M.css +1 -0
- ksadk/studio/static/assets/index-DeDqwTsD.js +283 -0
- ksadk/studio/static/index.html +2 -2
- ksadk/studio/validator.py +1 -1
- ksadk/studio/workspace.py +1 -0
- ksadk/version.py +1 -1
- ksadk_runtime_common/workspace_files/path_utils.py +41 -0
- ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +0 -1
- ksadk/server/static/assets/channel-4cQHKtx1.js +0 -1
- ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +0 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +0 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +0 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +0 -1
- ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +0 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +0 -1
- ksadk/server/static/assets/index-8ipRcQ-M.js +0 -240
- ksadk/server/static/assets/index-ByFCqjlh.css +0 -2
- ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +0 -1
- ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +0 -1
- ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +0 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +0 -1
- ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +0 -1
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +0 -1
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +0 -1
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +0 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +0 -8
- ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +0 -1
- ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +0 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +0 -1
- ksadk/studio/static/assets/index-BLpcdrgW.css +0 -1
- ksadk/studio/static/assets/index-CYekR2Xv.js +0 -260
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.2.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
ksadk/studio/run_service.py
CHANGED
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
6
8
|
import logging
|
|
7
9
|
import time
|
|
8
10
|
from collections.abc import Callable, Mapping
|
|
9
11
|
from dataclasses import asdict, dataclass, field
|
|
10
12
|
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
11
14
|
from typing import Any
|
|
12
15
|
from uuid import uuid4
|
|
13
16
|
|
|
14
17
|
from ksadk.agui.a2ui_projection import project_a2ui_operations
|
|
18
|
+
from ksadk.conversations.projector import (
|
|
19
|
+
project_conversation_item,
|
|
20
|
+
project_interaction_conversation_item,
|
|
21
|
+
)
|
|
15
22
|
from ksadk.events.canonical import (
|
|
16
23
|
ContinuationCreated,
|
|
17
24
|
ContinuationResumed,
|
|
@@ -22,6 +29,7 @@ from ksadk.events.canonical import (
|
|
|
22
29
|
ItemFailed,
|
|
23
30
|
ItemStarted,
|
|
24
31
|
ItemUpdated,
|
|
32
|
+
OutputRef,
|
|
25
33
|
RunCanceled,
|
|
26
34
|
RunCompleted,
|
|
27
35
|
RunFailed,
|
|
@@ -32,7 +40,9 @@ from ksadk.events.canonical import (
|
|
|
32
40
|
SourceRef,
|
|
33
41
|
UsageReported,
|
|
34
42
|
dump_runtime_event,
|
|
43
|
+
parse_runtime_event,
|
|
35
44
|
)
|
|
45
|
+
from ksadk.events.canonical_store import session_event_to_runtime_event
|
|
36
46
|
from ksadk.events.content import (
|
|
37
47
|
ContentSnapshot,
|
|
38
48
|
DataContent,
|
|
@@ -73,6 +83,7 @@ class StudioRunSpec:
|
|
|
73
83
|
model: str | None = None
|
|
74
84
|
request_config: Mapping[str, Any] = field(default_factory=dict)
|
|
75
85
|
manifest_sha256: str = ""
|
|
86
|
+
plugin_bundle_root: Path | None = None
|
|
76
87
|
|
|
77
88
|
|
|
78
89
|
class StudioRunService:
|
|
@@ -86,6 +97,7 @@ class StudioRunService:
|
|
|
86
97
|
event_store: RunEventStore | None = None,
|
|
87
98
|
session_service: BaseSessionService | None = None,
|
|
88
99
|
runtime_events: RuntimeEventStore | None = None,
|
|
100
|
+
plugin_runtime: Any | None = None,
|
|
89
101
|
) -> None:
|
|
90
102
|
self.workspace = workspace
|
|
91
103
|
self.executor = executor
|
|
@@ -94,10 +106,18 @@ class StudioRunService:
|
|
|
94
106
|
project_dir=str(workspace.root)
|
|
95
107
|
)
|
|
96
108
|
self.runtime_events = runtime_events or RuntimeEventStore(self.session_service)
|
|
109
|
+
self.plugin_runtime = plugin_runtime
|
|
97
110
|
self._active_handles: dict[str, Any] = {}
|
|
98
111
|
self._cancel_flags: dict[str, bool] = {}
|
|
99
|
-
self._control_queues: dict[
|
|
112
|
+
self._control_queues: dict[
|
|
113
|
+
str,
|
|
114
|
+
asyncio.Queue[tuple[str, ResumePayload | None, asyncio.Future[None] | None]],
|
|
115
|
+
] = {}
|
|
100
116
|
self._waiting_modes: dict[str, str] = {}
|
|
117
|
+
# Interaction resolution appends to one run-level event file. Serialise
|
|
118
|
+
# every decision for that run, not only identical interaction ids, so
|
|
119
|
+
# two concurrently visible cards cannot overwrite each other's receipt.
|
|
120
|
+
self._interaction_locks: dict[str, asyncio.Lock] = {}
|
|
101
121
|
|
|
102
122
|
async def recover_interrupted(self) -> None:
|
|
103
123
|
"""Settle local runs left active across a Studio restart.
|
|
@@ -173,11 +193,21 @@ class StudioRunService:
|
|
|
173
193
|
if on_event is not None:
|
|
174
194
|
on_event(created)
|
|
175
195
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if kernel_route_active():
|
|
196
|
+
kernel_runtime = self._kernel_runtime_for_spec(spec)
|
|
197
|
+
if kernel_runtime is not None:
|
|
179
198
|
return await self._kernel_run(
|
|
180
|
-
spec,
|
|
199
|
+
spec,
|
|
200
|
+
user_input,
|
|
201
|
+
record=record,
|
|
202
|
+
on_event=on_event,
|
|
203
|
+
kernel_runtime=kernel_runtime,
|
|
204
|
+
)
|
|
205
|
+
if spec.plugin_bundle_root is not None:
|
|
206
|
+
return await self._plugin_run(
|
|
207
|
+
spec,
|
|
208
|
+
user_input,
|
|
209
|
+
record=record,
|
|
210
|
+
on_event=on_event,
|
|
181
211
|
)
|
|
182
212
|
|
|
183
213
|
started = time.monotonic()
|
|
@@ -187,18 +217,32 @@ class StudioRunService:
|
|
|
187
217
|
|
|
188
218
|
async def persist(runtime_event: RuntimeEvent) -> RunEvent:
|
|
189
219
|
persisted = await self.runtime_events.append_one(record.session_id, runtime_event)
|
|
190
|
-
event_type, data = project_runtime_event(
|
|
220
|
+
event_type, data = project_runtime_event(
|
|
221
|
+
persisted,
|
|
222
|
+
session_id=record.session_id,
|
|
223
|
+
public_run_id=record.id,
|
|
224
|
+
)
|
|
225
|
+
if isinstance(persisted, InteractionRequested):
|
|
226
|
+
data["revision"] = 1
|
|
227
|
+
conversation_item = data.get("conversationItem")
|
|
228
|
+
if isinstance(conversation_item, dict):
|
|
229
|
+
item_payload = conversation_item.get("payload")
|
|
230
|
+
if isinstance(item_payload, dict):
|
|
231
|
+
item_payload["revision"] = 1
|
|
191
232
|
stored = self.event_store.append(record.id, event_type, data)
|
|
192
233
|
if on_event is not None:
|
|
193
234
|
on_event(stored)
|
|
194
235
|
return stored
|
|
195
236
|
|
|
196
237
|
handle = None
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
completed_text_by_item: dict[tuple[str, str], str] = {}
|
|
239
|
+
streamed_text_by_item: dict[tuple[str, str], str] = {}
|
|
240
|
+
terminal_output_refs: tuple[OutputRef, ...] = ()
|
|
199
241
|
runtime_duration_ms: int | None = None
|
|
200
242
|
item_phases: dict[tuple[str, str], str | None] = {}
|
|
201
|
-
control_queue: asyncio.Queue[
|
|
243
|
+
control_queue: asyncio.Queue[
|
|
244
|
+
tuple[str, ResumePayload | None, asyncio.Future[None] | None]
|
|
245
|
+
] = asyncio.Queue()
|
|
202
246
|
self._control_queues[run_id] = control_queue
|
|
203
247
|
try:
|
|
204
248
|
tool_approval_mode = str(spec.request_config.get("tool_approval_mode") or "")
|
|
@@ -250,10 +294,13 @@ class StudioRunService:
|
|
|
250
294
|
== "final_answer"
|
|
251
295
|
):
|
|
252
296
|
text = event.update.text if isinstance(event.update, TextContent) else ""
|
|
297
|
+
item_key = (event.scope_id, event.item_id)
|
|
253
298
|
if event.op == "replace":
|
|
254
|
-
|
|
299
|
+
streamed_text_by_item[item_key] = text
|
|
255
300
|
else:
|
|
256
|
-
|
|
301
|
+
streamed_text_by_item[item_key] = (
|
|
302
|
+
streamed_text_by_item.get(item_key, "") + text
|
|
303
|
+
)
|
|
257
304
|
elif (
|
|
258
305
|
isinstance(event, ItemCompleted)
|
|
259
306
|
and event.item_kind == "message"
|
|
@@ -262,10 +309,11 @@ class StudioRunService:
|
|
|
262
309
|
)
|
|
263
310
|
== "final_answer"
|
|
264
311
|
):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
312
|
+
completed_text_by_item[(event.scope_id, event.item_id)] = "".join(
|
|
313
|
+
part.text
|
|
314
|
+
for part in event.snapshot.parts
|
|
315
|
+
if isinstance(part, TextContent)
|
|
316
|
+
)
|
|
269
317
|
elif isinstance(event, UsageReported):
|
|
270
318
|
record.usage = Usage(
|
|
271
319
|
input_tokens=event.input_tokens,
|
|
@@ -318,6 +366,7 @@ class StudioRunService:
|
|
|
318
366
|
elif isinstance(event, RunCompleted):
|
|
319
367
|
terminal_seen = True
|
|
320
368
|
record.status = RunStatus.COMPLETED
|
|
369
|
+
terminal_output_refs = event.output_refs
|
|
321
370
|
raw_duration = event.source.metadata.get("duration_ms")
|
|
322
371
|
if raw_duration is None:
|
|
323
372
|
metrics = event.source.metadata.get("metrics")
|
|
@@ -331,7 +380,7 @@ class StudioRunService:
|
|
|
331
380
|
record.status = RunStatus.COMPLETED
|
|
332
381
|
break
|
|
333
382
|
|
|
334
|
-
command, resume_payload = await control_queue.get()
|
|
383
|
+
command, resume_payload, submit_ack = await control_queue.get()
|
|
335
384
|
if command == "cancel" or self._cancel_flags.get(run_id):
|
|
336
385
|
raise asyncio.CancelledError()
|
|
337
386
|
if command != "resume":
|
|
@@ -342,11 +391,18 @@ class StudioRunService:
|
|
|
342
391
|
if native_thread_id
|
|
343
392
|
else ResumeTarget(kind="invocation_id", id=handle.run_id)
|
|
344
393
|
)
|
|
345
|
-
|
|
346
|
-
handle
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
394
|
+
try:
|
|
395
|
+
handle = await self.executor.resume(
|
|
396
|
+
handle,
|
|
397
|
+
target,
|
|
398
|
+
resume_payload,
|
|
399
|
+
)
|
|
400
|
+
except Exception as exc:
|
|
401
|
+
if submit_ack is not None and not submit_ack.done():
|
|
402
|
+
submit_ack.set_exception(exc)
|
|
403
|
+
raise
|
|
404
|
+
if submit_ack is not None and not submit_ack.done():
|
|
405
|
+
submit_ack.set_result(None)
|
|
350
406
|
self._active_handles[run_id] = handle
|
|
351
407
|
record.runtime_handle = handle.model_dump(mode="json")
|
|
352
408
|
record.status = RunStatus.RUNNING
|
|
@@ -360,7 +416,17 @@ class StudioRunService:
|
|
|
360
416
|
if on_event is not None:
|
|
361
417
|
on_event(resumed)
|
|
362
418
|
self.event_store.save(record)
|
|
363
|
-
|
|
419
|
+
text_by_item = {**streamed_text_by_item, **completed_text_by_item}
|
|
420
|
+
if terminal_output_refs:
|
|
421
|
+
terminal_parts = [
|
|
422
|
+
text_by_item.get((ref.scope_id, ref.item_id), "")
|
|
423
|
+
for ref in terminal_output_refs
|
|
424
|
+
]
|
|
425
|
+
record.output = "\n\n".join(part for part in terminal_parts if part)
|
|
426
|
+
else:
|
|
427
|
+
completed_parts = [part for part in completed_text_by_item.values() if part]
|
|
428
|
+
fallback_parts = [part for part in streamed_text_by_item.values() if part]
|
|
429
|
+
record.output = "\n\n".join(completed_parts or fallback_parts)
|
|
364
430
|
except asyncio.CancelledError:
|
|
365
431
|
cancel_result = "task_cancelled"
|
|
366
432
|
if handle is not None and self.executor.is_attached(handle):
|
|
@@ -440,6 +506,229 @@ class StudioRunService:
|
|
|
440
506
|
async def _sync_trace(self, record: RunRecord) -> None:
|
|
441
507
|
self.event_store.trace_store.sync(record, await self.events(record.id))
|
|
442
508
|
|
|
509
|
+
async def _plugin_run(
|
|
510
|
+
self,
|
|
511
|
+
spec: StudioRunSpec,
|
|
512
|
+
user_input: str,
|
|
513
|
+
*,
|
|
514
|
+
record: RunRecord,
|
|
515
|
+
on_event: Callable[[RunEvent], None] | None,
|
|
516
|
+
) -> RunRecord:
|
|
517
|
+
"""Execute a composed Build without translating it into ADK/LangGraph.
|
|
518
|
+
|
|
519
|
+
PluginHost retains the activation under the Studio session id. Harness
|
|
520
|
+
providers write canonical RuntimeEvents directly; providers that only
|
|
521
|
+
implement the minimum request/result protocol receive a canonical
|
|
522
|
+
envelope here so Studio still has one durable conversation/event model.
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
started = time.monotonic()
|
|
526
|
+
record.status = RunStatus.RUNNING
|
|
527
|
+
record.started_at = datetime.now(timezone.utc)
|
|
528
|
+
record.runtime_handle = {
|
|
529
|
+
"provider": "pluginhost",
|
|
530
|
+
"bundleDigest": str(spec.request_config.get("plugin_bundle_digest") or ""),
|
|
531
|
+
"activationKey": record.session_id,
|
|
532
|
+
}
|
|
533
|
+
self.event_store.save(record)
|
|
534
|
+
rows_before = await self.session_service.get_events(record.session_id)
|
|
535
|
+
after_seq = max((int(row.seq_id or 0) for row in rows_before), default=0)
|
|
536
|
+
|
|
537
|
+
async def publish(events: list[RuntimeEvent]) -> None:
|
|
538
|
+
for runtime_event in events:
|
|
539
|
+
event_type, data = project_runtime_event(
|
|
540
|
+
runtime_event,
|
|
541
|
+
session_id=record.session_id,
|
|
542
|
+
)
|
|
543
|
+
stored = self.event_store.append(record.id, event_type, data)
|
|
544
|
+
if on_event is not None:
|
|
545
|
+
on_event(stored)
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
if self.plugin_runtime is None:
|
|
549
|
+
raise StudioError(
|
|
550
|
+
"PLUGIN_RUNTIME_UNAVAILABLE",
|
|
551
|
+
"Studio 尚未配置 PluginHost Runtime",
|
|
552
|
+
status_code=503,
|
|
553
|
+
)
|
|
554
|
+
request_metadata = {
|
|
555
|
+
key: value
|
|
556
|
+
for key, value in {
|
|
557
|
+
"tool_approval_mode": spec.request_config.get("tool_approval_mode"),
|
|
558
|
+
"collaboration_mode": spec.request_config.get("collaboration_mode"),
|
|
559
|
+
"goal_objective": spec.request_config.get("goal_objective"),
|
|
560
|
+
"reasoning_effort": spec.request_config.get("effort"),
|
|
561
|
+
}.items()
|
|
562
|
+
if value not in {None, ""}
|
|
563
|
+
}
|
|
564
|
+
result = await self.plugin_runtime.execute(
|
|
565
|
+
spec,
|
|
566
|
+
{
|
|
567
|
+
"user_id": "local-user",
|
|
568
|
+
"session_id": record.session_id,
|
|
569
|
+
"invocation_id": record.id,
|
|
570
|
+
"messages": [{"role": "user", "content": user_input}],
|
|
571
|
+
"model": spec.model,
|
|
572
|
+
"request_metadata": request_metadata,
|
|
573
|
+
},
|
|
574
|
+
session_id=record.session_id,
|
|
575
|
+
)
|
|
576
|
+
if result.session_id != record.session_id:
|
|
577
|
+
raise StudioError(
|
|
578
|
+
"PLUGIN_SESSION_MISMATCH",
|
|
579
|
+
"AgentProvider 返回了不同的 Session",
|
|
580
|
+
status_code=502,
|
|
581
|
+
details={
|
|
582
|
+
"expected": record.session_id,
|
|
583
|
+
"actual": result.session_id,
|
|
584
|
+
},
|
|
585
|
+
)
|
|
586
|
+
canonical = await self._plugin_events_after(
|
|
587
|
+
record.session_id,
|
|
588
|
+
after_seq=after_seq,
|
|
589
|
+
run_id=record.id,
|
|
590
|
+
)
|
|
591
|
+
if not canonical:
|
|
592
|
+
canonical = await self._persist_plugin_result_events(
|
|
593
|
+
record,
|
|
594
|
+
result.output_text,
|
|
595
|
+
usage=result.usage,
|
|
596
|
+
)
|
|
597
|
+
await publish(canonical)
|
|
598
|
+
record.output = result.output_text
|
|
599
|
+
record.status = RunStatus.COMPLETED
|
|
600
|
+
_apply_plugin_usage(record, result.usage)
|
|
601
|
+
except asyncio.CancelledError:
|
|
602
|
+
record.status = RunStatus.CANCELLED
|
|
603
|
+
record.error = {"code": "RUN_CANCELLED", "message": "运行已取消"}
|
|
604
|
+
cancelled = await self.runtime_events.append_one(
|
|
605
|
+
record.session_id,
|
|
606
|
+
RunCanceled(
|
|
607
|
+
**_plugin_event_envelope(record, "run.canceled"),
|
|
608
|
+
status="canceled",
|
|
609
|
+
reason="cancelled",
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
await publish([cancelled])
|
|
613
|
+
raise
|
|
614
|
+
except Exception as exc: # noqa: BLE001 - plugin boundary is typed below
|
|
615
|
+
record.status = RunStatus.FAILED
|
|
616
|
+
code = str(getattr(exc, "code", "PLUGIN_RUNTIME_FAILED"))
|
|
617
|
+
record.error = {"code": code, "message": str(exc)}
|
|
618
|
+
canonical = await self._plugin_events_after(
|
|
619
|
+
record.session_id,
|
|
620
|
+
after_seq=after_seq,
|
|
621
|
+
run_id=record.id,
|
|
622
|
+
)
|
|
623
|
+
if canonical:
|
|
624
|
+
await publish(canonical)
|
|
625
|
+
else:
|
|
626
|
+
failed = await self.runtime_events.append_one(
|
|
627
|
+
record.session_id,
|
|
628
|
+
RunFailed(
|
|
629
|
+
**_plugin_event_envelope(record, "run.failed"),
|
|
630
|
+
status="failed",
|
|
631
|
+
error=ErrorInfo(
|
|
632
|
+
code=code,
|
|
633
|
+
message=str(exc),
|
|
634
|
+
source="pluginhost",
|
|
635
|
+
scope_id=record.id,
|
|
636
|
+
),
|
|
637
|
+
),
|
|
638
|
+
)
|
|
639
|
+
await publish([failed])
|
|
640
|
+
finally:
|
|
641
|
+
record.completed_at = datetime.now(timezone.utc)
|
|
642
|
+
record.duration_ms = int((time.monotonic() - started) * 1000)
|
|
643
|
+
record.duration_source = "studio"
|
|
644
|
+
self.event_store.save(record)
|
|
645
|
+
await self._sync_trace(record)
|
|
646
|
+
return record
|
|
647
|
+
|
|
648
|
+
async def _plugin_events_after(
|
|
649
|
+
self,
|
|
650
|
+
session_id: str,
|
|
651
|
+
*,
|
|
652
|
+
after_seq: int,
|
|
653
|
+
run_id: str,
|
|
654
|
+
) -> list[RuntimeEvent]:
|
|
655
|
+
rows = await self.session_service.get_events(
|
|
656
|
+
session_id,
|
|
657
|
+
after_seq_id=after_seq,
|
|
658
|
+
)
|
|
659
|
+
events: list[RuntimeEvent] = []
|
|
660
|
+
for row in rows:
|
|
661
|
+
event = session_event_to_runtime_event(row)
|
|
662
|
+
if event is not None and event.run_id == run_id:
|
|
663
|
+
events.append(event)
|
|
664
|
+
return events
|
|
665
|
+
|
|
666
|
+
async def _persist_plugin_result_events(
|
|
667
|
+
self,
|
|
668
|
+
record: RunRecord,
|
|
669
|
+
output_text: str,
|
|
670
|
+
*,
|
|
671
|
+
usage: Mapping[str, Any],
|
|
672
|
+
) -> list[RuntimeEvent]:
|
|
673
|
+
item_id = f"{record.id}:assistant"
|
|
674
|
+
part_id = f"{record.id}:text"
|
|
675
|
+
snapshot = ContentSnapshot(
|
|
676
|
+
parts=(TextContent(part_id=part_id, text=output_text),)
|
|
677
|
+
)
|
|
678
|
+
events: list[RuntimeEvent] = [
|
|
679
|
+
RunStarted(
|
|
680
|
+
**_plugin_event_envelope(record, "run.started"),
|
|
681
|
+
status="running",
|
|
682
|
+
),
|
|
683
|
+
ItemStarted(
|
|
684
|
+
**_plugin_event_envelope(record, "item.started"),
|
|
685
|
+
item_id=item_id,
|
|
686
|
+
item_kind="message",
|
|
687
|
+
phase="final_answer",
|
|
688
|
+
),
|
|
689
|
+
ItemUpdated(
|
|
690
|
+
**_plugin_event_envelope(record, "item.updated"),
|
|
691
|
+
item_id=item_id,
|
|
692
|
+
item_kind="message",
|
|
693
|
+
op="append",
|
|
694
|
+
update=TextContent(part_id=part_id, text=output_text),
|
|
695
|
+
),
|
|
696
|
+
ItemCompleted(
|
|
697
|
+
**_plugin_event_envelope(record, "item.completed"),
|
|
698
|
+
item_id=item_id,
|
|
699
|
+
item_kind="message",
|
|
700
|
+
snapshot=snapshot,
|
|
701
|
+
),
|
|
702
|
+
]
|
|
703
|
+
normalized = _normalized_plugin_usage(usage)
|
|
704
|
+
if normalized["reported"]:
|
|
705
|
+
events.append(
|
|
706
|
+
UsageReported(
|
|
707
|
+
**_plugin_event_envelope(record, "usage.reported"),
|
|
708
|
+
input_tokens=normalized["input_tokens"],
|
|
709
|
+
output_tokens=normalized["output_tokens"],
|
|
710
|
+
total_tokens=normalized["total_tokens"],
|
|
711
|
+
cached_tokens=normalized["cached_tokens"],
|
|
712
|
+
reasoning_tokens=normalized["reasoning_tokens"],
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
events.append(
|
|
716
|
+
RunCompleted(
|
|
717
|
+
**_plugin_event_envelope(record, "run.completed"),
|
|
718
|
+
status="completed",
|
|
719
|
+
output_refs=(
|
|
720
|
+
OutputRef(
|
|
721
|
+
scope_id=record.id,
|
|
722
|
+
item_id=item_id,
|
|
723
|
+
part_id=part_id,
|
|
724
|
+
),
|
|
725
|
+
),
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
return [
|
|
729
|
+
await self.runtime_events.append_one(record.session_id, event)
|
|
730
|
+
for event in events
|
|
731
|
+
]
|
|
443
732
|
|
|
444
733
|
async def _kernel_run(
|
|
445
734
|
self,
|
|
@@ -448,6 +737,7 @@ class StudioRunService:
|
|
|
448
737
|
*,
|
|
449
738
|
record: RunRecord,
|
|
450
739
|
on_event: Callable[[RunEvent], None] | None,
|
|
740
|
+
kernel_runtime: Any,
|
|
451
741
|
) -> RunRecord:
|
|
452
742
|
"""kernel 路径(灰度 opt-in):Studio run -> AgentControlCommand -> receipt。
|
|
453
743
|
|
|
@@ -464,9 +754,15 @@ class StudioRunService:
|
|
|
464
754
|
trusted = _kernel_ingress.trusted_context(
|
|
465
755
|
source_kind="studio",
|
|
466
756
|
source_ref=record.id,
|
|
757
|
+
# The Worker polls the concrete Kernel Runtime instance, not
|
|
758
|
+
# whichever synthetic ``local-agent`` happened to be inferred
|
|
759
|
+
# from a Studio request. A Studio Build may use the Kernel
|
|
760
|
+
# path only after ``_kernel_runtime_for_spec`` established that
|
|
761
|
+
# it is this Runtime's exact Agent/Runtime binding.
|
|
762
|
+
tenant_id=str(kernel_runtime.config.tenant_id),
|
|
763
|
+
agent_instance_id=str(kernel_runtime.config.agent_instance_id),
|
|
467
764
|
session_id=record.session_id,
|
|
468
765
|
operations=("enqueue",),
|
|
469
|
-
launch_context=spec.launch_context,
|
|
470
766
|
)
|
|
471
767
|
idempotency_key = str(
|
|
472
768
|
(spec.request_config or {}).get("idempotency_key") or record.id
|
|
@@ -513,6 +809,7 @@ class StudioRunService:
|
|
|
513
809
|
self.event_store.save(record)
|
|
514
810
|
await self._sync_trace(record)
|
|
515
811
|
return record
|
|
812
|
+
|
|
516
813
|
except Exception as exc: # noqa: BLE001
|
|
517
814
|
logger.exception("Studio kernel ingress failed for run %s", record.id)
|
|
518
815
|
record.status = RunStatus.FAILED
|
|
@@ -526,12 +823,62 @@ class StudioRunService:
|
|
|
526
823
|
await self._sync_trace(record)
|
|
527
824
|
return record
|
|
528
825
|
|
|
826
|
+
@staticmethod
|
|
827
|
+
def _kernel_runtime_for_spec(spec: StudioRunSpec) -> Any | None:
|
|
828
|
+
"""Return the active Kernel Runtime only for its bound Studio Build.
|
|
829
|
+
|
|
830
|
+
An in-process AgentKernel owns one concrete RuntimeAdapter factory and
|
|
831
|
+
its immutable startup defaults. Merely observing that a Kernel is
|
|
832
|
+
enabled is therefore insufficient: sending an arbitrary Studio Build
|
|
833
|
+
through it can silently execute the default Adapter instead. When the
|
|
834
|
+
binding is not exact, the existing direct Studio path remains the
|
|
835
|
+
compatible local behaviour; it is safer than manufacturing a false
|
|
836
|
+
AgentControl/Scheduler success.
|
|
837
|
+
"""
|
|
838
|
+
|
|
839
|
+
from ksadk.kernel.bootstrap import get_agent_kernel_runtime
|
|
840
|
+
from ksadk.kernel.ingress import kernel_route_active
|
|
841
|
+
|
|
842
|
+
if not kernel_route_active():
|
|
843
|
+
return None
|
|
844
|
+
runtime = get_agent_kernel_runtime()
|
|
845
|
+
if runtime is None:
|
|
846
|
+
return None
|
|
847
|
+
active_context = getattr(runtime.config, "launch_context", None)
|
|
848
|
+
if active_context is None:
|
|
849
|
+
return None
|
|
850
|
+
if str(getattr(active_context, "runtime_type", "")).strip().lower() != (
|
|
851
|
+
spec.launch_context.runtime_type.strip().lower()
|
|
852
|
+
):
|
|
853
|
+
return None
|
|
854
|
+
try:
|
|
855
|
+
active_project = Path(active_context.project_dir).resolve()
|
|
856
|
+
build_project = Path(spec.launch_context.project_dir).resolve()
|
|
857
|
+
except (OSError, TypeError, ValueError):
|
|
858
|
+
return None
|
|
859
|
+
if active_project != build_project:
|
|
860
|
+
return None
|
|
861
|
+
defaults = getattr(runtime.config, "start_request_defaults", {}) or {}
|
|
862
|
+
bound_agent_id = str(
|
|
863
|
+
defaults.get("agent_id") or runtime.config.agent_instance_id
|
|
864
|
+
).strip()
|
|
865
|
+
if bound_agent_id != spec.agent_id:
|
|
866
|
+
return None
|
|
867
|
+
declared_instance = str(
|
|
868
|
+
(spec.launch_context.config or {}).get("agent_instance_id") or ""
|
|
869
|
+
).strip()
|
|
870
|
+
if declared_instance and declared_instance != str(
|
|
871
|
+
runtime.config.agent_instance_id
|
|
872
|
+
):
|
|
873
|
+
return None
|
|
874
|
+
return runtime
|
|
875
|
+
|
|
529
876
|
async def cancel_run(self, run_id: str) -> dict[str, str]:
|
|
530
877
|
"""Request cancellation; the flag and executor perform the actual stop."""
|
|
531
878
|
self._cancel_flags[run_id] = True
|
|
532
879
|
queue = self._control_queues.get(run_id)
|
|
533
880
|
if queue is not None:
|
|
534
|
-
queue.put_nowait(("cancel", None))
|
|
881
|
+
queue.put_nowait(("cancel", None, None))
|
|
535
882
|
handle = self._active_handles.get(run_id)
|
|
536
883
|
if handle is not None and self.executor.is_attached(handle):
|
|
537
884
|
try:
|
|
@@ -582,7 +929,7 @@ class StudioRunService:
|
|
|
582
929
|
"Studio 进程已重启,当前暂停点无法恢复",
|
|
583
930
|
status_code=409,
|
|
584
931
|
)
|
|
585
|
-
queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行")))
|
|
932
|
+
queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行"), None))
|
|
586
933
|
return {"runId": run_id, "status": "resuming"}
|
|
587
934
|
|
|
588
935
|
async def submit_interaction(
|
|
@@ -592,28 +939,76 @@ class StudioRunService:
|
|
|
592
939
|
*,
|
|
593
940
|
name: str,
|
|
594
941
|
data: dict[str, Any] | None = None,
|
|
942
|
+
expected_revision: int,
|
|
943
|
+
idempotency_key: str,
|
|
595
944
|
) -> dict[str, Any]:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
"
|
|
601
|
-
|
|
945
|
+
lock = self._interaction_locks.setdefault(run_id, asyncio.Lock())
|
|
946
|
+
payload_data = dict(data or {})
|
|
947
|
+
digest = hashlib.sha256(
|
|
948
|
+
json.dumps(
|
|
949
|
+
{"name": name, "data": payload_data},
|
|
950
|
+
ensure_ascii=False,
|
|
951
|
+
sort_keys=True,
|
|
952
|
+
separators=(",", ":"),
|
|
953
|
+
).encode("utf-8")
|
|
954
|
+
).hexdigest()
|
|
955
|
+
async with lock:
|
|
956
|
+
return await self._submit_interaction_locked(
|
|
957
|
+
run_id,
|
|
958
|
+
interaction_id,
|
|
959
|
+
name=name,
|
|
960
|
+
data=payload_data,
|
|
961
|
+
expected_revision=expected_revision,
|
|
962
|
+
idempotency_key=idempotency_key,
|
|
963
|
+
request_digest=digest,
|
|
602
964
|
)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
965
|
+
|
|
966
|
+
async def _submit_interaction_locked(
|
|
967
|
+
self,
|
|
968
|
+
run_id: str,
|
|
969
|
+
interaction_id: str,
|
|
970
|
+
*,
|
|
971
|
+
name: str,
|
|
972
|
+
data: dict[str, Any],
|
|
973
|
+
expected_revision: int,
|
|
974
|
+
idempotency_key: str,
|
|
975
|
+
request_digest: str,
|
|
976
|
+
) -> dict[str, Any]:
|
|
977
|
+
events = self.event_store.events(run_id)
|
|
978
|
+
prior = next(
|
|
979
|
+
(
|
|
980
|
+
event
|
|
981
|
+
for event in reversed(events)
|
|
982
|
+
if event.type == "a2ui.action"
|
|
983
|
+
and str(event.data.get("interactionId") or "") == interaction_id
|
|
984
|
+
and _interaction_revision(event.data) > 0
|
|
985
|
+
),
|
|
986
|
+
None,
|
|
987
|
+
)
|
|
988
|
+
if prior is not None:
|
|
989
|
+
if str(prior.data.get("idempotencyKey") or "") != idempotency_key:
|
|
990
|
+
raise StudioError(
|
|
991
|
+
"INTERACTION_ALREADY_RESOLVED",
|
|
992
|
+
"该交互已由其他请求处理",
|
|
993
|
+
status_code=409,
|
|
994
|
+
)
|
|
995
|
+
if str(prior.data.get("requestDigest") or "") != request_digest:
|
|
996
|
+
raise StudioError(
|
|
997
|
+
"INTERACTION_IDEMPOTENCY_CONFLICT",
|
|
998
|
+
"同一幂等键不能提交不同内容",
|
|
999
|
+
status_code=409,
|
|
1000
|
+
)
|
|
1001
|
+
record = self.event_store.get(run_id)
|
|
1002
|
+
if record.status == RunStatus.WAITING_INPUT:
|
|
1003
|
+
record.status = RunStatus.RUNNING
|
|
1004
|
+
self.event_store.save(record)
|
|
1005
|
+
self._waiting_modes.pop(run_id, None)
|
|
1006
|
+
return dict(prior.data.get("receipt") or {})
|
|
612
1007
|
|
|
613
1008
|
interaction = next(
|
|
614
1009
|
(
|
|
615
1010
|
event
|
|
616
|
-
for event in reversed(
|
|
1011
|
+
for event in reversed(events)
|
|
617
1012
|
if event.type == "a2ui.interaction"
|
|
618
1013
|
and str(event.data.get("interactionId") or event.data.get("interaction_id") or "")
|
|
619
1014
|
== interaction_id
|
|
@@ -622,8 +1017,29 @@ class StudioRunService:
|
|
|
622
1017
|
)
|
|
623
1018
|
if interaction is None:
|
|
624
1019
|
raise StudioError("INTERACTION_NOT_FOUND", "交互请求不存在", status_code=404)
|
|
1020
|
+
revision = _interaction_revision(interaction.data)
|
|
1021
|
+
if revision < 1:
|
|
1022
|
+
raise StudioError(
|
|
1023
|
+
"INTERACTION_READ_ONLY",
|
|
1024
|
+
"交互请求缺少权威 revision,只能查看不能操作",
|
|
1025
|
+
status_code=409,
|
|
1026
|
+
)
|
|
1027
|
+
if expected_revision != revision:
|
|
1028
|
+
raise StudioError(
|
|
1029
|
+
"INTERACTION_REVISION_MISMATCH",
|
|
1030
|
+
"交互版本已变化,请刷新后重试",
|
|
1031
|
+
status_code=409,
|
|
1032
|
+
details={"expectedRevision": expected_revision, "actualRevision": revision},
|
|
1033
|
+
)
|
|
1034
|
+
record = self.event_store.get(run_id)
|
|
1035
|
+
if record.status != RunStatus.WAITING_INPUT:
|
|
1036
|
+
raise StudioError(
|
|
1037
|
+
"INTERACTION_NOT_PENDING",
|
|
1038
|
+
"该 Run 当前没有等待中的交互",
|
|
1039
|
+
status_code=409,
|
|
1040
|
+
)
|
|
625
1041
|
kind = str(interaction.data.get("kind") or "form")
|
|
626
|
-
payload_data = {"decision": name, **
|
|
1042
|
+
payload_data = {"decision": name, **data}
|
|
627
1043
|
payload = ResumePayload(
|
|
628
1044
|
kind="approval_decision" if kind == "approval" else "hitl_answer",
|
|
629
1045
|
call_id=interaction_id,
|
|
@@ -636,7 +1052,7 @@ class StudioRunService:
|
|
|
636
1052
|
raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
|
|
637
1053
|
try:
|
|
638
1054
|
await self.executor.submit(handle, payload)
|
|
639
|
-
except
|
|
1055
|
+
except Exception as exc: # noqa: BLE001 - provider errors are user-safe here
|
|
640
1056
|
raise StudioError(
|
|
641
1057
|
"INTERACTION_SUBMIT_FAILED",
|
|
642
1058
|
str(exc),
|
|
@@ -646,45 +1062,54 @@ class StudioRunService:
|
|
|
646
1062
|
queue = self._control_queues.get(run_id)
|
|
647
1063
|
if queue is None:
|
|
648
1064
|
raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
|
|
649
|
-
|
|
1065
|
+
submit_ack = asyncio.get_running_loop().create_future()
|
|
1066
|
+
queue.put_nowait(("resume", payload, submit_ack))
|
|
1067
|
+
try:
|
|
1068
|
+
await submit_ack
|
|
1069
|
+
except Exception as exc: # noqa: BLE001 - provider errors are user-safe here
|
|
1070
|
+
raise StudioError(
|
|
1071
|
+
"INTERACTION_SUBMIT_FAILED",
|
|
1072
|
+
str(exc),
|
|
1073
|
+
status_code=409,
|
|
1074
|
+
) from exc
|
|
650
1075
|
else:
|
|
651
1076
|
raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
|
|
652
1077
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
"
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1078
|
+
next_revision = revision + 1
|
|
1079
|
+
resolved_data = {
|
|
1080
|
+
"runId": run_id,
|
|
1081
|
+
"interactionId": interaction_id,
|
|
1082
|
+
"callId": interaction_id,
|
|
1083
|
+
"name": name,
|
|
1084
|
+
"data": data,
|
|
1085
|
+
"revision": next_revision,
|
|
1086
|
+
"expectedRevision": expected_revision,
|
|
1087
|
+
"idempotencyKey": idempotency_key,
|
|
1088
|
+
}
|
|
1089
|
+
action_data = {
|
|
1090
|
+
"runId": run_id,
|
|
1091
|
+
"surfaceId": str(
|
|
1092
|
+
interaction.data.get("surfaceId") or interaction.data.get("surface_id") or ""
|
|
1093
|
+
),
|
|
1094
|
+
"interactionId": interaction_id,
|
|
1095
|
+
"actionId": f"action-{interaction_id}",
|
|
1096
|
+
"name": name,
|
|
1097
|
+
"data": data,
|
|
1098
|
+
"revision": next_revision,
|
|
1099
|
+
"expectedRevision": expected_revision,
|
|
1100
|
+
"idempotencyKey": idempotency_key,
|
|
1101
|
+
"requestDigest": request_digest,
|
|
1102
|
+
}
|
|
1103
|
+
_, _, receipt = self.event_store.append_interaction_resolution(
|
|
665
1104
|
run_id,
|
|
666
|
-
"
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
"surfaceId": str(
|
|
670
|
-
interaction.data.get("surfaceId") or interaction.data.get("surface_id") or ""
|
|
671
|
-
),
|
|
672
|
-
"interactionId": interaction_id,
|
|
673
|
-
"actionId": f"action-{interaction_id}",
|
|
674
|
-
"name": name,
|
|
675
|
-
"data": dict(data or {}),
|
|
676
|
-
},
|
|
1105
|
+
resolved_type=("approval.resolved" if kind == "approval" else "interaction.resolved"),
|
|
1106
|
+
resolved_data=resolved_data,
|
|
1107
|
+
action_data=action_data,
|
|
677
1108
|
)
|
|
678
1109
|
record.status = RunStatus.RUNNING
|
|
679
1110
|
self.event_store.save(record)
|
|
680
1111
|
self._waiting_modes.pop(run_id, None)
|
|
681
|
-
return
|
|
682
|
-
"runId": run_id,
|
|
683
|
-
"interactionId": interaction_id,
|
|
684
|
-
"status": "resolved",
|
|
685
|
-
"eventId": action.id,
|
|
686
|
-
"resolutionEventId": resolved.id,
|
|
687
|
-
}
|
|
1112
|
+
return receipt
|
|
688
1113
|
|
|
689
1114
|
async def _capture_pcm_evidence(
|
|
690
1115
|
self,
|
|
@@ -831,6 +1256,7 @@ class StudioRunService:
|
|
|
831
1256
|
"surfaceId": surface_id,
|
|
832
1257
|
"interactionId": approval_id,
|
|
833
1258
|
"kind": "approval",
|
|
1259
|
+
"revision": 1,
|
|
834
1260
|
"inputSchema": {
|
|
835
1261
|
"type": "object",
|
|
836
1262
|
"properties": {
|
|
@@ -881,7 +1307,96 @@ class StudioRunService:
|
|
|
881
1307
|
return {}
|
|
882
1308
|
|
|
883
1309
|
|
|
884
|
-
def
|
|
1310
|
+
def _plugin_event_envelope(record: RunRecord, event_type: str) -> dict[str, Any]:
|
|
1311
|
+
return {
|
|
1312
|
+
"schema_version": 2,
|
|
1313
|
+
"event_id": f"{record.id}:{event_type}",
|
|
1314
|
+
"seq": 0,
|
|
1315
|
+
"timestamp": time.time(),
|
|
1316
|
+
"run_id": record.id,
|
|
1317
|
+
"scope_id": record.id,
|
|
1318
|
+
"source": SourceRef(
|
|
1319
|
+
framework="ksadk",
|
|
1320
|
+
metadata={
|
|
1321
|
+
"runtime": "pluginhost",
|
|
1322
|
+
"agent_id": record.agent_id,
|
|
1323
|
+
"session_id": record.session_id,
|
|
1324
|
+
"build_id": record.build_id,
|
|
1325
|
+
},
|
|
1326
|
+
),
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def _normalized_plugin_usage(usage: Mapping[str, Any]) -> dict[str, Any]:
|
|
1331
|
+
def number(*keys: str) -> int:
|
|
1332
|
+
for key in keys:
|
|
1333
|
+
value = usage.get(key)
|
|
1334
|
+
if value is not None:
|
|
1335
|
+
try:
|
|
1336
|
+
return max(0, int(value))
|
|
1337
|
+
except (TypeError, ValueError):
|
|
1338
|
+
return 0
|
|
1339
|
+
return 0
|
|
1340
|
+
|
|
1341
|
+
normalized = {
|
|
1342
|
+
"input_tokens": number("input_tokens", "inputTokens", "prompt_tokens"),
|
|
1343
|
+
"output_tokens": number(
|
|
1344
|
+
"output_tokens", "outputTokens", "completion_tokens"
|
|
1345
|
+
),
|
|
1346
|
+
"total_tokens": number("total_tokens", "totalTokens"),
|
|
1347
|
+
"cached_tokens": number("cached_tokens", "cachedTokens"),
|
|
1348
|
+
"reasoning_tokens": number("reasoning_tokens", "reasoningTokens"),
|
|
1349
|
+
}
|
|
1350
|
+
if not normalized["total_tokens"]:
|
|
1351
|
+
normalized["total_tokens"] = (
|
|
1352
|
+
normalized["input_tokens"] + normalized["output_tokens"]
|
|
1353
|
+
)
|
|
1354
|
+
normalized["reported"] = bool(usage) and any(
|
|
1355
|
+
key in usage
|
|
1356
|
+
for key in (
|
|
1357
|
+
"input_tokens",
|
|
1358
|
+
"inputTokens",
|
|
1359
|
+
"prompt_tokens",
|
|
1360
|
+
"output_tokens",
|
|
1361
|
+
"outputTokens",
|
|
1362
|
+
"completion_tokens",
|
|
1363
|
+
"total_tokens",
|
|
1364
|
+
"totalTokens",
|
|
1365
|
+
)
|
|
1366
|
+
)
|
|
1367
|
+
return normalized
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def _interaction_revision(data: Mapping[str, Any]) -> int:
|
|
1371
|
+
"""Treat malformed or historical interaction revisions as read-only."""
|
|
1372
|
+
|
|
1373
|
+
try:
|
|
1374
|
+
return max(0, int(data.get("revision") or 0))
|
|
1375
|
+
except (TypeError, ValueError):
|
|
1376
|
+
return 0
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _apply_plugin_usage(record: RunRecord, usage: Mapping[str, Any]) -> None:
|
|
1380
|
+
normalized = _normalized_plugin_usage(usage)
|
|
1381
|
+
if not normalized["reported"]:
|
|
1382
|
+
return
|
|
1383
|
+
record.usage = Usage(
|
|
1384
|
+
input_tokens=normalized["input_tokens"],
|
|
1385
|
+
output_tokens=normalized["output_tokens"],
|
|
1386
|
+
total_tokens=normalized["total_tokens"],
|
|
1387
|
+
cached_input_tokens=normalized["cached_tokens"],
|
|
1388
|
+
reasoning_output_tokens=normalized["reasoning_tokens"],
|
|
1389
|
+
reported=True,
|
|
1390
|
+
source="pluginhost",
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def project_runtime_event(
|
|
1395
|
+
event: RuntimeEvent,
|
|
1396
|
+
*,
|
|
1397
|
+
session_id: str | None = None,
|
|
1398
|
+
public_run_id: str | None = None,
|
|
1399
|
+
) -> tuple[str, dict[str, Any]]:
|
|
885
1400
|
"""Project the canonical RuntimeEvent into Studio's persisted event view.
|
|
886
1401
|
|
|
887
1402
|
公开承诺字段(契约声明见 ``ksadk/events/projections.py``,执行形态为
|
|
@@ -1045,7 +1560,18 @@ def project_runtime_event(event: RuntimeEvent) -> tuple[str, dict[str, Any]]:
|
|
|
1045
1560
|
payload = {}
|
|
1046
1561
|
|
|
1047
1562
|
_attach_studio_identity(payload, event)
|
|
1563
|
+
if public_run_id and public_run_id != event.run_id:
|
|
1564
|
+
payload["runtimeRunId"] = event.run_id
|
|
1565
|
+
payload["runId"] = public_run_id
|
|
1048
1566
|
payload["runtimeEvent"] = dump_runtime_event(event)
|
|
1567
|
+
# Additive only: legacy Studio/Web consumers keep reading the established
|
|
1568
|
+
# event type and payload keys. New surfaces may opt into this typed,
|
|
1569
|
+
# identity-aware representation without reconstructing one from text.
|
|
1570
|
+
payload["conversationItem"] = project_conversation_item(
|
|
1571
|
+
event,
|
|
1572
|
+
session_id=session_id,
|
|
1573
|
+
run_id=public_run_id,
|
|
1574
|
+
).model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
1049
1575
|
return projected, payload
|
|
1050
1576
|
|
|
1051
1577
|
|
|
@@ -1073,6 +1599,11 @@ def _project_a2ui_surface(
|
|
|
1073
1599
|
projected = "a2ui.surface.end"
|
|
1074
1600
|
data_parts = event.snapshot.parts
|
|
1075
1601
|
|
|
1602
|
+
if event.source.metadata.get("operation_batch") is True and isinstance(event, ItemCompleted):
|
|
1603
|
+
# Completion closes the immutable canonical batch; it is not a request
|
|
1604
|
+
# to remove the surface that the batch just created.
|
|
1605
|
+
projected = "a2ui.surface.begin"
|
|
1606
|
+
|
|
1076
1607
|
surface_id = str(event.source.metadata.get("surface_id") or "")
|
|
1077
1608
|
operations: list[dict[str, Any]] = []
|
|
1078
1609
|
for part in data_parts:
|
|
@@ -1080,6 +1611,8 @@ def _project_a2ui_surface(
|
|
|
1080
1611
|
data = part.data
|
|
1081
1612
|
if isinstance(data, list):
|
|
1082
1613
|
operations.extend(dict(op) for op in data if isinstance(op, Mapping))
|
|
1614
|
+
elif isinstance(data, Mapping):
|
|
1615
|
+
operations.extend(project_a2ui_operations(projected, data))
|
|
1083
1616
|
if not operations:
|
|
1084
1617
|
operations = project_a2ui_operations(projected, {"surface_id": surface_id})
|
|
1085
1618
|
payload: dict[str, Any] = {
|
|
@@ -1122,6 +1655,44 @@ def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
|
|
|
1122
1655
|
"""Session envelope -> Studio RunEvent 投影;cursor 仍用 envelope.seq。"""
|
|
1123
1656
|
|
|
1124
1657
|
payload = envelope.payload or {}
|
|
1658
|
+
# Kernel RuntimeEvent is persisted as a typed runtime/v2 SessionEvent
|
|
1659
|
+
# envelope. Reuse the exact same projector as the direct Studio path so
|
|
1660
|
+
# a consumer receives its additive ConversationItem regardless of which
|
|
1661
|
+
# ingress delivered the Run. Legacy event families retain the historical
|
|
1662
|
+
# text-only fallback below.
|
|
1663
|
+
if envelope.family == "runtime" and envelope.family_version == 2:
|
|
1664
|
+
try:
|
|
1665
|
+
runtime_event = parse_runtime_event(payload)
|
|
1666
|
+
except (TypeError, ValueError):
|
|
1667
|
+
# A damaged/unknown runtime payload must not make old Studio
|
|
1668
|
+
# subscriptions fail. Preserve its event type for legacy
|
|
1669
|
+
# observability while refusing to invent a typed conversation item.
|
|
1670
|
+
return envelope.event_type, {"runId": envelope.run_id or ""}
|
|
1671
|
+
return project_runtime_event(runtime_event, session_id=envelope.session_id)
|
|
1672
|
+
if envelope.family == "interaction" and envelope.family_version == 1:
|
|
1673
|
+
conversation_item = project_interaction_conversation_item(envelope)
|
|
1674
|
+
if conversation_item is None:
|
|
1675
|
+
# Keep a malformed durable fact observable without reusing an
|
|
1676
|
+
# actionable legacy event name. Existing Studio reducers turn
|
|
1677
|
+
# ``approval.requested`` into a submit button, so emitting that
|
|
1678
|
+
# name without an authoritative revision would bypass the typed
|
|
1679
|
+
# ConversationItem fail-closed boundary.
|
|
1680
|
+
payload = envelope.payload or {}
|
|
1681
|
+
return envelope.event_type, {
|
|
1682
|
+
"runId": envelope.run_id or str(payload.get("run_id") or ""),
|
|
1683
|
+
"itemId": str(payload.get("interaction_id") or envelope.event_id),
|
|
1684
|
+
"interactionReadOnly": True,
|
|
1685
|
+
}
|
|
1686
|
+
projected = _project_interaction_envelope_legacy(envelope)
|
|
1687
|
+
if projected is None:
|
|
1688
|
+
return None
|
|
1689
|
+
event_type, data = projected
|
|
1690
|
+
data["conversationItem"] = conversation_item.model_dump(
|
|
1691
|
+
by_alias=True,
|
|
1692
|
+
exclude_none=True,
|
|
1693
|
+
mode="json",
|
|
1694
|
+
)
|
|
1695
|
+
return event_type, data
|
|
1125
1696
|
if envelope.event_type == "run.completed":
|
|
1126
1697
|
return "run.completed", {
|
|
1127
1698
|
"runId": envelope.run_id or "",
|
|
@@ -1131,3 +1702,70 @@ def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
|
|
|
1131
1702
|
if text:
|
|
1132
1703
|
return "message.delta", {"delta": text}
|
|
1133
1704
|
return None
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _project_interaction_envelope_legacy(
|
|
1708
|
+
envelope,
|
|
1709
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
1710
|
+
"""Keep established Studio event names while Interaction/v1 owns truth."""
|
|
1711
|
+
|
|
1712
|
+
payload = envelope.payload or {}
|
|
1713
|
+
interaction_id = str(payload.get("interaction_id") or "")
|
|
1714
|
+
interaction_kind = str(payload.get("kind") or "")
|
|
1715
|
+
revision = payload.get("revision")
|
|
1716
|
+
request = payload.get("request")
|
|
1717
|
+
request = request if isinstance(request, Mapping) else {}
|
|
1718
|
+
presentation = request.get("presentation")
|
|
1719
|
+
presentation = presentation if isinstance(presentation, Mapping) else {}
|
|
1720
|
+
common: dict[str, Any] = {
|
|
1721
|
+
"runId": envelope.run_id or str(payload.get("run_id") or ""),
|
|
1722
|
+
"itemId": interaction_id,
|
|
1723
|
+
"interactionId": interaction_id,
|
|
1724
|
+
}
|
|
1725
|
+
# Expose the value as received for legacy/read-only diagnostics. Only the
|
|
1726
|
+
# typed ConversationItem validates it as an authoritative writable token.
|
|
1727
|
+
if revision is not None:
|
|
1728
|
+
common["revision"] = revision
|
|
1729
|
+
|
|
1730
|
+
if envelope.event_type == "interaction.requested":
|
|
1731
|
+
if interaction_kind == "approval":
|
|
1732
|
+
return "approval.requested", {
|
|
1733
|
+
**common,
|
|
1734
|
+
"approvalId": interaction_id,
|
|
1735
|
+
"callId": "",
|
|
1736
|
+
"kind": str(presentation.get("title") or interaction_kind),
|
|
1737
|
+
"detail": presentation.get("description"),
|
|
1738
|
+
}
|
|
1739
|
+
return "a2ui.interaction", {
|
|
1740
|
+
**common,
|
|
1741
|
+
"kind": interaction_kind or "form",
|
|
1742
|
+
"inputSchema": (
|
|
1743
|
+
dict(request.get("request_schema"))
|
|
1744
|
+
if isinstance(request.get("request_schema"), Mapping)
|
|
1745
|
+
else {}
|
|
1746
|
+
),
|
|
1747
|
+
}
|
|
1748
|
+
if envelope.event_type in {
|
|
1749
|
+
"interaction.resolved",
|
|
1750
|
+
"interaction.cancelled",
|
|
1751
|
+
"interaction.expired",
|
|
1752
|
+
}:
|
|
1753
|
+
outcome = str(
|
|
1754
|
+
payload.get("outcome")
|
|
1755
|
+
or (
|
|
1756
|
+
"cancelled"
|
|
1757
|
+
if envelope.event_type == "interaction.cancelled"
|
|
1758
|
+
else "expired"
|
|
1759
|
+
if envelope.event_type == "interaction.expired"
|
|
1760
|
+
else ""
|
|
1761
|
+
)
|
|
1762
|
+
)
|
|
1763
|
+
if interaction_kind == "approval":
|
|
1764
|
+
return "approval.resolved", {
|
|
1765
|
+
**common,
|
|
1766
|
+
"approvalId": interaction_id,
|
|
1767
|
+
"callId": "",
|
|
1768
|
+
"decision": outcome,
|
|
1769
|
+
}
|
|
1770
|
+
return "a2ui.action", {**common, "name": outcome}
|
|
1771
|
+
return None
|