agentengine-sdk-python 0.8.1__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.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/METADATA +25 -12
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/RECORD +421 -232
- ksadk/_build_provenance.json +1 -0
- 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 +247 -0
- ksadk/agui/a2ui_projection.py +12 -5
- ksadk/agui/agent.py +443 -259
- ksadk/api/client.py +528 -27
- ksadk/builders/code_builder.py +284 -8
- ksadk/builders/container_builder.py +8 -1
- ksadk/builders/managed_runtime_builder.py +42 -35
- ksadk/cli/__init__.py +23 -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_plugin.py +657 -0
- 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 +475 -331
- ksadk/configs/env_registry.py +94 -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/configs/settings.py +9 -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 +409 -0
- ksadk/context_engine/hosted_pipeline.py +417 -0
- ksadk/context_engine/models.py +117 -0
- ksadk/context_engine/planner.py +698 -0
- ksadk/context_engine/policies.py +319 -0
- ksadk/context_engine/projection.py +31 -0
- ksadk/context_engine/shadow_plan.py +266 -0
- ksadk/context_engine/tokenizer.py +164 -0
- ksadk/conversations/__init__.py +19 -0
- ksadk/conversations/context.py +230 -30
- ksadk/conversations/contracts.py +266 -0
- ksadk/conversations/message_projection.py +308 -26
- ksadk/conversations/model_context.py +49 -3
- ksadk/conversations/projector.py +616 -0
- ksadk/conversations/reducer.py +98 -0
- 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 +755 -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 +907 -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/__init__.py +45 -35
- ksadk/harness/runtime.py +220 -58
- 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 +62 -0
- ksadk/kernel/contracts.py +370 -0
- ksadk/kernel/control.py +220 -0
- ksadk/kernel/errors.py +79 -0
- ksadk/kernel/ingress.py +1123 -0
- ksadk/kernel/mapping.py +111 -0
- ksadk/kernel/memory_store.py +1134 -0
- ksadk/kernel/postgres_store.py +1757 -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 +1411 -0
- ksadk/kernel/state.py +145 -0
- ksadk/kernel/store.py +232 -0
- ksadk/kernel/worker.py +964 -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 +497 -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/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/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 +797 -0
- ksadk/runtime/adapter.py +119 -2
- ksadk/runtime/conversation_execution.py +316 -182
- ksadk/runtime/executor.py +144 -3
- ksadk/runtime/factory.py +268 -8
- ksadk/runtime/framework_adapters.py +18 -0
- ksadk/runtime/hosted_finalizer.py +268 -0
- ksadk/runtime/launch.py +14 -1
- ksadk/runtime/preprocessing.py +25 -3
- ksadk/runtime/runner_adapter.py +293 -508
- ksadk/runtime/usage.py +33 -0
- ksadk/sandbox/__init__.py +2 -1
- ksadk/sandbox/backends/e2b.py +35 -2
- 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/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-10GPXIJ6.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-CynLknyO.js → CodeBlock-BXk1l-G1.js} +4 -4
- ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
- ksadk/server/static/assets/{MathMessageMarkdown-3sQdOysS.js → MathMessageMarkdown-D9uB55Oh.js} +4 -4
- ksadk/server/static/assets/MermaidBlock--OEYoXIJ.js +312 -0
- ksadk/server/static/assets/NativeTerminalPanel-DrEe1syq.js +1 -0
- ksadk/server/static/assets/abnfDiagram-VCTEODGH-eeTFJLvo.js +1 -0
- ksadk/server/static/assets/arc-S3ZNbIAd.js +1 -0
- ksadk/server/static/assets/architecture-7GRP2DOG-Dk9nRQ6-.js +1 -0
- ksadk/server/static/assets/architectureDiagram-5GKGNRK7-CRjgEJDX.js +36 -0
- ksadk/server/static/assets/blockDiagram-NRAW4CY4-C7bsBoNH.js +129 -0
- ksadk/server/static/assets/c4Diagram-UCG6FXSJ-BX3c33Ob.js +38 -0
- ksadk/server/static/assets/channel-s354Yo6o.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-CDpPpKR5.js +1 -0
- ksadk/server/static/assets/{chunk-FMBD7UC4-B_fH2YuH.js → chunk-5VM5RSS4-bZ7ws-8m.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-D6rFLaVV.js → chunk-F27PBJKO-D2r0pvhY.js} +1 -1
- ksadk/server/static/assets/chunk-G27WJ6UU-CZwF3oUP.js +231 -0
- ksadk/server/static/assets/{chunk-4BX2VUAB-B9drTgOW.js → chunk-JWPE2WC7-DigFYCML.js} +1 -1
- ksadk/server/static/assets/chunk-LCL6LL3I-CnorGV5q.js +206 -0
- ksadk/server/static/assets/chunk-POPQ4Y6H-CexntQA-.js +1 -0
- ksadk/server/static/assets/chunk-RHFEMEQ7-CHTC7UxA.js +168 -0
- ksadk/server/static/assets/chunk-SVP7TREG-BC1EWt_-.js +88 -0
- ksadk/server/static/assets/chunk-XXDRQBXY-BTG24xN0.js +1 -0
- 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-S5V4N54A-ChNs_Mdk.js → cose-bilkent-JH36ORCC-CkonRdAS.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-BBlT4uMf.js +1 -0
- ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-DEOCBe15.js +62 -0
- ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
- ksadk/server/static/assets/dagre-3AP2YEHR-DBThPuLa.js +4 -0
- ksadk/server/static/assets/dagre-ClTPrTht.js +1 -0
- ksadk/server/static/assets/diagram-S7CK7UJ4-DsZHrnpJ.js +30 -0
- ksadk/server/static/assets/diagram-UQ7AKVKN-BJR4nhVr.js +41 -0
- ksadk/server/static/assets/diagram-VSXAHHWV-xzcERX0a.js +3 -0
- ksadk/server/static/assets/diagram-VX7I27RA-tn2xM1xu.js +24 -0
- ksadk/server/static/assets/diagram-Z3DM3KII-BZPGBkw5.js +24 -0
- ksadk/server/static/assets/{dist-gLnrGsoU.js → dist-4zN1_tq5.js} +1 -1
- ksadk/server/static/assets/{dist-C1sz5Eo3.js → dist-8VWI-2bZ.js} +1 -1
- ksadk/server/static/assets/{dist-BjOdlDjj.js → dist-B-46q3Hk.js} +1 -1
- ksadk/server/static/assets/{dist-o2OVmKvt.js → dist-B0gdk4rl.js} +1 -1
- ksadk/server/static/assets/{dist-4N2NMVYg.js → dist-B1oWRmrH.js} +1 -1
- ksadk/server/static/assets/{dist-CmZHzlwZ.js → dist-BI1qQH4n.js} +1 -1
- ksadk/server/static/assets/{dist-X4TnqwOK.js → dist-BJLUwH6W.js} +1 -1
- ksadk/server/static/assets/{dist-B5oS1zcj.js → dist-Bbwqa8TG.js} +1 -1
- ksadk/server/static/assets/{dist-j6Dxmf_j.js → dist-Btyo4vX4.js} +1 -1
- ksadk/server/static/assets/{dist-DQU9EWw-.js → dist-C71CsujO.js} +1 -1
- ksadk/server/static/assets/{dist-BIlKoW0k.js → dist-CXpH_Rdd.js} +1 -1
- ksadk/server/static/assets/{dist-DJN_6Y39.js → dist-Cq4OL6nO.js} +1 -1
- ksadk/server/static/assets/{dist-Dli7qW-B.js → dist-D1V3f1B4.js} +1 -1
- ksadk/server/static/assets/{dist-fhOuWds5.js → dist-DJmCPRVB.js} +1 -1
- ksadk/server/static/assets/{dist-CWV4lDr1.js → dist-DQAB0qn_.js} +2 -2
- ksadk/server/static/assets/{dist-D6MpCZtz.js → dist-DUwKa_l6.js} +1 -1
- ksadk/server/static/assets/dist-DdRBIceA.js +1 -0
- ksadk/server/static/assets/{dist-DWKByxWV.js → dist-DfGFrb0v.js} +1 -1
- ksadk/server/static/assets/{dist-DsHPdk3W.js → dist-DjhwGUgZ.js} +1 -1
- ksadk/server/static/assets/{dist-B-jNR6Vn.js → dist-DyqV4fL4.js} +1 -1
- ksadk/server/static/assets/{dist-BB3-OwTQ.js → dist-M67Hwu0n.js} +1 -1
- ksadk/server/static/assets/{dist-_pHPTyZ-.js → dist-u8tVH5Ak.js} +1 -1
- ksadk/server/static/assets/ebnfDiagram-PWID7BFC-DVfMsLge.js +1 -0
- ksadk/server/static/assets/erDiagram-SSCWMZ5O-CWEZSYaw.js +99 -0
- 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-TWIxoyYL.js +292 -0
- ksadk/server/static/assets/gitGraph-4MIJSDKK-CKiKY9Cf.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-DDh2hd2S.js +106 -0
- 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-CgoyWiDR.js +2 -0
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-K0OOfVna.js → ishikawaDiagram-5VMMS53U-CyRIu_om.js} +6 -6
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-8LeOJaZp.js → journeyDiagram-EYS64GPL-DN5JXHzN.js} +6 -6
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-C6uCgZrW.js → kanban-definition-3QL26DDD-CNP3ui66.js} +10 -10
- ksadk/server/static/assets/katex-DWEzQy1d.js +257 -0
- ksadk/server/static/assets/{linear-hdBcYzM8.js → linear-C-qHp7d2.js} +1 -1
- ksadk/server/static/assets/mermaid-parser.core-Cl-K943T.js +166 -0
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-Bjy_2v4_.js → mindmap-definition-FBJOCRG2-D5DB5mYm.js} +29 -29
- ksadk/server/static/assets/packet-AYTQ26CC-C7R3NUlK.js +1 -0
- ksadk/server/static/assets/pegDiagram-XKGWAZYB-BQVc9ia_.js +1 -0
- ksadk/server/static/assets/pie-WAS4IAKB-BIT6qjr3.js +1 -0
- ksadk/server/static/assets/pieDiagram-E7YTZNPT-B6UCAvtY.js +39 -0
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-BmkRbwCg.js → quadrantDiagram-AXDQQJYC-mIr2d3eb.js} +6 -6
- 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-Dt3FFDmK.js +1 -0
- ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-Cz_vQtgF.js +84 -0
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-l6iXmU65.js → sankeyDiagram-P5KCCOFB-wlO1TsH5.js} +7 -7
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-Cq6UBeQB.js → sequenceDiagram-WJ2MYXX4-CHRbElYq.js} +10 -10
- ksadk/server/static/assets/sizeCapture-X5ZJPWSS-DXX54iyO.js +1 -0
- ksadk/server/static/assets/stateDiagram-HBIQ2CUA-Dp_lw9II.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-5b-8o2iS.js +1 -0
- ksadk/server/static/assets/swimlanes-XN3QIQJK-D8O5UlLG.js +1 -0
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-uf6WPRA4.js +8 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-DEz24vV0.js → timeline-definition-24CTP7MA-8mfKcxvG.js} +7 -7
- 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-EnNl0DE7.js +34 -0
- ksadk/server/static/assets/wardley-WFR3VGLG-YW3Q0ml_.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DBu5fbYr.js +78 -0
- ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-BSVXs4IY.js +7 -0
- ksadk/server/static/index.html +2 -2
- ksadk/sessions/__init__.py +26 -9
- ksadk/sessions/_local_service_sync.py +688 -0
- ksadk/sessions/_local_tables.py +21 -0
- ksadk/sessions/_postgres_schema.py +305 -0
- ksadk/sessions/_postgres_tables.py +15 -0
- ksadk/sessions/base.py +80 -1
- ksadk/sessions/in_memory.py +53 -2
- ksadk/sessions/local_service.py +63 -604
- ksadk/sessions/postgres_service.py +58 -148
- ksadk/sessions/resilient.py +35 -1
- ksadk/skills/service_client.py +48 -0
- ksadk/studio/agent_lifecycle.py +26 -20
- ksadk/studio/api.py +1063 -69
- ksadk/studio/api_catalog_routes.py +5 -0
- ksadk/studio/api_contracts.py +148 -6
- ksadk/studio/api_memory_routes.py +82 -0
- ksadk/studio/api_plugin_routes.py +480 -0
- ksadk/studio/attachment_store.py +187 -0
- ksadk/studio/authoring.py +259 -28
- ksadk/studio/authoring_coordinator.py +809 -67
- ksadk/studio/builder.py +197 -40
- ksadk/studio/capabilities.py +17 -0
- ksadk/studio/cloud.py +1920 -95
- ksadk/studio/codex_agent_service.py +137 -15
- ksadk/studio/codex_authoring.py +469 -0
- ksadk/studio/codex_builder.py +177 -25
- ksadk/studio/codex_manifest.py +71 -22
- ksadk/studio/codex_run.py +59 -4
- ksadk/studio/compatibility_report.py +445 -0
- ksadk/studio/compiler.py +7 -1
- ksadk/studio/contracts.py +234 -4
- ksadk/studio/dsh_provider_registration.py +768 -0
- ksadk/studio/event_store.py +79 -101
- ksadk/studio/framework_run.py +264 -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 +47 -5
- ksadk/studio/otel_trace.py +348 -42
- ksadk/studio/pcm_memory.py +85 -0
- ksadk/studio/plugin_composition.py +301 -0
- ksadk/studio/plugin_kernel_adapter.py +128 -0
- ksadk/studio/plugin_runtime.py +652 -0
- ksadk/studio/repository.py +23 -2
- ksadk/studio/resource_catalog.py +49 -1
- ksadk/studio/run_service.py +1266 -202
- ksadk/studio/runtime_source.py +51 -17
- ksadk/studio/scheduler_runtime.py +374 -0
- ksadk/studio/scheduler_service.py +211 -0
- ksadk/studio/service.py +1815 -117
- ksadk/studio/shared_web.py +199 -50
- 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/templates.py +100 -8
- ksadk/studio/validator.py +20 -1
- ksadk/studio/workspace.py +31 -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_runtime_common/workspace_files/path_utils.py +41 -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.3.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.1.dist-info → agentengine_sdk_python-0.8.3.dist-info}/top_level.txt +0 -0
ksadk/studio/run_service.py
CHANGED
|
@@ -3,15 +3,54 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
6
9
|
import time
|
|
7
10
|
from collections.abc import Callable, Mapping
|
|
8
|
-
from dataclasses import dataclass, field
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
9
12
|
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
10
14
|
from typing import Any
|
|
11
15
|
from uuid import uuid4
|
|
12
16
|
|
|
13
17
|
from ksadk.agui.a2ui_projection import project_a2ui_operations
|
|
14
|
-
from ksadk.
|
|
18
|
+
from ksadk.conversations.projector import (
|
|
19
|
+
project_conversation_item,
|
|
20
|
+
project_interaction_conversation_item,
|
|
21
|
+
)
|
|
22
|
+
from ksadk.events.canonical import (
|
|
23
|
+
ContinuationCreated,
|
|
24
|
+
ContinuationResumed,
|
|
25
|
+
ErrorInfo,
|
|
26
|
+
InteractionRequested,
|
|
27
|
+
InteractionResolved,
|
|
28
|
+
ItemCompleted,
|
|
29
|
+
ItemFailed,
|
|
30
|
+
ItemStarted,
|
|
31
|
+
ItemUpdated,
|
|
32
|
+
OutputRef,
|
|
33
|
+
RunCanceled,
|
|
34
|
+
RunCompleted,
|
|
35
|
+
RunFailed,
|
|
36
|
+
RunInterrupted,
|
|
37
|
+
RunProgress,
|
|
38
|
+
RunStarted,
|
|
39
|
+
RuntimeEvent,
|
|
40
|
+
SourceRef,
|
|
41
|
+
UsageReported,
|
|
42
|
+
dump_runtime_event,
|
|
43
|
+
parse_runtime_event,
|
|
44
|
+
)
|
|
45
|
+
from ksadk.events.canonical_store import session_event_to_runtime_event
|
|
46
|
+
from ksadk.events.content import (
|
|
47
|
+
ContentSnapshot,
|
|
48
|
+
DataContent,
|
|
49
|
+
TextContent,
|
|
50
|
+
ToolCallContent,
|
|
51
|
+
ToolResultContent,
|
|
52
|
+
)
|
|
53
|
+
from ksadk.events.store import RuntimeEventStore
|
|
15
54
|
from ksadk.runtime import (
|
|
16
55
|
CONVERSATION_PREPROCESSING_METADATA_KEY,
|
|
17
56
|
PauseResult,
|
|
@@ -21,6 +60,8 @@ from ksadk.runtime import (
|
|
|
21
60
|
RuntimeLaunchContext,
|
|
22
61
|
StartRequest,
|
|
23
62
|
)
|
|
63
|
+
from ksadk.sessions.base import BaseSessionService
|
|
64
|
+
from ksadk.sessions.local_service import LocalSessionService
|
|
24
65
|
from ksadk.studio.contracts import RunEvent, RunRecord, RunStatus, Usage
|
|
25
66
|
from ksadk.studio.errors import StudioError
|
|
26
67
|
from ksadk.studio.event_store import RunEventStore
|
|
@@ -29,6 +70,9 @@ from ksadk.studio.workspace import Workspace
|
|
|
29
70
|
_CANCEL_TIMEOUT_SECONDS = 2.0
|
|
30
71
|
|
|
31
72
|
|
|
73
|
+
logger = logging.getLogger(__name__)
|
|
74
|
+
|
|
75
|
+
|
|
32
76
|
@dataclass(frozen=True)
|
|
33
77
|
class StudioRunSpec:
|
|
34
78
|
"""Product metadata plus the core launch context for one Studio run."""
|
|
@@ -39,6 +83,7 @@ class StudioRunSpec:
|
|
|
39
83
|
model: str | None = None
|
|
40
84
|
request_config: Mapping[str, Any] = field(default_factory=dict)
|
|
41
85
|
manifest_sha256: str = ""
|
|
86
|
+
plugin_bundle_root: Path | None = None
|
|
42
87
|
|
|
43
88
|
|
|
44
89
|
class StudioRunService:
|
|
@@ -50,14 +95,57 @@ class StudioRunService:
|
|
|
50
95
|
executor: RuntimeExecutor,
|
|
51
96
|
*,
|
|
52
97
|
event_store: RunEventStore | None = None,
|
|
98
|
+
session_service: BaseSessionService | None = None,
|
|
99
|
+
runtime_events: RuntimeEventStore | None = None,
|
|
100
|
+
plugin_runtime: Any | None = None,
|
|
53
101
|
) -> None:
|
|
54
102
|
self.workspace = workspace
|
|
55
103
|
self.executor = executor
|
|
56
104
|
self.event_store = event_store or RunEventStore(workspace)
|
|
105
|
+
self.session_service = session_service or LocalSessionService(
|
|
106
|
+
project_dir=str(workspace.root)
|
|
107
|
+
)
|
|
108
|
+
self.runtime_events = runtime_events or RuntimeEventStore(self.session_service)
|
|
109
|
+
self.plugin_runtime = plugin_runtime
|
|
57
110
|
self._active_handles: dict[str, Any] = {}
|
|
58
111
|
self._cancel_flags: dict[str, bool] = {}
|
|
59
|
-
self._control_queues: dict[
|
|
112
|
+
self._control_queues: dict[
|
|
113
|
+
str,
|
|
114
|
+
asyncio.Queue[tuple[str, ResumePayload | None, asyncio.Future[None] | None]],
|
|
115
|
+
] = {}
|
|
60
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] = {}
|
|
121
|
+
|
|
122
|
+
async def recover_interrupted(self) -> None:
|
|
123
|
+
"""Settle local runs left active across a Studio restart.
|
|
124
|
+
|
|
125
|
+
An in-process handle cannot be resumed safely. Keep its durable event
|
|
126
|
+
history and record an explicit interruption; lease-backed hosted
|
|
127
|
+
recovery remains owned by the Kernel composition root.
|
|
128
|
+
"""
|
|
129
|
+
active = {RunStatus.RUNNING, RunStatus.WAITING_INPUT}
|
|
130
|
+
for record in self.event_store.list_runs():
|
|
131
|
+
if record.status not in active:
|
|
132
|
+
continue
|
|
133
|
+
record.status = RunStatus.INTERRUPTED
|
|
134
|
+
record.completed_at = datetime.now(timezone.utc)
|
|
135
|
+
record.error = {
|
|
136
|
+
"code": "LOCAL_STUDIO_RESTARTED",
|
|
137
|
+
"message": "本地 Studio 重启,运行未在本地恢复。",
|
|
138
|
+
}
|
|
139
|
+
self.event_store.save(record)
|
|
140
|
+
self.event_store.append(
|
|
141
|
+
record.id,
|
|
142
|
+
"run.interrupted",
|
|
143
|
+
{"reason": "local_studio_restarted", "recoverable": False},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
async def events(self, run_id: str, *, after: int = 0) -> list[RunEvent]:
|
|
147
|
+
"""Read the durable Studio event timeline for API/SSE replay."""
|
|
148
|
+
return self.event_store.events(run_id, after=after)
|
|
61
149
|
|
|
62
150
|
async def run(
|
|
63
151
|
self,
|
|
@@ -84,7 +172,9 @@ class StudioRunService:
|
|
|
84
172
|
goal_objective=str(spec.request_config.get("goal_objective") or ""),
|
|
85
173
|
input=user_input,
|
|
86
174
|
)
|
|
175
|
+
await self.session_service.create_session(spec.agent_id, "local-user", session)
|
|
87
176
|
self.event_store.create(record)
|
|
177
|
+
prepared_turn = await self._capture_pcm_evidence(record, spec, user_input)
|
|
88
178
|
created = self.event_store.append(
|
|
89
179
|
record.id,
|
|
90
180
|
"run.created",
|
|
@@ -103,23 +193,56 @@ class StudioRunService:
|
|
|
103
193
|
if on_event is not None:
|
|
104
194
|
on_event(created)
|
|
105
195
|
|
|
196
|
+
kernel_runtime = self._kernel_runtime_for_spec(spec)
|
|
197
|
+
if kernel_runtime is not None:
|
|
198
|
+
return await self._kernel_run(
|
|
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,
|
|
211
|
+
)
|
|
212
|
+
|
|
106
213
|
started = time.monotonic()
|
|
107
214
|
record.status = RunStatus.RUNNING
|
|
108
215
|
record.started_at = datetime.now(timezone.utc)
|
|
109
216
|
self.event_store.save(record)
|
|
110
217
|
|
|
111
|
-
def persist(runtime_event: RuntimeEvent) -> RunEvent:
|
|
112
|
-
|
|
218
|
+
async def persist(runtime_event: RuntimeEvent) -> RunEvent:
|
|
219
|
+
persisted = await self.runtime_events.append_one(record.session_id, 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
|
|
113
232
|
stored = self.event_store.append(record.id, event_type, data)
|
|
114
233
|
if on_event is not None:
|
|
115
234
|
on_event(stored)
|
|
116
235
|
return stored
|
|
117
236
|
|
|
118
237
|
handle = None
|
|
119
|
-
|
|
120
|
-
|
|
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, ...] = ()
|
|
121
241
|
runtime_duration_ms: int | None = None
|
|
122
|
-
|
|
242
|
+
item_phases: dict[tuple[str, str], str | None] = {}
|
|
243
|
+
control_queue: asyncio.Queue[
|
|
244
|
+
tuple[str, ResumePayload | None, asyncio.Future[None] | None]
|
|
245
|
+
] = asyncio.Queue()
|
|
123
246
|
self._control_queues[run_id] = control_queue
|
|
124
247
|
try:
|
|
125
248
|
tool_approval_mode = str(spec.request_config.get("tool_approval_mode") or "")
|
|
@@ -134,6 +257,8 @@ class StudioRunService:
|
|
|
134
257
|
conversation_request["request_metadata"] = {
|
|
135
258
|
"tool_approval_mode": tool_approval_mode,
|
|
136
259
|
}
|
|
260
|
+
if prepared_turn is not None:
|
|
261
|
+
conversation_request["prepared_turn"] = asdict(prepared_turn)
|
|
137
262
|
request = StartRequest(
|
|
138
263
|
input=runtime_input if runtime_input is not None else user_input,
|
|
139
264
|
user_id="local-user",
|
|
@@ -155,63 +280,76 @@ class StudioRunService:
|
|
|
155
280
|
terminal_seen = False
|
|
156
281
|
should_resume = False
|
|
157
282
|
async for event in self.executor.stream(handle):
|
|
158
|
-
persist(event)
|
|
283
|
+
await persist(event)
|
|
159
284
|
if self._cancel_flags.get(run_id):
|
|
160
285
|
raise asyncio.CancelledError()
|
|
161
|
-
if event
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
286
|
+
if isinstance(event, ItemStarted) and event.item_kind == "message":
|
|
287
|
+
item_phases[(event.scope_id, event.item_id)] = event.phase
|
|
288
|
+
elif (
|
|
289
|
+
isinstance(event, ItemUpdated)
|
|
290
|
+
and event.item_kind == "message"
|
|
291
|
+
and item_phases.get(
|
|
292
|
+
(event.scope_id, event.item_id), "final_answer"
|
|
293
|
+
)
|
|
294
|
+
== "final_answer"
|
|
295
|
+
):
|
|
296
|
+
text = event.update.text if isinstance(event.update, TextContent) else ""
|
|
297
|
+
item_key = (event.scope_id, event.item_id)
|
|
298
|
+
if event.op == "replace":
|
|
299
|
+
streamed_text_by_item[item_key] = text
|
|
165
300
|
else:
|
|
166
|
-
|
|
301
|
+
streamed_text_by_item[item_key] = (
|
|
302
|
+
streamed_text_by_item.get(item_key, "") + text
|
|
303
|
+
)
|
|
167
304
|
elif (
|
|
168
|
-
event
|
|
169
|
-
and event.
|
|
305
|
+
isinstance(event, ItemCompleted)
|
|
306
|
+
and event.item_kind == "message"
|
|
307
|
+
and item_phases.get(
|
|
308
|
+
(event.scope_id, event.item_id), "final_answer"
|
|
309
|
+
)
|
|
310
|
+
== "final_answer"
|
|
170
311
|
):
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
)
|
|
317
|
+
elif isinstance(event, UsageReported):
|
|
173
318
|
record.usage = Usage(
|
|
174
|
-
input_tokens=
|
|
175
|
-
output_tokens=
|
|
176
|
-
total_tokens=
|
|
177
|
-
cached_input_tokens=
|
|
178
|
-
reasoning_output_tokens=
|
|
319
|
+
input_tokens=event.input_tokens,
|
|
320
|
+
output_tokens=event.output_tokens,
|
|
321
|
+
total_tokens=event.total_tokens,
|
|
322
|
+
cached_input_tokens=event.cached_tokens,
|
|
323
|
+
reasoning_output_tokens=event.reasoning_tokens,
|
|
179
324
|
reported=True,
|
|
180
|
-
source=str(event.
|
|
325
|
+
source=str(event.source.framework or runtime_type),
|
|
181
326
|
)
|
|
182
|
-
elif event
|
|
183
|
-
EventType.APPROVAL_REQUESTED,
|
|
184
|
-
EventType.A2UI_INTERACTION,
|
|
185
|
-
}:
|
|
327
|
+
elif isinstance(event, InteractionRequested):
|
|
186
328
|
record.status = RunStatus.WAITING_INPUT
|
|
187
329
|
self._waiting_modes[run_id] = "live"
|
|
188
330
|
self.event_store.save(record)
|
|
189
|
-
if event.
|
|
331
|
+
if event.interaction_kind == "approval":
|
|
190
332
|
self._persist_approval_surface(record, event, on_event=on_event)
|
|
191
|
-
elif event
|
|
192
|
-
EventType.APPROVAL_RESOLVED,
|
|
193
|
-
EventType.A2UI_ACTION,
|
|
194
|
-
}:
|
|
333
|
+
elif isinstance(event, InteractionResolved):
|
|
195
334
|
record.status = RunStatus.RUNNING
|
|
196
335
|
self._waiting_modes.pop(run_id, None)
|
|
197
336
|
self.event_store.save(record)
|
|
198
|
-
elif event
|
|
337
|
+
elif isinstance(event, RunFailed):
|
|
199
338
|
terminal_seen = True
|
|
200
339
|
record.status = RunStatus.FAILED
|
|
201
340
|
record.error = {
|
|
202
341
|
"code": "RUNTIME_RUN_FAILED",
|
|
203
|
-
"message": str(event.
|
|
342
|
+
"message": str(event.error.message or "Runtime 运行失败"),
|
|
204
343
|
}
|
|
205
|
-
elif event
|
|
344
|
+
elif isinstance(event, RunCanceled):
|
|
206
345
|
terminal_seen = True
|
|
207
346
|
record.status = RunStatus.CANCELLED
|
|
208
347
|
record.error = {
|
|
209
348
|
"code": "RUN_CANCELLED",
|
|
210
|
-
"message": str(event.
|
|
349
|
+
"message": str(event.reason or "运行已取消"),
|
|
211
350
|
}
|
|
212
|
-
elif event
|
|
213
|
-
|
|
214
|
-
if interrupted_status == "paused":
|
|
351
|
+
elif isinstance(event, RunInterrupted):
|
|
352
|
+
if event.reason == "user_pause":
|
|
215
353
|
record.status = RunStatus.PAUSED
|
|
216
354
|
should_resume = True
|
|
217
355
|
elif record.status == RunStatus.WAITING_INPUT:
|
|
@@ -222,13 +360,18 @@ class StudioRunService:
|
|
|
222
360
|
record.status = RunStatus.INTERRUPTED
|
|
223
361
|
record.error = {
|
|
224
362
|
"code": "RUN_INTERRUPTED",
|
|
225
|
-
"message":
|
|
363
|
+
"message": event.reason or "运行已中断",
|
|
226
364
|
}
|
|
227
365
|
self.event_store.save(record)
|
|
228
|
-
elif event
|
|
366
|
+
elif isinstance(event, RunCompleted):
|
|
229
367
|
terminal_seen = True
|
|
230
368
|
record.status = RunStatus.COMPLETED
|
|
231
|
-
|
|
369
|
+
terminal_output_refs = event.output_refs
|
|
370
|
+
raw_duration = event.source.metadata.get("duration_ms")
|
|
371
|
+
if raw_duration is None:
|
|
372
|
+
metrics = event.source.metadata.get("metrics")
|
|
373
|
+
if isinstance(metrics, dict):
|
|
374
|
+
raw_duration = metrics.get("duration_ms")
|
|
232
375
|
if raw_duration is not None:
|
|
233
376
|
runtime_duration_ms = max(0, int(raw_duration))
|
|
234
377
|
if terminal_seen:
|
|
@@ -237,7 +380,7 @@ class StudioRunService:
|
|
|
237
380
|
record.status = RunStatus.COMPLETED
|
|
238
381
|
break
|
|
239
382
|
|
|
240
|
-
command, resume_payload = await control_queue.get()
|
|
383
|
+
command, resume_payload, submit_ack = await control_queue.get()
|
|
241
384
|
if command == "cancel" or self._cancel_flags.get(run_id):
|
|
242
385
|
raise asyncio.CancelledError()
|
|
243
386
|
if command != "resume":
|
|
@@ -248,11 +391,18 @@ class StudioRunService:
|
|
|
248
391
|
if native_thread_id
|
|
249
392
|
else ResumeTarget(kind="invocation_id", id=handle.run_id)
|
|
250
393
|
)
|
|
251
|
-
|
|
252
|
-
handle
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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)
|
|
256
406
|
self._active_handles[run_id] = handle
|
|
257
407
|
record.runtime_handle = handle.model_dump(mode="json")
|
|
258
408
|
record.status = RunStatus.RUNNING
|
|
@@ -266,7 +416,17 @@ class StudioRunService:
|
|
|
266
416
|
if on_event is not None:
|
|
267
417
|
on_event(resumed)
|
|
268
418
|
self.event_store.save(record)
|
|
269
|
-
|
|
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)
|
|
270
430
|
except asyncio.CancelledError:
|
|
271
431
|
cancel_result = "task_cancelled"
|
|
272
432
|
if handle is not None and self.executor.is_attached(handle):
|
|
@@ -282,30 +442,44 @@ class StudioRunService:
|
|
|
282
442
|
cancel_result = "cancel_failed"
|
|
283
443
|
record.status = RunStatus.CANCELLED
|
|
284
444
|
record.error = {"code": "RUN_CANCELLED", "message": "运行已取消"}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
445
|
+
cancelled_run_id = handle.run_id if handle is not None else run_id
|
|
446
|
+
cancelled = RunCanceled(
|
|
447
|
+
schema_version=2,
|
|
448
|
+
event_id=f"evt_cancel_{uuid4().hex}",
|
|
449
|
+
seq=len(self.event_store.events(run_id)) + 1,
|
|
450
|
+
timestamp=time.time(),
|
|
451
|
+
run_id=cancelled_run_id,
|
|
452
|
+
scope_id=cancelled_run_id,
|
|
453
|
+
source=SourceRef(
|
|
454
|
+
framework="ksadk",
|
|
455
|
+
metadata={"cancel_result": cancel_result},
|
|
456
|
+
),
|
|
457
|
+
status="canceled",
|
|
458
|
+
reason="cancelled",
|
|
293
459
|
)
|
|
294
|
-
persist(cancelled)
|
|
460
|
+
await persist(cancelled)
|
|
295
461
|
raise
|
|
296
462
|
except Exception as exc: # noqa: BLE001
|
|
297
463
|
record.status = RunStatus.FAILED
|
|
298
464
|
record.error = {"code": "RUNTIME_RUN_FAILED", "message": str(exc)}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
465
|
+
failed_run_id = handle.run_id if handle is not None else run_id
|
|
466
|
+
failure = RunFailed(
|
|
467
|
+
schema_version=2,
|
|
468
|
+
event_id=f"evt_fail_{uuid4().hex}",
|
|
469
|
+
seq=len(self.event_store.events(run_id)) + 1,
|
|
470
|
+
timestamp=time.time(),
|
|
471
|
+
run_id=failed_run_id,
|
|
472
|
+
scope_id=failed_run_id,
|
|
473
|
+
source=SourceRef(framework="ksadk"),
|
|
474
|
+
status="failed",
|
|
475
|
+
error=ErrorInfo(
|
|
476
|
+
code="RUNTIME_RUN_FAILED",
|
|
477
|
+
message=str(exc),
|
|
478
|
+
source="ksadk",
|
|
479
|
+
scope_id=failed_run_id,
|
|
480
|
+
),
|
|
307
481
|
)
|
|
308
|
-
persist(failure)
|
|
482
|
+
await persist(failure)
|
|
309
483
|
finally:
|
|
310
484
|
if handle is not None and self.executor.is_attached(handle):
|
|
311
485
|
try:
|
|
@@ -326,14 +500,385 @@ class StudioRunService:
|
|
|
326
500
|
record.duration_ms = int((time.monotonic() - started) * 1000)
|
|
327
501
|
record.duration_source = "studio"
|
|
328
502
|
self.event_store.save(record)
|
|
503
|
+
await self._sync_trace(record)
|
|
329
504
|
return record
|
|
330
505
|
|
|
506
|
+
async def _sync_trace(self, record: RunRecord) -> None:
|
|
507
|
+
self.event_store.trace_store.sync(record, await self.events(record.id))
|
|
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
|
+
]
|
|
732
|
+
|
|
733
|
+
async def _kernel_run(
|
|
734
|
+
self,
|
|
735
|
+
spec: StudioRunSpec,
|
|
736
|
+
user_input: str,
|
|
737
|
+
*,
|
|
738
|
+
record: RunRecord,
|
|
739
|
+
on_event: Callable[[RunEvent], None] | None,
|
|
740
|
+
kernel_runtime: Any,
|
|
741
|
+
) -> RunRecord:
|
|
742
|
+
"""kernel 路径(灰度 opt-in):Studio run -> AgentControlCommand -> receipt。
|
|
743
|
+
|
|
744
|
+
mutation 只走 kernel.submit;Studio RunEvent shape 保留,cursor 源自
|
|
745
|
+
同一 Session seq(SessionEventSubscription.after_seq)。
|
|
746
|
+
"""
|
|
747
|
+
from ksadk.kernel import ingress as _kernel_ingress
|
|
748
|
+
|
|
749
|
+
started = time.monotonic()
|
|
750
|
+
record.status = RunStatus.RUNNING
|
|
751
|
+
record.started_at = datetime.now(timezone.utc)
|
|
752
|
+
self.event_store.save(record)
|
|
753
|
+
try:
|
|
754
|
+
trusted = _kernel_ingress.trusted_context(
|
|
755
|
+
source_kind="studio",
|
|
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),
|
|
764
|
+
session_id=record.session_id,
|
|
765
|
+
operations=("enqueue",),
|
|
766
|
+
)
|
|
767
|
+
idempotency_key = str(
|
|
768
|
+
(spec.request_config or {}).get("idempotency_key") or record.id
|
|
769
|
+
)
|
|
770
|
+
command = _kernel_ingress.map_studio_request(
|
|
771
|
+
session_id=record.session_id,
|
|
772
|
+
idempotency_key=idempotency_key,
|
|
773
|
+
content=user_input,
|
|
774
|
+
run_id=record.id,
|
|
775
|
+
trusted=trusted,
|
|
776
|
+
)
|
|
777
|
+
receipt = await _kernel_ingress.submit_command(
|
|
778
|
+
command, permit=trusted.permit
|
|
779
|
+
)
|
|
780
|
+
if receipt.status not in ("accepted", "duplicate"):
|
|
781
|
+
record.status = RunStatus.FAILED
|
|
782
|
+
record.error = {
|
|
783
|
+
"code": "kernel_command_rejected",
|
|
784
|
+
"message": f"agent kernel rejected command: {receipt.status}",
|
|
785
|
+
}
|
|
786
|
+
self.event_store.save(record)
|
|
787
|
+
return record
|
|
788
|
+
output_text = ""
|
|
789
|
+
async for _seq, projected in _kernel_ingress.subscribe_projected(
|
|
790
|
+
record.session_id,
|
|
791
|
+
trusted=trusted,
|
|
792
|
+
after_seq=int(receipt.accepted_seq or 0),
|
|
793
|
+
projector=_studio_envelope_projection,
|
|
794
|
+
):
|
|
795
|
+
if projected is None:
|
|
796
|
+
continue
|
|
797
|
+
event_type, data = projected
|
|
798
|
+
if event_type == "message.delta":
|
|
799
|
+
output_text += str(data.get("delta") or "")
|
|
800
|
+
elif event_type == "run.completed":
|
|
801
|
+
output_text = str(data.get("output_text") or output_text)
|
|
802
|
+
stored = self.event_store.append(record.id, event_type, data)
|
|
803
|
+
if on_event is not None:
|
|
804
|
+
on_event(stored)
|
|
805
|
+
record.output = output_text
|
|
806
|
+
record.status = RunStatus.COMPLETED
|
|
807
|
+
record.completed_at = datetime.now(timezone.utc)
|
|
808
|
+
record.duration_ms = int((time.monotonic() - started) * 1000)
|
|
809
|
+
self.event_store.save(record)
|
|
810
|
+
await self._sync_trace(record)
|
|
811
|
+
return record
|
|
812
|
+
|
|
813
|
+
except Exception as exc: # noqa: BLE001
|
|
814
|
+
logger.exception("Studio kernel ingress failed for run %s", record.id)
|
|
815
|
+
record.status = RunStatus.FAILED
|
|
816
|
+
record.error = {
|
|
817
|
+
"code": "kernel_ingress_failed",
|
|
818
|
+
"message": str(exc),
|
|
819
|
+
}
|
|
820
|
+
record.completed_at = datetime.now(timezone.utc)
|
|
821
|
+
record.duration_ms = int((time.monotonic() - started) * 1000)
|
|
822
|
+
self.event_store.save(record)
|
|
823
|
+
await self._sync_trace(record)
|
|
824
|
+
return record
|
|
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
|
+
|
|
331
876
|
async def cancel_run(self, run_id: str) -> dict[str, str]:
|
|
332
877
|
"""Request cancellation; the flag and executor perform the actual stop."""
|
|
333
878
|
self._cancel_flags[run_id] = True
|
|
334
879
|
queue = self._control_queues.get(run_id)
|
|
335
880
|
if queue is not None:
|
|
336
|
-
queue.put_nowait(("cancel", None))
|
|
881
|
+
queue.put_nowait(("cancel", None, None))
|
|
337
882
|
handle = self._active_handles.get(run_id)
|
|
338
883
|
if handle is not None and self.executor.is_attached(handle):
|
|
339
884
|
try:
|
|
@@ -384,7 +929,7 @@ class StudioRunService:
|
|
|
384
929
|
"Studio 进程已重启,当前暂停点无法恢复",
|
|
385
930
|
status_code=409,
|
|
386
931
|
)
|
|
387
|
-
queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行")))
|
|
932
|
+
queue.put_nowait(("resume", ResumePayload(kind="free_text", data="继续运行"), None))
|
|
388
933
|
return {"runId": run_id, "status": "resuming"}
|
|
389
934
|
|
|
390
935
|
async def submit_interaction(
|
|
@@ -394,28 +939,76 @@ class StudioRunService:
|
|
|
394
939
|
*,
|
|
395
940
|
name: str,
|
|
396
941
|
data: dict[str, Any] | None = None,
|
|
942
|
+
expected_revision: int,
|
|
943
|
+
idempotency_key: str,
|
|
397
944
|
) -> dict[str, Any]:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
"
|
|
403
|
-
|
|
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,
|
|
404
964
|
)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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 {})
|
|
414
1007
|
|
|
415
1008
|
interaction = next(
|
|
416
1009
|
(
|
|
417
1010
|
event
|
|
418
|
-
for event in reversed(
|
|
1011
|
+
for event in reversed(events)
|
|
419
1012
|
if event.type == "a2ui.interaction"
|
|
420
1013
|
and str(event.data.get("interactionId") or event.data.get("interaction_id") or "")
|
|
421
1014
|
== interaction_id
|
|
@@ -424,8 +1017,29 @@ class StudioRunService:
|
|
|
424
1017
|
)
|
|
425
1018
|
if interaction is None:
|
|
426
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
|
+
)
|
|
427
1041
|
kind = str(interaction.data.get("kind") or "form")
|
|
428
|
-
payload_data = {"decision": name, **
|
|
1042
|
+
payload_data = {"decision": name, **data}
|
|
429
1043
|
payload = ResumePayload(
|
|
430
1044
|
kind="approval_decision" if kind == "approval" else "hitl_answer",
|
|
431
1045
|
call_id=interaction_id,
|
|
@@ -438,7 +1052,7 @@ class StudioRunService:
|
|
|
438
1052
|
raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
|
|
439
1053
|
try:
|
|
440
1054
|
await self.executor.submit(handle, payload)
|
|
441
|
-
except
|
|
1055
|
+
except Exception as exc: # noqa: BLE001 - provider errors are user-safe here
|
|
442
1056
|
raise StudioError(
|
|
443
1057
|
"INTERACTION_SUBMIT_FAILED",
|
|
444
1058
|
str(exc),
|
|
@@ -448,61 +1062,152 @@ class StudioRunService:
|
|
|
448
1062
|
queue = self._control_queues.get(run_id)
|
|
449
1063
|
if queue is None:
|
|
450
1064
|
raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
|
|
451
|
-
|
|
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
|
|
452
1075
|
else:
|
|
453
1076
|
raise StudioError("INTERACTION_EXPIRED", "运行时交互已失效", status_code=409)
|
|
454
1077
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
"
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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(
|
|
467
1104
|
run_id,
|
|
468
|
-
"
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"surfaceId": str(
|
|
472
|
-
interaction.data.get("surfaceId") or interaction.data.get("surface_id") or ""
|
|
473
|
-
),
|
|
474
|
-
"interactionId": interaction_id,
|
|
475
|
-
"actionId": f"action-{interaction_id}",
|
|
476
|
-
"name": name,
|
|
477
|
-
"data": dict(data or {}),
|
|
478
|
-
},
|
|
1105
|
+
resolved_type=("approval.resolved" if kind == "approval" else "interaction.resolved"),
|
|
1106
|
+
resolved_data=resolved_data,
|
|
1107
|
+
action_data=action_data,
|
|
479
1108
|
)
|
|
480
1109
|
record.status = RunStatus.RUNNING
|
|
481
1110
|
self.event_store.save(record)
|
|
482
1111
|
self._waiting_modes.pop(run_id, None)
|
|
483
|
-
return
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1112
|
+
return receipt
|
|
1113
|
+
|
|
1114
|
+
async def _capture_pcm_evidence(
|
|
1115
|
+
self,
|
|
1116
|
+
record: RunRecord,
|
|
1117
|
+
spec: StudioRunSpec,
|
|
1118
|
+
user_input: str,
|
|
1119
|
+
) -> Any | None:
|
|
1120
|
+
"""Prepare once and persist the exact prompt/context input sent to Runtime."""
|
|
1121
|
+
|
|
1122
|
+
try:
|
|
1123
|
+
from ksadk.conversations.runtime_preparation import build_run_input
|
|
1124
|
+
from ksadk.sessions.in_memory import InMemorySessionService
|
|
1125
|
+
|
|
1126
|
+
temporary_sessions = InMemorySessionService()
|
|
1127
|
+
await temporary_sessions.create_session(
|
|
1128
|
+
agent_id=spec.agent_id,
|
|
1129
|
+
user_id="local-user",
|
|
1130
|
+
session_id=record.session_id,
|
|
1131
|
+
)
|
|
1132
|
+
config = dict(spec.request_config or {})
|
|
1133
|
+
prepared = await build_run_input(
|
|
1134
|
+
agent_id=spec.agent_id,
|
|
1135
|
+
user_id="local-user",
|
|
1136
|
+
session_id=record.session_id,
|
|
1137
|
+
messages=self._conversation_messages(
|
|
1138
|
+
spec.agent_id,
|
|
1139
|
+
record.session_id,
|
|
1140
|
+
user_input,
|
|
1141
|
+
),
|
|
1142
|
+
model=spec.model,
|
|
1143
|
+
instructions=str(config.get("instructions") or ""),
|
|
1144
|
+
agent_system=str(config.get("agent_system") or ""),
|
|
1145
|
+
agent_task=str(config.get("agent_task") or ""),
|
|
1146
|
+
prompt_integration_mode=str(config.get("prompt_integration_mode") or ""),
|
|
1147
|
+
context_engine_rollout=str(config.get("context_engine_rollout") or "") or None,
|
|
1148
|
+
memory_recall_enabled=config.get("memory_recall_enabled"),
|
|
1149
|
+
memory_write_rollout=str(config.get("memory_write_rollout") or "") or None,
|
|
1150
|
+
memory_enabled=config.get("memory_enabled"),
|
|
1151
|
+
memory_write_mode=str(config.get("memory_write_mode") or "candidate"),
|
|
1152
|
+
flush_before_compaction=bool(config.get("flush_before_compaction", True)),
|
|
1153
|
+
provider_ref=str(config.get("provider_ref") or "local-default"),
|
|
1154
|
+
runtime_type=spec.launch_context.runtime_type,
|
|
1155
|
+
deployment_mode=spec.launch_context.deployment_mode,
|
|
1156
|
+
invocation_id=record.id,
|
|
1157
|
+
session_service_provider=lambda: temporary_sessions,
|
|
1158
|
+
agent_max_input_tokens=config.get("max_input_tokens"),
|
|
1159
|
+
agent_reserve_output_tokens=config.get("reserve_output_tokens"),
|
|
1160
|
+
)
|
|
1161
|
+
record.context_plan = prepared.context_plan
|
|
1162
|
+
compiled = prepared.compiled_prompt
|
|
1163
|
+
shadow = prepared.shadow_context_plan or {}
|
|
1164
|
+
record.prompt_evidence = {
|
|
1165
|
+
"contentHash": (compiled or {}).get("prompt_content_hash"),
|
|
1166
|
+
"stablePrefixHash": (compiled or {}).get("prompt_stable_prefix_hash"),
|
|
1167
|
+
"sectionHashes": (compiled or {}).get("prompt_section_hashes", {}),
|
|
1168
|
+
"tokensBySection": (compiled or {}).get("prompt_tokens_by_section", {}),
|
|
1169
|
+
"estimatedTokens": (compiled or {}).get("prompt_estimated_tokens"),
|
|
1170
|
+
"sectionCount": (compiled or {}).get("prompt_section_count"),
|
|
1171
|
+
"integrationMode": shadow.get("integration_mode")
|
|
1172
|
+
or config.get("prompt_integration_mode")
|
|
1173
|
+
or "native",
|
|
1174
|
+
"accountingAccuracy": shadow.get("accounting_accuracy") or "opaque",
|
|
1175
|
+
"promptOwner": shadow.get("prompt_owner") or "runtime",
|
|
1176
|
+
"runtimeType": shadow.get("runtime_type") or spec.launch_context.runtime_type,
|
|
1177
|
+
"deploymentMode": shadow.get("deployment_mode")
|
|
1178
|
+
or spec.launch_context.deployment_mode,
|
|
1179
|
+
"capabilityHash": shadow.get("capability_hash"),
|
|
1180
|
+
"tokensByKind": shadow.get("tokens_by_kind", {}),
|
|
1181
|
+
"plannedInputTokens": shadow.get("planned_input_tokens"),
|
|
1182
|
+
}
|
|
1183
|
+
record.working_state = prepared.working_state
|
|
1184
|
+
for event in getattr(prepared, "memory_recall_events", []):
|
|
1185
|
+
self.event_store.append(
|
|
1186
|
+
record.id,
|
|
1187
|
+
event.get("type", "memory.recall.event"),
|
|
1188
|
+
event,
|
|
1189
|
+
)
|
|
1190
|
+
self.event_store.save(record)
|
|
1191
|
+
return prepared
|
|
1192
|
+
except Exception: # noqa: BLE001 - evidence collection is best effort
|
|
1193
|
+
logger.exception("Studio PCM evidence capture failed for run %s", record.id)
|
|
1194
|
+
return None
|
|
490
1195
|
|
|
491
1196
|
def _persist_approval_surface(
|
|
492
1197
|
self,
|
|
493
1198
|
record: RunRecord,
|
|
494
|
-
event:
|
|
1199
|
+
event: InteractionRequested,
|
|
495
1200
|
*,
|
|
496
1201
|
on_event: Callable[[RunEvent], None] | None,
|
|
497
1202
|
) -> None:
|
|
498
|
-
approval_id =
|
|
1203
|
+
approval_id = event.interaction_id
|
|
499
1204
|
if not approval_id:
|
|
500
1205
|
return
|
|
501
1206
|
surface_id = f"approval-{approval_id}"
|
|
502
|
-
|
|
503
|
-
detail =
|
|
1207
|
+
detail_value = event.request.detail
|
|
1208
|
+
detail = detail_value if isinstance(detail_value, dict) else {}
|
|
504
1209
|
command = str(detail.get("command") or detail.get("reason") or "")
|
|
505
|
-
kind = str(event.
|
|
1210
|
+
kind = str(event.request.kind or "tool")
|
|
506
1211
|
components = [
|
|
507
1212
|
{
|
|
508
1213
|
"id": "root",
|
|
@@ -551,6 +1256,7 @@ class StudioRunService:
|
|
|
551
1256
|
"surfaceId": surface_id,
|
|
552
1257
|
"interactionId": approval_id,
|
|
553
1258
|
"kind": "approval",
|
|
1259
|
+
"revision": 1,
|
|
554
1260
|
"inputSchema": {
|
|
555
1261
|
"type": "object",
|
|
556
1262
|
"properties": {
|
|
@@ -601,107 +1307,465 @@ class StudioRunService:
|
|
|
601
1307
|
return {}
|
|
602
1308
|
|
|
603
1309
|
|
|
604
|
-
def
|
|
605
|
-
|
|
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
|
+
}
|
|
606
1328
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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",
|
|
619
1365
|
)
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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]]:
|
|
1400
|
+
"""Project the canonical RuntimeEvent into Studio's persisted event view.
|
|
1401
|
+
|
|
1402
|
+
公开承诺字段(契约声明见 ``ksadk/events/projections.py``,执行形态为
|
|
1403
|
+
``tests/protocol/test_cross_projection_golden.py``):
|
|
1404
|
+
- 所有事件 payload 必含 ``runId``/``scopeId``;
|
|
1405
|
+
- ``message.*``/``thinking.*`` 含 ``itemId``(delta 另含 ``partId``);
|
|
1406
|
+
- ``tool.*``/``command.*``/``approval.*``/``a2ui.*`` 含 ``itemId``;
|
|
1407
|
+
- ``a2ui.surface.*`` 含 ``surfaceId`` 与 ``a2uiOperations`` 列表。
|
|
1408
|
+
|
|
1409
|
+
内部不保证字段:除上述外的 payload 附加键、事件类型枚举的完备性
|
|
1410
|
+
(新增 canonical 事件类型在未适配前可能整条丢弃)。
|
|
1411
|
+
"""
|
|
1412
|
+
|
|
1413
|
+
if isinstance(event, RunStarted):
|
|
1414
|
+
projected = "run.started"
|
|
1415
|
+
payload: dict[str, Any] = {}
|
|
1416
|
+
elif isinstance(event, RunProgress):
|
|
1417
|
+
projected = "run.progress"
|
|
1418
|
+
payload = {"progress": event.progress, "message": event.message}
|
|
1419
|
+
elif isinstance(event, RunCompleted):
|
|
1420
|
+
projected = "run.completed"
|
|
1421
|
+
payload = {}
|
|
1422
|
+
elif isinstance(event, RunFailed):
|
|
1423
|
+
projected = "run.failed"
|
|
1424
|
+
payload = {"error": event.error.message or ""}
|
|
1425
|
+
elif isinstance(event, RunCanceled):
|
|
1426
|
+
projected = "run.cancelled"
|
|
1427
|
+
payload = {"reason": event.reason or ""}
|
|
1428
|
+
elif isinstance(event, RunInterrupted):
|
|
1429
|
+
projected = "run.paused" if event.reason == "user_pause" else "run.interrupted"
|
|
1430
|
+
payload = {"reason": event.reason or ""}
|
|
1431
|
+
elif isinstance(event, ItemUpdated) and event.item_kind in {"message", "reasoning"}:
|
|
1432
|
+
text = event.update.text if isinstance(event.update, TextContent) else ""
|
|
1433
|
+
is_thinking = event.item_kind == "reasoning"
|
|
1434
|
+
projected = "thinking.delta" if is_thinking else "message.delta"
|
|
1435
|
+
payload = {"text": text}
|
|
1436
|
+
elif isinstance(event, ItemCompleted) and event.item_kind in {"message", "reasoning"}:
|
|
1437
|
+
text = ""
|
|
1438
|
+
for part in event.snapshot.parts:
|
|
1439
|
+
if isinstance(part, TextContent):
|
|
1440
|
+
text = part.text
|
|
1441
|
+
break
|
|
1442
|
+
is_thinking = event.item_kind == "reasoning"
|
|
1443
|
+
projected = "thinking.completed" if is_thinking else "message.completed"
|
|
1444
|
+
payload = {"text": text}
|
|
1445
|
+
elif isinstance(event, ItemStarted) and event.item_kind == "tool_call":
|
|
1446
|
+
tool_part = _first_content(event.initial, ToolCallContent) if event.initial else None
|
|
1447
|
+
if tool_part is not None and tool_part.name == "codex.command":
|
|
1448
|
+
projected = "command.started"
|
|
1449
|
+
args = tool_part.arguments if isinstance(tool_part.arguments, dict) else {}
|
|
629
1450
|
payload = {
|
|
630
|
-
"callId":
|
|
1451
|
+
"callId": tool_part.call_id,
|
|
631
1452
|
"command": str(args.get("command") or ""),
|
|
632
1453
|
"cwd": str(args.get("cwd") or ""),
|
|
633
1454
|
"commandActions": args.get("command_actions") or [],
|
|
634
1455
|
}
|
|
635
|
-
|
|
1456
|
+
elif tool_part is not None:
|
|
1457
|
+
projected = "tool.started"
|
|
636
1458
|
payload = {
|
|
637
|
-
"callId":
|
|
638
|
-
"tool":
|
|
639
|
-
"args":
|
|
1459
|
+
"callId": tool_part.call_id,
|
|
1460
|
+
"tool": tool_part.name,
|
|
1461
|
+
"args": tool_part.arguments,
|
|
640
1462
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1463
|
+
else:
|
|
1464
|
+
projected = "tool.started"
|
|
1465
|
+
payload = {}
|
|
1466
|
+
elif isinstance(event, ItemCompleted) and event.item_kind == "tool_call":
|
|
1467
|
+
tool_call = _first_content(event.snapshot, ToolCallContent)
|
|
1468
|
+
tool_result = _first_content(event.snapshot, ToolResultContent)
|
|
1469
|
+
if tool_call is not None and tool_call.name == "codex.command":
|
|
1470
|
+
projected = "command.completed"
|
|
1471
|
+
result = (
|
|
1472
|
+
tool_result.result
|
|
1473
|
+
if tool_result is not None and isinstance(tool_result.result, dict)
|
|
1474
|
+
else {}
|
|
1475
|
+
)
|
|
648
1476
|
payload = {
|
|
649
|
-
"callId":
|
|
1477
|
+
"callId": tool_call.call_id,
|
|
650
1478
|
"status": str(result.get("status") or ""),
|
|
651
1479
|
"exitCode": result.get("exit_code"),
|
|
652
1480
|
"durationMs": result.get("duration_ms"),
|
|
653
1481
|
"output": str(result.get("output") or ""),
|
|
654
1482
|
}
|
|
655
1483
|
else:
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1484
|
+
projected = "tool.completed"
|
|
1485
|
+
result = (
|
|
1486
|
+
tool_result.result
|
|
1487
|
+
if tool_result is not None and isinstance(tool_result.result, dict)
|
|
1488
|
+
else {}
|
|
659
1489
|
)
|
|
660
|
-
tool_status = str(
|
|
661
|
-
if tool_result.
|
|
1490
|
+
tool_status = str(result.get("status") or "")
|
|
1491
|
+
if tool_result is not None and tool_result.is_error:
|
|
662
1492
|
tool_status = "failed"
|
|
1493
|
+
call_id = (tool_call or tool_result).call_id if (tool_call or tool_result) else ""
|
|
663
1494
|
payload = {
|
|
664
|
-
"callId":
|
|
665
|
-
"tool":
|
|
1495
|
+
"callId": call_id,
|
|
1496
|
+
"tool": tool_call.name if tool_call is not None else "",
|
|
666
1497
|
"status": tool_status,
|
|
667
|
-
"durationMs":
|
|
668
|
-
"output": str(
|
|
669
|
-
**({"error": str(tool_result["error"])} if tool_result.get("error") else {}),
|
|
1498
|
+
"durationMs": result.get("duration_ms"),
|
|
1499
|
+
"output": str(result.get("output") or ""),
|
|
670
1500
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
elif
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
projected = (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1501
|
+
if tool_result is not None and tool_result.is_error:
|
|
1502
|
+
payload["error"] = str(result.get("error") or "")
|
|
1503
|
+
elif (
|
|
1504
|
+
isinstance(event, (ItemStarted, ItemUpdated, ItemCompleted))
|
|
1505
|
+
and event.item_kind == "data"
|
|
1506
|
+
and event.source.protocol == "a2ui"
|
|
1507
|
+
):
|
|
1508
|
+
projected, payload = _project_a2ui_surface(event)
|
|
1509
|
+
elif isinstance(event, InteractionRequested):
|
|
1510
|
+
if event.interaction_kind == "approval":
|
|
1511
|
+
projected = "approval.requested"
|
|
1512
|
+
payload = {
|
|
1513
|
+
"approvalId": event.interaction_id,
|
|
1514
|
+
"callId": event.request.call_id or "",
|
|
1515
|
+
"kind": event.request.kind,
|
|
1516
|
+
"detail": event.request.detail,
|
|
1517
|
+
}
|
|
1518
|
+
else:
|
|
1519
|
+
projected = "a2ui.interaction"
|
|
1520
|
+
payload = {
|
|
1521
|
+
"interactionId": event.interaction_id,
|
|
1522
|
+
"kind": "form",
|
|
1523
|
+
"inputSchema": {},
|
|
1524
|
+
}
|
|
1525
|
+
elif isinstance(event, InteractionResolved):
|
|
1526
|
+
if event.interaction_kind == "approval":
|
|
1527
|
+
projected = "approval.resolved"
|
|
1528
|
+
payload = {
|
|
1529
|
+
"approvalId": event.interaction_id,
|
|
1530
|
+
"callId": "",
|
|
1531
|
+
"decision": "",
|
|
1532
|
+
}
|
|
1533
|
+
else:
|
|
1534
|
+
projected = "a2ui.action"
|
|
1535
|
+
payload = {"interactionId": event.interaction_id}
|
|
1536
|
+
elif isinstance(event, ContinuationCreated):
|
|
1537
|
+
projected = "checkpoint.created"
|
|
1538
|
+
payload = {
|
|
1539
|
+
"checkpointId": event.continuation_id,
|
|
1540
|
+
"granularity": event.ref.get("granularity", "snapshot"),
|
|
1541
|
+
"resumable": event.resumable,
|
|
1542
|
+
}
|
|
1543
|
+
elif isinstance(event, ContinuationResumed):
|
|
1544
|
+
projected = "checkpoint.resumed"
|
|
1545
|
+
payload = {
|
|
1546
|
+
"checkpointId": event.continuation_id,
|
|
1547
|
+
"resumeAttemptId": event.resume_attempt_id,
|
|
1548
|
+
}
|
|
1549
|
+
elif isinstance(event, UsageReported):
|
|
1550
|
+
projected = "usage.reported"
|
|
1551
|
+
payload = {
|
|
1552
|
+
"inputTokens": event.input_tokens,
|
|
1553
|
+
"outputTokens": event.output_tokens,
|
|
1554
|
+
"totalTokens": event.total_tokens,
|
|
1555
|
+
"cachedTokens": event.cached_tokens,
|
|
1556
|
+
"reasoningTokens": event.reasoning_tokens,
|
|
1557
|
+
}
|
|
1558
|
+
else:
|
|
1559
|
+
projected = event.event_type
|
|
1560
|
+
payload = {}
|
|
1561
|
+
|
|
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
|
|
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")
|
|
1575
|
+
return projected, payload
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
def _first_content(
|
|
1579
|
+
snapshot: ContentSnapshot | None, content_type: type
|
|
1580
|
+
) -> Any | None:
|
|
1581
|
+
if snapshot is None:
|
|
1582
|
+
return None
|
|
1583
|
+
for part in snapshot.parts:
|
|
1584
|
+
if isinstance(part, content_type):
|
|
1585
|
+
return part
|
|
1586
|
+
return None
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def _project_a2ui_surface(
|
|
1590
|
+
event: ItemStarted | ItemUpdated | ItemCompleted,
|
|
1591
|
+
) -> tuple[str, dict[str, Any]]:
|
|
1592
|
+
if isinstance(event, ItemStarted):
|
|
1593
|
+
projected = "a2ui.surface.begin"
|
|
1594
|
+
data_parts = event.initial.parts if event.initial is not None else ()
|
|
1595
|
+
elif isinstance(event, ItemUpdated):
|
|
1596
|
+
projected = "a2ui.surface.update"
|
|
1597
|
+
data_parts = (event.update,) if isinstance(event.update, DataContent) else ()
|
|
701
1598
|
else:
|
|
702
|
-
projected =
|
|
703
|
-
|
|
1599
|
+
projected = "a2ui.surface.end"
|
|
1600
|
+
data_parts = event.snapshot.parts
|
|
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
|
+
|
|
1607
|
+
surface_id = str(event.source.metadata.get("surface_id") or "")
|
|
1608
|
+
operations: list[dict[str, Any]] = []
|
|
1609
|
+
for part in data_parts:
|
|
1610
|
+
if isinstance(part, DataContent):
|
|
1611
|
+
data = part.data
|
|
1612
|
+
if isinstance(data, list):
|
|
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))
|
|
1616
|
+
if not operations:
|
|
1617
|
+
operations = project_a2ui_operations(projected, {"surface_id": surface_id})
|
|
1618
|
+
payload: dict[str, Any] = {
|
|
1619
|
+
"surfaceId": surface_id,
|
|
1620
|
+
"a2uiOperations": operations,
|
|
1621
|
+
}
|
|
704
1622
|
return projected, payload
|
|
705
1623
|
|
|
706
1624
|
|
|
1625
|
+
def _attach_studio_identity(payload: dict[str, Any], event: RuntimeEvent) -> None:
|
|
1626
|
+
"""Attach §8.4 identity fields (runId/scopeId/itemId/partId/operation)."""
|
|
1627
|
+
payload["runId"] = event.run_id
|
|
1628
|
+
payload["scopeId"] = event.scope_id
|
|
1629
|
+
if isinstance(event, (ItemStarted, ItemUpdated, ItemCompleted, ItemFailed)):
|
|
1630
|
+
payload["itemId"] = event.item_id
|
|
1631
|
+
if isinstance(event, ItemUpdated):
|
|
1632
|
+
payload["operation"] = event.op
|
|
1633
|
+
if isinstance(event, ItemUpdated) and hasattr(event.update, "part_id"):
|
|
1634
|
+
payload["partId"] = event.update.part_id
|
|
1635
|
+
if isinstance(event, ItemStarted) and event.initial is not None:
|
|
1636
|
+
for part in event.initial.parts:
|
|
1637
|
+
if hasattr(part, "part_id"):
|
|
1638
|
+
payload["partId"] = part.part_id
|
|
1639
|
+
break
|
|
1640
|
+
if isinstance(event, ItemCompleted):
|
|
1641
|
+
for part in event.snapshot.parts:
|
|
1642
|
+
if hasattr(part, "part_id"):
|
|
1643
|
+
payload["partId"] = part.part_id
|
|
1644
|
+
break
|
|
1645
|
+
if isinstance(event, (InteractionRequested, InteractionResolved)):
|
|
1646
|
+
payload["itemId"] = event.interaction_id
|
|
1647
|
+
if isinstance(event, (ContinuationCreated, ContinuationResumed)):
|
|
1648
|
+
payload["itemId"] = event.continuation_id
|
|
1649
|
+
|
|
1650
|
+
|
|
707
1651
|
__all__ = ["StudioRunService", "StudioRunSpec", "project_runtime_event"]
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def _studio_envelope_projection(envelope) -> tuple[str, dict[str, Any]] | None:
|
|
1655
|
+
"""Session envelope -> Studio RunEvent 投影;cursor 仍用 envelope.seq。"""
|
|
1656
|
+
|
|
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
|
|
1696
|
+
if envelope.event_type == "run.completed":
|
|
1697
|
+
return "run.completed", {
|
|
1698
|
+
"runId": envelope.run_id or "",
|
|
1699
|
+
"output_text": str(payload.get("output_text") or ""),
|
|
1700
|
+
}
|
|
1701
|
+
text = str(payload.get("delta") or payload.get("text") or "")
|
|
1702
|
+
if text:
|
|
1703
|
+
return "message.delta", {"delta": text}
|
|
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
|