agentengine-sdk-python 0.8.0__py3-none-any.whl → 0.8.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/METADATA +43 -7
- agentengine_sdk_python-0.8.2.dist-info/RECORD +778 -0
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/WHEEL +1 -1
- ksadk/a2a/__init__.py +2 -0
- ksadk/a2a/_space_client_events.py +381 -0
- ksadk/a2a/bootstrap.py +0 -2
- ksadk/a2a/card.py +6 -111
- ksadk/a2a/control_plane.py +239 -1
- ksadk/a2a/event_adapter.py +156 -64
- ksadk/a2a/executor.py +195 -172
- ksadk/a2a/routes.py +12 -5
- ksadk/a2a/server.py +1 -8
- ksadk/a2a/service_env.py +33 -0
- ksadk/a2a/space_client.py +43 -349
- ksadk/a2a_card.py +101 -0
- ksadk/a2ui/core.py +222 -53
- ksadk/agui/_agent_helpers.py +243 -0
- ksadk/agui/a2ui_projection.py +12 -5
- ksadk/agui/agent.py +474 -275
- ksadk/agui/config.py +11 -5
- ksadk/agui/routes.py +7 -15
- ksadk/api/client.py +530 -40
- ksadk/api/environment.py +27 -0
- ksadk/builders/code_builder.py +352 -53
- ksadk/builders/container_builder.py +71 -44
- ksadk/builders/framework_requirements.py +2 -1
- ksadk/builders/ks3_uploader.py +2 -2
- ksadk/builders/managed_runtime_builder.py +60 -28
- ksadk/cli/__init__.py +25 -0
- ksadk/cli/cmd_a2a.py +18 -31
- ksadk/cli/cmd_create.py +13 -26
- ksadk/cli/cmd_dashboard.py +15 -1
- ksadk/cli/cmd_deploy.py +28 -3
- ksadk/cli/cmd_eval.py +443 -0
- ksadk/cli/cmd_evalset.py +284 -0
- ksadk/cli/cmd_files.py +5 -1
- ksadk/cli/cmd_hermes.py +137 -163
- ksadk/cli/cmd_invoke.py +28 -38
- ksadk/cli/cmd_launch.py +12 -0
- ksadk/cli/cmd_managed_runtime.py +95 -0
- ksadk/cli/cmd_observe.py +85 -0
- ksadk/cli/cmd_openclaw.py +85 -479
- ksadk/cli/cmd_replay.py +28 -4
- ksadk/cli/cmd_run.py +29 -67
- ksadk/cli/cmd_studio.py +201 -0
- ksadk/cli/cmd_web.py +15 -6
- ksadk/cli/dry_run.py +2 -1
- ksadk/cli/env_options.py +83 -0
- ksadk/cli/global_options.py +3 -1
- ksadk/cli/hermes_env.py +151 -0
- ksadk/cli/invoke_payload.py +49 -0
- ksadk/cli/network_options.py +7 -5
- ksadk/cli/openclaw_env.py +458 -0
- ksadk/cli/runtime_bootstrap.py +51 -0
- ksadk/cli/ui.py +6 -0
- ksadk/cli/workflow_common.py +5 -5
- ksadk/codex/__init__.py +3 -3
- ksadk/codex/client.py +833 -49
- ksadk/codex/phase.py +1 -1
- ksadk/codex/runtime.py +720 -127
- ksadk/common/kop_client.py +232 -0
- ksadk/configs/__init__.py +1 -3
- ksadk/configs/env_registry.py +244 -45
- ksadk/configs/env_registry_pcm.py +181 -0
- ksadk/configs/env_var_spec.py +18 -0
- ksadk/configs/global_config.py +6 -1
- ksadk/configs/settings.py +5 -108
- ksadk/context_engine/__init__.py +98 -0
- ksadk/context_engine/assembler.py +176 -0
- ksadk/context_engine/baseline.py +419 -0
- ksadk/context_engine/cache_observability.py +228 -0
- ksadk/context_engine/capabilities.py +443 -0
- ksadk/context_engine/contributors.py +344 -0
- ksadk/context_engine/hosted_pipeline.py +417 -0
- ksadk/context_engine/models.py +117 -0
- ksadk/context_engine/planner.py +698 -0
- ksadk/context_engine/policies.py +319 -0
- ksadk/context_engine/projection.py +31 -0
- ksadk/context_engine/shadow_plan.py +266 -0
- ksadk/context_engine/tokenizer.py +164 -0
- ksadk/conversations/__init__.py +85 -94
- ksadk/conversations/context.py +126 -6
- ksadk/conversations/message_projection.py +292 -28
- ksadk/conversations/model_context.py +49 -3
- ksadk/conversations/runtime_compaction.py +207 -5
- ksadk/conversations/runtime_input.py +193 -3
- ksadk/conversations/runtime_invocation.py +81 -3
- ksadk/conversations/runtime_observability.py +157 -4
- ksadk/conversations/runtime_payloads.py +42 -0
- ksadk/conversations/runtime_preparation.py +280 -2
- ksadk/conversations/runtime_resume.py +71 -32
- ksadk/conversations/runtime_stream_events.py +94 -9
- ksadk/conversations/runtime_streaming.py +175 -22
- ksadk/conversations/semantic_summary.py +311 -1
- ksadk/conversations/session_lock.py +51 -0
- ksadk/deployment/env_forward.py +72 -0
- ksadk/deployment/managed_runtime.py +38 -0
- ksadk/deployment/manager.py +24 -8
- ksadk/deployment/providers/serverless.py +228 -92
- ksadk/evaluation/__init__.py +116 -0
- ksadk/evaluation/a2a_adapter.py +452 -0
- ksadk/evaluation/adapters.py +67 -0
- ksadk/evaluation/agent_eval_client.py +395 -0
- ksadk/evaluation/cloud_binding.py +80 -0
- ksadk/evaluation/cloud_converter.py +194 -0
- ksadk/evaluation/cloud_service.py +199 -0
- ksadk/evaluation/contracts.py +478 -0
- ksadk/evaluation/evalset.py +351 -0
- ksadk/evaluation/evaluators.py +627 -0
- ksadk/evaluation/evidence.py +258 -0
- ksadk/evaluation/executor.py +143 -0
- ksadk/evaluation/local_adapter.py +550 -0
- ksadk/evaluation/service_env.py +27 -0
- ksadk/evaluation/storage.py +68 -0
- ksadk/evaluation/studio_build_adapter.py +253 -0
- ksadk/evaluation/target.py +82 -0
- ksadk/events/__init__.py +13 -13
- ksadk/events/_v1_compat/__init__.py +1 -0
- ksadk/events/_v1_compat/models.py +299 -0
- ksadk/events/_v1_compat/parser.py +247 -0
- ksadk/events/_v1_compat/projection.py +753 -0
- ksadk/events/adapters/__init__.py +34 -0
- ksadk/events/adapters/_a2a_snapshot.py +622 -0
- ksadk/events/adapters/_a2a_support.py +275 -0
- ksadk/events/adapters/_codex_interactions.py +375 -0
- ksadk/events/adapters/_codex_items.py +801 -0
- ksadk/events/adapters/_codex_validators.py +311 -0
- ksadk/events/adapters/_langgraph_support.py +785 -0
- ksadk/events/adapters/a2a.py +841 -0
- ksadk/events/adapters/adk.py +503 -0
- ksadk/events/adapters/codex.py +717 -0
- ksadk/events/adapters/langgraph.py +745 -0
- ksadk/events/canonical.py +440 -0
- ksadk/events/canonical_replay.py +497 -0
- ksadk/events/canonical_store.py +468 -0
- ksadk/events/cold_recovery.py +257 -0
- ksadk/events/content.py +89 -0
- ksadk/events/identity.py +86 -0
- ksadk/events/pipeline.py +455 -0
- ksadk/events/projections.py +66 -0
- ksadk/events/reducer.py +661 -0
- ksadk/events/replay.py +3 -36
- ksadk/events/runtime_event.py +20 -256
- ksadk/events/session_event.py +324 -0
- ksadk/events/store.py +3 -316
- ksadk/events/v1_compat.py +41 -0
- ksadk/harness/__init__.py +3 -2
- ksadk/harness/app.py +114 -123
- ksadk/harness/config.py +1 -1
- ksadk/harness/runtime.py +467 -0
- ksadk/hermes_terminal.py +5 -0
- ksadk/identity/resolver.py +4 -3
- ksadk/interaction/__init__.py +28 -0
- ksadk/interaction/contracts.py +136 -0
- ksadk/interaction/ledger.py +189 -0
- ksadk/interaction/provider.py +122 -0
- ksadk/interaction/providers/__init__.py +52 -0
- ksadk/interaction/providers/adk.py +40 -0
- ksadk/interaction/providers/codex.py +94 -0
- ksadk/interaction/providers/langgraph.py +58 -0
- ksadk/kernel/__init__.py +117 -0
- ksadk/kernel/authorization.py +258 -0
- ksadk/kernel/bootstrap.py +1074 -0
- ksadk/kernel/contract_fingerprints.py +61 -0
- ksadk/kernel/contracts.py +362 -0
- ksadk/kernel/control.py +219 -0
- ksadk/kernel/errors.py +79 -0
- ksadk/kernel/ingress.py +1102 -0
- ksadk/kernel/mapping.py +111 -0
- ksadk/kernel/memory_store.py +1133 -0
- ksadk/kernel/postgres_store.py +1756 -0
- ksadk/kernel/recovery.py +452 -0
- ksadk/kernel/runtime_identity.py +103 -0
- ksadk/kernel/sql/001_agent_kernel.sql +120 -0
- ksadk/kernel/sqlite_store.py +1410 -0
- ksadk/kernel/state.py +145 -0
- ksadk/kernel/store.py +232 -0
- ksadk/kernel/worker.py +953 -0
- ksadk/knowledge_base/client.py +22 -4
- ksadk/knowledge_base/service.py +32 -5
- ksadk/managed_a2a_card.py +160 -0
- ksadk/managed_runtime.py +16 -9
- ksadk/memory/__init__.py +61 -0
- ksadk/memory/adk/backends/base_ltm_backend.py +96 -1
- ksadk/memory/adk/backends/http_ltm_backend.py +14 -5
- ksadk/memory/adk/backends/inmemory_ltm_backend.py +136 -3
- ksadk/memory/adk/backends/sdk_ltm_backend.py +394 -5
- ksadk/memory/adk/backends/sqlite_ltm_backend.py +95 -0
- ksadk/memory/adk/long_term_memory.py +3 -2
- ksadk/memory/coordinator.py +461 -0
- ksadk/memory/events.py +202 -0
- ksadk/memory/extraction.py +228 -0
- ksadk/memory/ltm_backend_factory.py +16 -3
- ksadk/memory/models.py +316 -0
- ksadk/memory/policy.py +215 -0
- ksadk/memory/provider.py +47 -0
- ksadk/memory/provider_adapter.py +159 -0
- ksadk/memory/provider_resolver.py +71 -0
- ksadk/memory/providers/__init__.py +5 -0
- ksadk/memory/providers/local_sqlite.py +490 -0
- ksadk/memory/resolved_policy.py +145 -0
- ksadk/memory/service.py +198 -8
- ksadk/memory/tool.py +4 -0
- ksadk/model_proxy/bootstrap.py +16 -9
- ksadk/model_proxy/config.py +22 -1
- ksadk/model_proxy/detect.py +202 -9
- ksadk/model_proxy/namespace.py +67 -15
- ksadk/model_proxy/server.py +168 -6
- ksadk/model_proxy/transform.py +105 -3
- ksadk/observability/__init__.py +25 -0
- ksadk/observability/session_log.py +420 -0
- ksadk/observability/trajectory.py +105 -0
- ksadk/prompts/__init__.py +71 -0
- ksadk/prompts/compiler.py +215 -0
- ksadk/prompts/models.py +82 -0
- ksadk/prompts/projection.py +89 -0
- ksadk/prompts/resolved.py +137 -0
- ksadk/prompts/sources.py +233 -0
- ksadk/runners/_langgraph_runner_streams.py +822 -0
- ksadk/runners/adk_runner.py +14 -31
- ksadk/runners/base_runner.py +44 -3
- ksadk/runners/factory.py +3 -3
- ksadk/runners/langgraph_runner.py +252 -433
- ksadk/runners/utils/__init__.py +0 -10
- ksadk/runners/utils/loader.py +16 -0
- ksadk/runtime/__init__.py +67 -41
- ksadk/runtime/_runner_adapter/__init__.py +1 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +788 -0
- ksadk/runtime/adapter.py +199 -16
- ksadk/runtime/conversation_execution.py +687 -0
- ksadk/runtime/executor.py +392 -0
- ksadk/runtime/factory.py +342 -0
- ksadk/runtime/framework_adapters.py +6 -12
- ksadk/runtime/hosted_finalizer.py +268 -0
- ksadk/runtime/launch.py +61 -0
- ksadk/runtime/preprocessing.py +49 -14
- ksadk/runtime/runner_adapter.py +330 -391
- ksadk/runtime/runner_loading.py +36 -0
- ksadk/runtime/usage.py +33 -0
- ksadk/sandbox/backends/e2b.py +36 -3
- ksadk/server/__init__.py +39 -4
- ksadk/server/app.py +18 -492
- ksadk/server/composition.py +36 -2
- ksadk/server/factory.py +210 -149
- ksadk/server/routes/common.py +15 -46
- ksadk/server/routes/control.py +13 -9
- ksadk/server/routes/kernel_ingress.py +258 -0
- ksadk/server/routes/misc.py +10 -33
- ksadk/server/routes/models.py +14 -6
- ksadk/server/routes/openai_compat.py +129 -27
- ksadk/server/routes/projection.py +84 -16
- ksadk/server/routes/run.py +347 -460
- ksadk/server/routes/sessions.py +37 -40
- ksadk/server/routes/streaming.py +52 -7
- ksadk/server/routes/workspace.py +54 -21
- ksadk/server/static/assets/{ArtifactsPanel-Cjnqt-8W.js → ArtifactsPanel-DSU1sWD_.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-BqyqZfuB.js → CodeBlock-DOH6MVcn.js} +4 -4
- ksadk/server/static/assets/{MathMessageMarkdown-BpH7jiFM.js → MathMessageMarkdown-BL8my1R2.js} +4 -4
- ksadk/server/static/assets/MathMessageMarkdown-BorAY7qD.css +1 -0
- ksadk/server/static/assets/MermaidBlock-Dz4IP-Tx.js +312 -0
- ksadk/server/static/assets/NativeTerminalPanel-DUrK0JpZ.js +1 -0
- ksadk/server/static/assets/abnfDiagram-VCTEODGH-g20pFzNV.js +1 -0
- ksadk/server/static/assets/arc-C4FzinUA.js +1 -0
- ksadk/server/static/assets/architecture-7GRP2DOG-DjKratdf.js +1 -0
- ksadk/server/static/assets/architectureDiagram-5GKGNRK7-DpIqL5h4.js +36 -0
- ksadk/server/static/assets/blockDiagram-NRAW4CY4-BjkljTNz.js +129 -0
- ksadk/server/static/assets/c4Diagram-UCG6FXSJ-Dk_ieq2X.js +38 -0
- ksadk/server/static/assets/channel-4cQHKtx1.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-DmgWkESh.js +1 -0
- ksadk/server/static/assets/{chunk-FMBD7UC4-BGhvzDYt.js → chunk-5VM5RSS4-BVHAchFb.js} +1 -1
- ksadk/server/static/assets/{chunk-ND2GUHAM-D98vPd-E.js → chunk-F27PBJKO-C-ipQzuS.js} +1 -1
- ksadk/server/static/assets/chunk-G27WJ6UU-C2EyJbIK.js +231 -0
- ksadk/server/static/assets/{chunk-4BX2VUAB-CsWIQRhc.js → chunk-JWPE2WC7-vYvVJb_M.js} +1 -1
- ksadk/server/static/assets/chunk-LCL6LL3I-BcI6ysPT.js +206 -0
- ksadk/server/static/assets/chunk-POPQ4Y6H-C030x_Z1.js +1 -0
- ksadk/server/static/assets/chunk-RHFEMEQ7-yyGpsz4n.js +168 -0
- ksadk/server/static/assets/chunk-SVP7TREG-BLTlmMU7.js +88 -0
- ksadk/server/static/assets/chunk-XXDRQBXY-C_32ArgP.js +1 -0
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-DLFd9Xi0.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DLFd9Xi0.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-S5V4N54A-CX3yotHq.js → cose-bilkent-JH36ORCC-9YQYwdl0.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-DjEM48Qp.js +1 -0
- ksadk/server/static/assets/cynefinDiagram-5FMLGOSQ-ErLF5N13.js +62 -0
- ksadk/server/static/assets/cytoscape.esm-CyCl8rPi.js +321 -0
- ksadk/server/static/assets/dagre-3AP2YEHR-DFiQF-6f.js +4 -0
- ksadk/server/static/assets/dagre-BuhGnRI1.js +1 -0
- ksadk/server/static/assets/diagram-S7CK7UJ4-Bt1v8-GC.js +30 -0
- ksadk/server/static/assets/diagram-UQ7AKVKN-DSCxJdBK.js +41 -0
- ksadk/server/static/assets/diagram-VSXAHHWV-DHfYwp_9.js +3 -0
- ksadk/server/static/assets/diagram-VX7I27RA-DdzD-4Le.js +24 -0
- ksadk/server/static/assets/diagram-Z3DM3KII-Du-nAJ9F.js +24 -0
- ksadk/server/static/assets/{dist-D82mtD2s.js → dist-8hqcx0sU.js} +1 -1
- ksadk/server/static/assets/dist-B7seoj3d.js +1 -0
- ksadk/server/static/assets/{dist-DHn6nBzr.js → dist-BE7bu-DL.js} +1 -1
- ksadk/server/static/assets/{dist-eoW6tZXA.js → dist-BVDffZ0U.js} +1 -1
- ksadk/server/static/assets/{dist-C2xjE7cO.js → dist-BX8z72IC.js} +1 -1
- ksadk/server/static/assets/{dist-CgF3fj15.js → dist-Bf2M8m3N.js} +1 -1
- ksadk/server/static/assets/{dist-Bs-jQcRX.js → dist-BjU6y-A2.js} +1 -1
- ksadk/server/static/assets/{dist-Djgl2wVZ.js → dist-BytlxIDT.js} +1 -1
- ksadk/server/static/assets/{dist-CVQRZjPu.js → dist-CSh_DE14.js} +1 -1
- ksadk/server/static/assets/{dist-6CVKuTAe.js → dist-CXYYBw3_.js} +1 -1
- ksadk/server/static/assets/{dist-DlzCRy0S.js → dist-C_wsv-Qd.js} +1 -1
- ksadk/server/static/assets/{dist-DF06ORQo.js → dist-ClxGviKw.js} +1 -1
- ksadk/server/static/assets/{dist-Cf23NY4L.js → dist-CttYUqzS.js} +1 -1
- ksadk/server/static/assets/{dist-ivOya1LD.js → dist-DNp6rLRA.js} +1 -1
- ksadk/server/static/assets/{dist-BibcG1V9.js → dist-DUX-id_F.js} +1 -1
- ksadk/server/static/assets/{dist-BUHRxvDG.js → dist-DmldrHd_.js} +1 -1
- ksadk/server/static/assets/{dist-BZ9n9AjO.js → dist-DnfXs8Vn.js} +2 -2
- ksadk/server/static/assets/{dist-DqNnNFrw.js → dist-Dq9gLD7L.js} +1 -1
- ksadk/server/static/assets/{dist-CGcp2ex8.js → dist-W-TIVntx.js} +1 -1
- ksadk/server/static/assets/{dist-Cwt7koer.js → dist-dDdADKFA.js} +1 -1
- ksadk/server/static/assets/{dist-C7-3jInC.js → dist-dydu68Fl.js} +1 -1
- ksadk/server/static/assets/{dist-C99THtIA.js → dist-ngtN0DJB.js} +1 -1
- ksadk/server/static/assets/ebnfDiagram-PWID7BFC-Da9C9NO1.js +1 -0
- ksadk/server/static/assets/erDiagram-SSCWMZ5O-EAGqqR79.js +99 -0
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-CMgMaJrC.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-BBKL0x3P.js +1 -0
- ksadk/server/static/assets/ganttDiagram-EL5Y4UJY-DqJsKb59.js +292 -0
- ksadk/server/static/assets/gitGraph-4MIJSDKK-Dw63mrhw.js +1 -0
- ksadk/server/static/assets/gitGraphDiagram-WWUBYQGX-B6g4dtDi.js +106 -0
- ksadk/server/static/assets/index-8ipRcQ-M.js +240 -0
- ksadk/server/static/assets/index-ByFCqjlh.css +2 -0
- ksadk/server/static/assets/info-A6RAGUB7-B6iAblII.js +1 -0
- ksadk/server/static/assets/infoDiagram-RXCK75RN-Djm_nbd5.js +2 -0
- ksadk/server/static/assets/{ishikawaDiagram-YF4QCWOH-DLOHGvWw.js → ishikawaDiagram-5VMMS53U-DWya9SG2.js} +6 -6
- ksadk/server/static/assets/{journeyDiagram-JHISSGLW-BIzKeejT.js → journeyDiagram-EYS64GPL-DutxhSNI.js} +6 -6
- ksadk/server/static/assets/{kanban-definition-UN3LZRKU-BWHoFSw2.js → kanban-definition-3QL26DDD-DJdOF8Fm.js} +10 -10
- ksadk/server/static/assets/katex-vFWytM5c.js +257 -0
- ksadk/server/static/assets/{linear-CvMBw9cA.js → linear-BHjG8b-J.js} +1 -1
- ksadk/server/static/assets/mermaid-parser.core-KGSy4jWT.js +166 -0
- ksadk/server/static/assets/{mindmap-definition-RKZ34NQL-CSAQxEAe.js → mindmap-definition-FBJOCRG2-9-HC88D4.js} +29 -29
- ksadk/server/static/assets/packet-AYTQ26CC-BOYAipuq.js +1 -0
- ksadk/server/static/assets/pegDiagram-XKGWAZYB-DrEZd4fD.js +1 -0
- ksadk/server/static/assets/pie-WAS4IAKB-Ctq0Kbku.js +1 -0
- ksadk/server/static/assets/pieDiagram-E7YTZNPT-DpjqOFFp.js +39 -0
- ksadk/server/static/assets/{quadrantDiagram-W4KKPZXB-CxIR4JXI.js → quadrantDiagram-AXDQQJYC-BiNhgOXX.js} +6 -6
- ksadk/server/static/assets/radar-RG4KPBEZ-DN0-ul_0.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-C6cC_to0.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-BimjIuRc.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CvY65ABq.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-D72Iq_Tv.js +1 -0
- ksadk/server/static/assets/railroadDiagram-O6MQD6OU-_Y3Ojg6G.js +1 -0
- ksadk/server/static/assets/requirementDiagram-EFPCY7ZU-DeCFdg9K.js +84 -0
- ksadk/server/static/assets/{sankeyDiagram-5OEKKPKP-DpGa6EAV.js → sankeyDiagram-P5KCCOFB-CPH75rhw.js} +7 -7
- ksadk/server/static/assets/{sequenceDiagram-3UESZ5HK-NTKNGGe5.js → sequenceDiagram-WJ2MYXX4-OEpQQdr4.js} +10 -10
- ksadk/server/static/assets/sizeCapture-X5ZJPWSS-heUErzhI.js +1 -0
- ksadk/server/static/assets/stateDiagram-HBIQ2CUA-DvA3jSMB.js +1 -0
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BgQY03nz.js +1 -0
- ksadk/server/static/assets/swimlanes-XN3QIQJK-CR-dfN_t.js +1 -0
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-DLVw3q5Q.js +8 -0
- ksadk/server/static/assets/{timeline-definition-PNZ67QCA-jqoYs3jQ.js → timeline-definition-24CTP7MA-DAVJf1TX.js} +7 -7
- ksadk/server/static/assets/treeView-Q6P3EWNA-BMpO0wsB.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-Et0aqzU3.js +1 -0
- ksadk/server/static/assets/vennDiagram-4TSXK5OY-DwM8eDkh.js +34 -0
- ksadk/server/static/assets/wardley-WFR3VGLG-CbY0mhpl.js +1 -0
- ksadk/server/static/assets/wardleyDiagram-VM6X3IG4-DZL-Zz2t.js +78 -0
- ksadk/server/static/assets/xychartDiagram-S5SC5T6Z-Dy6Yh_hu.js +7 -0
- ksadk/server/static/index.html +2 -2
- ksadk/sessions/__init__.py +26 -9
- ksadk/sessions/_local_service_sync.py +688 -0
- ksadk/sessions/_local_tables.py +21 -0
- ksadk/sessions/_postgres_schema.py +305 -0
- ksadk/sessions/_postgres_tables.py +15 -0
- ksadk/sessions/base.py +80 -1
- ksadk/sessions/in_memory.py +53 -2
- ksadk/sessions/local_service.py +63 -604
- ksadk/sessions/postgres_service.py +58 -148
- ksadk/sessions/resilient.py +35 -1
- ksadk/skills/runtime/backends/e2b.py +1 -1
- ksadk/skills/service_client.py +13 -3
- ksadk/studio/__init__.py +5 -0
- ksadk/studio/agent_avatar_assets.py +120 -0
- ksadk/studio/agent_lifecycle.py +51 -0
- ksadk/studio/api.py +1598 -0
- ksadk/studio/api_catalog_routes.py +324 -0
- ksadk/studio/api_contracts.py +286 -0
- ksadk/studio/api_helpers.py +132 -0
- ksadk/studio/api_memory_routes.py +82 -0
- ksadk/studio/authoring.py +749 -0
- ksadk/studio/authoring_coordinator.py +1017 -0
- ksadk/studio/builder.py +397 -0
- ksadk/studio/capabilities.py +368 -0
- ksadk/studio/cloud.py +2099 -0
- ksadk/studio/codex_agent_service.py +694 -0
- ksadk/studio/codex_authoring.py +469 -0
- ksadk/studio/codex_builder.py +437 -0
- ksadk/studio/codex_manifest.py +307 -0
- ksadk/studio/codex_run.py +395 -0
- ksadk/studio/compiler.py +190 -0
- ksadk/studio/contracts.py +755 -0
- ksadk/studio/errors.py +47 -0
- ksadk/studio/event_store.py +214 -0
- ksadk/studio/framework_run.py +342 -0
- ksadk/studio/hosted_kernel.py +317 -0
- ksadk/studio/identifiers.py +33 -0
- ksadk/studio/manifest_resolver.py +131 -0
- ksadk/studio/mcp_runtime.py +229 -0
- ksadk/studio/model_client.py +629 -0
- ksadk/studio/model_profile_service.py +214 -0
- ksadk/studio/operations.py +275 -0
- ksadk/studio/otel_trace.py +1060 -0
- ksadk/studio/pagination.py +182 -0
- ksadk/studio/pcm_memory.py +85 -0
- ksadk/studio/python_tool_inspection.py +183 -0
- ksadk/studio/repository.py +327 -0
- ksadk/studio/resource_catalog.py +1260 -0
- ksadk/studio/run_service.py +1133 -0
- ksadk/studio/runtime_catalog.py +74 -0
- ksadk/studio/runtime_source.py +365 -0
- ksadk/studio/service.py +2249 -0
- ksadk/studio/shared_web.py +1004 -0
- ksadk/studio/skill_discovery.py +628 -0
- ksadk/studio/static/assets/PrismRenderer-IYN-ffww.js +3 -0
- ksadk/studio/static/assets/index-BLpcdrgW.css +1 -0
- ksadk/studio/static/assets/index-CYekR2Xv.js +260 -0
- ksadk/studio/static/assets/react-vendor-BNmTiJ-I.js +9 -0
- ksadk/studio/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- ksadk/studio/static/index.html +15 -0
- ksadk/studio/templates.py +722 -0
- ksadk/studio/validator.py +265 -0
- ksadk/studio/workspace.py +135 -0
- ksadk/tools/gateway.py +9 -0
- ksadk/toolsets/__init__.py +27 -1
- ksadk/toolsets/a2a.py +426 -0
- ksadk/tracing/__init__.py +19 -21
- ksadk/tracing/setup.py +317 -403
- ksadk/tracing/span_utils.py +0 -44
- ksadk/version.py +1 -1
- ksadk_runtime_common/memory_backend/registry.py +56 -56
- ksadk_runtime_common/schemas/runtime_event_v1.json +37 -0
- ksadk_runtime_common/schemas/runtime_event_v2.json +271 -0
- ksadk_runtime_common/workspace_files/router.py +8 -2
- agentengine_sdk_python-0.8.0.dist-info/RECORD +0 -578
- ksadk/a2a/card_builder.py +0 -64
- ksadk/a2a/client.py +0 -309
- ksadk/events/parser.py +0 -191
- ksadk/harness/runner.py +0 -211
- ksadk/runners/codex_runner.py +0 -132
- ksadk/runners/utils/langfuse.py +0 -230
- ksadk/server/static/assets/MathMessageMarkdown-DeYh8QPr.css +0 -1
- ksadk/server/static/assets/MermaidBlock-DWuxETaJ.js +0 -303
- ksadk/server/static/assets/NativeTerminalPanel-MDTopWMO.js +0 -1
- ksadk/server/static/assets/arc-CFKwSk_B.js +0 -1
- ksadk/server/static/assets/architecture-7EHR7CIX-CynqOWjZ.js +0 -1
- ksadk/server/static/assets/architectureDiagram-3BPJPVTR-Dej8jX8c.js +0 -36
- ksadk/server/static/assets/blockDiagram-GPEHLZMM-1F64jqvy.js +0 -132
- ksadk/server/static/assets/c4Diagram-AAUBKEIU-9q7AUAKb.js +0 -10
- ksadk/server/static/assets/channel-iUGv2N24.js +0 -1
- ksadk/server/static/assets/chunk-2J33WTMH-6qgwJm7Q.js +0 -1
- ksadk/server/static/assets/chunk-55IACEB6-BK8paA20.js +0 -1
- ksadk/server/static/assets/chunk-727SXJPM-B-JEhy7h.js +0 -206
- ksadk/server/static/assets/chunk-AQP2D5EJ-DYBvMHAa.js +0 -231
- ksadk/server/static/assets/chunk-QZHKN3VN-YQIEXeF6.js +0 -1
- ksadk/server/static/assets/classDiagram-4FO5ZUOK-CDAUuoXK.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-Q7XG4LA2-CDAUuoXK.js +0 -1
- ksadk/server/static/assets/cytoscape.esm-DelgaX4f.js +0 -321
- ksadk/server/static/assets/dagre-BM42HDAG-C7TguAWo.js +0 -4
- ksadk/server/static/assets/dagre-Bx709z4p.js +0 -1
- ksadk/server/static/assets/diagram-2AECGRRQ-Dd17_kws.js +0 -43
- ksadk/server/static/assets/diagram-5GNKFQAL-DVIClIil.js +0 -10
- ksadk/server/static/assets/diagram-KO2AKTUF-Df_3gYQa.js +0 -3
- ksadk/server/static/assets/diagram-LMA3HP47-BhLk6ZtA.js +0 -24
- ksadk/server/static/assets/diagram-OG6HWLK6-D4GNGvuu.js +0 -24
- ksadk/server/static/assets/dist-DyvYLMW9.js +0 -1
- ksadk/server/static/assets/erDiagram-TEJ5UH35-CnfRcY2M.js +0 -85
- ksadk/server/static/assets/eventmodeling-FCH6USID-ChYO-ijM.js +0 -1
- ksadk/server/static/assets/flowDiagram-I6XJVG4X-DGaJjGNJ.js +0 -162
- ksadk/server/static/assets/ganttDiagram-6RSMTGT7-DpCoB2g9.js +0 -292
- ksadk/server/static/assets/gitGraph-WXDBUCRP-BN_0Fk-j.js +0 -1
- ksadk/server/static/assets/gitGraphDiagram-PVQCEYII-9IKVFBVT.js +0 -106
- ksadk/server/static/assets/graphlib-B8gBHxth.js +0 -1
- ksadk/server/static/assets/index-QSwCHuB8.js +0 -177
- ksadk/server/static/assets/index-jOPmTOZH.css +0 -2
- ksadk/server/static/assets/info-J43DQDTF-Cp6o3vId.js +0 -1
- ksadk/server/static/assets/infoDiagram-5YYISTIA-CYIIsb8y.js +0 -2
- ksadk/server/static/assets/katex-DATssjp1.js +0 -265
- ksadk/server/static/assets/mermaid-parser.core-mGAcEwcq.js +0 -161
- ksadk/server/static/assets/packet-YPE3B663-wQzct3cT.js +0 -1
- ksadk/server/static/assets/pie-LRSECV5Y-BgTQiv-9.js +0 -1
- ksadk/server/static/assets/pieDiagram-4H26LBE5-By-ra07M.js +0 -30
- ksadk/server/static/assets/radar-GUYGQ44K-BIq5fXsn.js +0 -1
- ksadk/server/static/assets/requirementDiagram-4Y6WPE33-DpW90iu3.js +0 -84
- ksadk/server/static/assets/stateDiagram-AJRCARHV-DGpd9yFG.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-BHNVJYJU-B3o579IQ.js +0 -1
- ksadk/server/static/assets/treeView-BLDUP644-Dqi-E0EM.js +0 -1
- ksadk/server/static/assets/treemap-LRROVOQU-ClQP9CJ9.js +0 -1
- ksadk/server/static/assets/vennDiagram-CIIHVFJN-l3Z7-wbP.js +0 -34
- ksadk/server/static/assets/wardley-L42UT6IY-8XLvnNIs.js +0 -1
- ksadk/server/static/assets/wardleyDiagram-YWT4CUSO-BR54GPmr.js +0 -78
- ksadk/server/static/assets/xychartDiagram-2RQKCTM6-SkiH8GsK.js +0 -7
- ksadk/tracing/exporters/langfuse_exporter.py +0 -460
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.0.dist-info → agentengine_sdk_python-0.8.2.dist-info}/top_level.txt +0 -0
ksadk/studio/cloud.py
ADDED
|
@@ -0,0 +1,2099 @@
|
|
|
1
|
+
"""Studio Bundle upload and existing Agent lifecycle gateway contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
from collections.abc import AsyncIterator
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable, Protocol, cast
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from ksadk.api import AgentEngineAPIError, AgentEngineClient
|
|
19
|
+
from ksadk.builders.ks3_uploader import KS3Uploader
|
|
20
|
+
from ksadk.studio.contracts import (
|
|
21
|
+
BuildRecord,
|
|
22
|
+
BuildStatus,
|
|
23
|
+
DeploymentRecord,
|
|
24
|
+
DeploymentRequest,
|
|
25
|
+
)
|
|
26
|
+
from ksadk.studio.errors import StudioError
|
|
27
|
+
from ksadk.studio.hosted_kernel import preflight_hosted_kernel_bundle
|
|
28
|
+
from ksadk.studio.repository import BuildRepository
|
|
29
|
+
from ksadk.studio.workspace import Workspace
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_STUDIO_CODE_COMMAND = (
|
|
34
|
+
"ksadk",
|
|
35
|
+
"web",
|
|
36
|
+
"/app/code/runtime",
|
|
37
|
+
"--port",
|
|
38
|
+
"8080",
|
|
39
|
+
"--host",
|
|
40
|
+
"0.0.0.0",
|
|
41
|
+
"--no-open",
|
|
42
|
+
)
|
|
43
|
+
# A Dashboard access link is both the user/session credential and the Agent
|
|
44
|
+
# binding. Keep the hosted surface in that same link instead of opening the
|
|
45
|
+
# Agent image's legacy `/chat` static bundle after authentication.
|
|
46
|
+
_HOSTED_AGENT_UI_PATH = "/hosted-ui/chat"
|
|
47
|
+
_NATIVE_DASHBOARD_UI_PATH = "/"
|
|
48
|
+
_NATIVE_DASHBOARD_FRAMEWORKS = frozenset({"hermes", "openclaw"})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class AccountCloudAgentReference:
|
|
53
|
+
agent_id: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CloudDeploymentGateway(Protocol):
|
|
57
|
+
async def upload_bundle(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
bundle: bytes,
|
|
61
|
+
bundle_digest: str,
|
|
62
|
+
provenance: dict[str, Any],
|
|
63
|
+
) -> str: ...
|
|
64
|
+
|
|
65
|
+
async def create_version(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
agent_id: str,
|
|
69
|
+
bundle_uri: str,
|
|
70
|
+
bundle_digest: str,
|
|
71
|
+
provenance: dict[str, Any],
|
|
72
|
+
) -> str: ...
|
|
73
|
+
|
|
74
|
+
async def create_deployment(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
build_id: str,
|
|
78
|
+
version_id: str,
|
|
79
|
+
bundle_digest: str,
|
|
80
|
+
request: DeploymentRequest,
|
|
81
|
+
) -> DeploymentRecord: ...
|
|
82
|
+
|
|
83
|
+
async def replace_deployment(
|
|
84
|
+
self,
|
|
85
|
+
deployment: DeploymentRecord,
|
|
86
|
+
*,
|
|
87
|
+
build_id: str,
|
|
88
|
+
version_id: str,
|
|
89
|
+
bundle_digest: str,
|
|
90
|
+
request: DeploymentRequest,
|
|
91
|
+
) -> DeploymentRecord: ...
|
|
92
|
+
|
|
93
|
+
async def get_deployment_status(self, deployment: DeploymentRecord) -> DeploymentRecord: ...
|
|
94
|
+
|
|
95
|
+
async def get_deployment_dashboard_access(
|
|
96
|
+
self, deployment: DeploymentRecord
|
|
97
|
+
) -> dict[str, str | None]: ...
|
|
98
|
+
|
|
99
|
+
async def delete_deployment(self, deployment: DeploymentRecord) -> bool: ...
|
|
100
|
+
|
|
101
|
+
async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]: ...
|
|
102
|
+
|
|
103
|
+
async def get_account_agent(self, agent_id: str) -> dict[str, Any]: ...
|
|
104
|
+
|
|
105
|
+
async def list_account_agent_versions(
|
|
106
|
+
self, agent_id: str, *, page: int, size: int
|
|
107
|
+
) -> dict[str, Any]: ...
|
|
108
|
+
|
|
109
|
+
async def rollback_account_agent_version(
|
|
110
|
+
self, agent_id: str, *, version_id: str
|
|
111
|
+
) -> dict[str, Any]: ...
|
|
112
|
+
|
|
113
|
+
async def get_account_agent_dashboard_access(
|
|
114
|
+
self, agent_id: str
|
|
115
|
+
) -> dict[str, str | None]: ...
|
|
116
|
+
|
|
117
|
+
async def delete_account_agent(self, agent_id: str) -> bool: ...
|
|
118
|
+
|
|
119
|
+
async def create_managed_runtime_deployment(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
build_id: str,
|
|
123
|
+
agent_name: str,
|
|
124
|
+
manifest: str,
|
|
125
|
+
runtime_name: str,
|
|
126
|
+
runtime_version: str,
|
|
127
|
+
manifest_digest: str,
|
|
128
|
+
request: DeploymentRequest,
|
|
129
|
+
) -> DeploymentRecord: ...
|
|
130
|
+
|
|
131
|
+
async def replace_managed_runtime_deployment(
|
|
132
|
+
self,
|
|
133
|
+
deployment: DeploymentRecord,
|
|
134
|
+
*,
|
|
135
|
+
build_id: str,
|
|
136
|
+
manifest: str,
|
|
137
|
+
runtime_name: str,
|
|
138
|
+
runtime_version: str,
|
|
139
|
+
manifest_digest: str,
|
|
140
|
+
request: DeploymentRequest,
|
|
141
|
+
) -> DeploymentRecord: ...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class UnavailableCloudGateway:
|
|
145
|
+
async def upload_bundle(self, **_kwargs) -> str:
|
|
146
|
+
raise StudioError(
|
|
147
|
+
"CLOUD_BUNDLE_DEPLOYMENT_UNAVAILABLE",
|
|
148
|
+
"当前未配置可用的云端签名账号,不能上传 Bundle",
|
|
149
|
+
status_code=501,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def create_version(self, **_kwargs) -> str:
|
|
153
|
+
raise AssertionError("upload_bundle must fail first")
|
|
154
|
+
|
|
155
|
+
async def create_deployment(self, **_kwargs) -> DeploymentRecord:
|
|
156
|
+
raise AssertionError("upload_bundle must fail first")
|
|
157
|
+
|
|
158
|
+
async def replace_deployment(
|
|
159
|
+
self,
|
|
160
|
+
_deployment: DeploymentRecord,
|
|
161
|
+
**_kwargs,
|
|
162
|
+
) -> DeploymentRecord:
|
|
163
|
+
raise AssertionError("upload_bundle must fail first")
|
|
164
|
+
|
|
165
|
+
async def create_managed_runtime_deployment(self, **_kwargs) -> DeploymentRecord:
|
|
166
|
+
raise StudioError(
|
|
167
|
+
"CLOUD_MANAGED_RUNTIME_DEPLOYMENT_UNAVAILABLE",
|
|
168
|
+
"当前未配置可用的云端签名账号,不能部署声明式 Agent",
|
|
169
|
+
status_code=501,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
async def replace_managed_runtime_deployment(
|
|
173
|
+
self, _deployment: DeploymentRecord, **_kwargs
|
|
174
|
+
) -> DeploymentRecord:
|
|
175
|
+
raise AssertionError("managed runtime deployment must fail first")
|
|
176
|
+
|
|
177
|
+
async def get_deployment_dashboard_access(
|
|
178
|
+
self, _deployment: DeploymentRecord
|
|
179
|
+
) -> dict[str, str | None]:
|
|
180
|
+
raise StudioError(
|
|
181
|
+
"CLOUD_DASHBOARD_UNAVAILABLE",
|
|
182
|
+
"当前未配置可用的云端签名账号,不能打开云端 Agent UI",
|
|
183
|
+
status_code=501,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def delete_deployment(self, _deployment: DeploymentRecord) -> bool:
|
|
187
|
+
raise StudioError(
|
|
188
|
+
"CLOUD_AGENT_DELETE_UNAVAILABLE",
|
|
189
|
+
"当前未配置可用的云端签名账号,不能删除云端 Agent",
|
|
190
|
+
status_code=501,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]:
|
|
194
|
+
# Local-only Studio remains usable without cloud credentials. Detail,
|
|
195
|
+
# chat and mutation still fail closed when explicitly requested.
|
|
196
|
+
return {
|
|
197
|
+
"items": [],
|
|
198
|
+
"total": 0,
|
|
199
|
+
"page": page,
|
|
200
|
+
"size": size,
|
|
201
|
+
"available": False,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async def get_account_agent(self, _agent_id: str) -> dict[str, Any]:
|
|
205
|
+
raise StudioError(
|
|
206
|
+
"CLOUD_AGENT_DIRECTORY_UNAVAILABLE",
|
|
207
|
+
"当前未配置可用的云端签名账号,不能读取云端 Agent",
|
|
208
|
+
status_code=501,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def list_account_agent_versions(
|
|
212
|
+
self, _agent_id: str, *, page: int, size: int
|
|
213
|
+
) -> dict[str, Any]:
|
|
214
|
+
raise StudioError(
|
|
215
|
+
"CLOUD_AGENT_VERSION_DIRECTORY_UNAVAILABLE",
|
|
216
|
+
"当前未配置可用的云端签名账号,不能读取云端版本",
|
|
217
|
+
status_code=501,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def rollback_account_agent_version(
|
|
221
|
+
self, _agent_id: str, *, version_id: str
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
raise StudioError(
|
|
224
|
+
"CLOUD_AGENT_VERSION_ROLLBACK_UNAVAILABLE",
|
|
225
|
+
"当前未配置可用的云端签名账号,不能回滚云端版本",
|
|
226
|
+
status_code=501,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def get_account_agent_dashboard_access(
|
|
230
|
+
self, _agent_id: str
|
|
231
|
+
) -> dict[str, str | None]:
|
|
232
|
+
raise StudioError(
|
|
233
|
+
"CLOUD_DASHBOARD_UNAVAILABLE",
|
|
234
|
+
"当前未配置可用的云端签名账号,不能打开云端 Agent UI",
|
|
235
|
+
status_code=501,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def delete_account_agent(self, _agent_id: str) -> bool:
|
|
239
|
+
raise StudioError(
|
|
240
|
+
"CLOUD_AGENT_DELETE_UNAVAILABLE",
|
|
241
|
+
"当前未配置可用的云端签名账号,不能删除云端 Agent",
|
|
242
|
+
status_code=501,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class InMemoryCloudGateway:
|
|
247
|
+
"""Contract-test gateway; it records exactly what would cross the cloud boundary."""
|
|
248
|
+
|
|
249
|
+
def __init__(self) -> None:
|
|
250
|
+
self.uploads: list[dict[str, Any]] = []
|
|
251
|
+
self.versions: list[dict[str, Any]] = []
|
|
252
|
+
self.deployments: list[DeploymentRecord] = []
|
|
253
|
+
self.deleted_agent_ids: list[str] = []
|
|
254
|
+
|
|
255
|
+
async def upload_bundle(self, **kwargs) -> str:
|
|
256
|
+
self.uploads.append(kwargs)
|
|
257
|
+
return f"memory://artifacts/{kwargs['bundle_digest']}"
|
|
258
|
+
|
|
259
|
+
async def create_version(self, **kwargs) -> str:
|
|
260
|
+
self.versions.append(kwargs)
|
|
261
|
+
return f"ver_{uuid4().hex}"
|
|
262
|
+
|
|
263
|
+
async def create_deployment(self, **kwargs) -> DeploymentRecord:
|
|
264
|
+
record = DeploymentRecord(
|
|
265
|
+
id=f"dep_{uuid4().hex}",
|
|
266
|
+
build_id=kwargs["build_id"],
|
|
267
|
+
bundle_digest=kwargs["bundle_digest"],
|
|
268
|
+
version_id=kwargs["version_id"],
|
|
269
|
+
status="READY",
|
|
270
|
+
target=kwargs["request"].target,
|
|
271
|
+
)
|
|
272
|
+
self.deployments.append(record)
|
|
273
|
+
return record
|
|
274
|
+
|
|
275
|
+
async def replace_deployment(
|
|
276
|
+
self,
|
|
277
|
+
deployment: DeploymentRecord,
|
|
278
|
+
**kwargs,
|
|
279
|
+
) -> DeploymentRecord:
|
|
280
|
+
record = DeploymentRecord(
|
|
281
|
+
id=f"dep_{uuid4().hex}",
|
|
282
|
+
build_id=kwargs["build_id"],
|
|
283
|
+
bundle_digest=kwargs["bundle_digest"],
|
|
284
|
+
version_id=kwargs["version_id"],
|
|
285
|
+
status="READY",
|
|
286
|
+
target=kwargs["request"].target,
|
|
287
|
+
agent_id=deployment.agent_id,
|
|
288
|
+
instance_id=deployment.instance_id,
|
|
289
|
+
endpoint=deployment.endpoint,
|
|
290
|
+
)
|
|
291
|
+
self.deployments.append(record)
|
|
292
|
+
return record
|
|
293
|
+
|
|
294
|
+
async def get_deployment_status(self, deployment: DeploymentRecord) -> DeploymentRecord:
|
|
295
|
+
return deployment
|
|
296
|
+
|
|
297
|
+
async def get_deployment_dashboard_access(
|
|
298
|
+
self, deployment: DeploymentRecord
|
|
299
|
+
) -> dict[str, str | None]:
|
|
300
|
+
if not deployment.agent_id:
|
|
301
|
+
raise StudioError(
|
|
302
|
+
"DEPLOYMENT_DASHBOARD_UNAVAILABLE",
|
|
303
|
+
"Deployment receipt 缺少云端 Agent 标识",
|
|
304
|
+
status_code=409,
|
|
305
|
+
)
|
|
306
|
+
return {
|
|
307
|
+
"access_url": f"memory://dashboard/{deployment.agent_id}",
|
|
308
|
+
"agent_id": deployment.agent_id,
|
|
309
|
+
"instance_id": deployment.instance_id,
|
|
310
|
+
"expires_at": None,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async def delete_deployment(self, deployment: DeploymentRecord) -> bool:
|
|
314
|
+
if not deployment.agent_id:
|
|
315
|
+
raise StudioError(
|
|
316
|
+
"CLOUD_AGENT_DELETE_UNAVAILABLE",
|
|
317
|
+
"Deployment receipt 缺少云端 Agent 标识",
|
|
318
|
+
status_code=409,
|
|
319
|
+
)
|
|
320
|
+
self.deleted_agent_ids.append(deployment.agent_id)
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]:
|
|
324
|
+
rows = [
|
|
325
|
+
{
|
|
326
|
+
"agentId": item.agent_id,
|
|
327
|
+
"name": item.agent_id,
|
|
328
|
+
"status": item.status,
|
|
329
|
+
"endpoint": item.endpoint,
|
|
330
|
+
}
|
|
331
|
+
for item in self.deployments
|
|
332
|
+
if item.agent_id
|
|
333
|
+
]
|
|
334
|
+
start = (page - 1) * size
|
|
335
|
+
return {"items": rows[start : start + size], "total": len(rows), "page": page, "size": size}
|
|
336
|
+
|
|
337
|
+
async def get_account_agent(self, agent_id: str) -> dict[str, Any]:
|
|
338
|
+
for item in reversed(self.deployments):
|
|
339
|
+
if item.agent_id == agent_id:
|
|
340
|
+
return {
|
|
341
|
+
"agentId": agent_id,
|
|
342
|
+
"name": agent_id,
|
|
343
|
+
"status": item.status,
|
|
344
|
+
"endpoint": item.endpoint,
|
|
345
|
+
"versionId": item.version_id,
|
|
346
|
+
}
|
|
347
|
+
raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 不存在", status_code=404)
|
|
348
|
+
|
|
349
|
+
async def list_account_agent_versions(
|
|
350
|
+
self, agent_id: str, *, page: int, size: int
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
detail = await self.get_account_agent(agent_id)
|
|
353
|
+
version_id = str(detail.get("versionId") or "").strip()
|
|
354
|
+
items = []
|
|
355
|
+
if version_id:
|
|
356
|
+
items.append(
|
|
357
|
+
{
|
|
358
|
+
"versionId": version_id,
|
|
359
|
+
"versionName": version_id,
|
|
360
|
+
"tag": "",
|
|
361
|
+
"status": "current",
|
|
362
|
+
"trafficPercentage": 100,
|
|
363
|
+
"canRollback": False,
|
|
364
|
+
"rollbackDisabledReason": "当前版本不可回滚至自身",
|
|
365
|
+
"createdAt": None,
|
|
366
|
+
"createdBy": "",
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
return {
|
|
370
|
+
"items": items,
|
|
371
|
+
"total": len(items),
|
|
372
|
+
"currentVersionId": version_id or None,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async def rollback_account_agent_version(
|
|
376
|
+
self, agent_id: str, *, version_id: str
|
|
377
|
+
) -> dict[str, Any]:
|
|
378
|
+
await self.get_account_agent(agent_id)
|
|
379
|
+
return {
|
|
380
|
+
"agentId": agent_id,
|
|
381
|
+
"targetVersionId": version_id,
|
|
382
|
+
"status": "UPDATING",
|
|
383
|
+
"noop": False,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async def get_account_agent_dashboard_access(
|
|
387
|
+
self, agent_id: str
|
|
388
|
+
) -> dict[str, str | None]:
|
|
389
|
+
return {
|
|
390
|
+
"access_url": f"memory://dashboard/{agent_id}",
|
|
391
|
+
"agent_id": agent_id,
|
|
392
|
+
"instance_id": None,
|
|
393
|
+
"expires_at": None,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async def delete_account_agent(self, agent_id: str) -> bool:
|
|
397
|
+
self.deleted_agent_ids.append(agent_id)
|
|
398
|
+
return True
|
|
399
|
+
|
|
400
|
+
async def create_managed_runtime_deployment(self, **kwargs) -> DeploymentRecord:
|
|
401
|
+
digest = str(kwargs["manifest_digest"])
|
|
402
|
+
record = DeploymentRecord(
|
|
403
|
+
id=f"dep_{uuid4().hex}",
|
|
404
|
+
build_id=str(kwargs["build_id"]),
|
|
405
|
+
bundle_digest=f"sha256:{digest}",
|
|
406
|
+
version_id=f"managed-{digest[:16]}",
|
|
407
|
+
status="READY",
|
|
408
|
+
target=kwargs["request"].target,
|
|
409
|
+
artifact_id="managed-runtime",
|
|
410
|
+
)
|
|
411
|
+
self.deployments.append(record)
|
|
412
|
+
return record
|
|
413
|
+
|
|
414
|
+
async def replace_managed_runtime_deployment(
|
|
415
|
+
self, deployment: DeploymentRecord, **kwargs
|
|
416
|
+
) -> DeploymentRecord:
|
|
417
|
+
digest = str(kwargs["manifest_digest"])
|
|
418
|
+
record = DeploymentRecord(
|
|
419
|
+
id=f"dep_{uuid4().hex}",
|
|
420
|
+
build_id=str(kwargs["build_id"]),
|
|
421
|
+
bundle_digest=f"sha256:{digest}",
|
|
422
|
+
version_id=f"managed-{digest[:16]}",
|
|
423
|
+
status="READY",
|
|
424
|
+
target=kwargs["request"].target,
|
|
425
|
+
agent_id=deployment.agent_id,
|
|
426
|
+
instance_id=deployment.instance_id,
|
|
427
|
+
endpoint=deployment.endpoint,
|
|
428
|
+
artifact_id="managed-runtime",
|
|
429
|
+
)
|
|
430
|
+
self.deployments.append(record)
|
|
431
|
+
return record
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class DirectAgentEngineCloudDeploymentGateway:
|
|
435
|
+
"""Deploy a Studio Bundle with the established KS3 and signed Agent APIs.
|
|
436
|
+
|
|
437
|
+
A local Studio has the user's existing AK/SK and therefore uses the same
|
|
438
|
+
two-step path as ``agentengine build --push`` then ``agentengine deploy``:
|
|
439
|
+
upload an immutable ZIP to KS3, then call ``CreateAgent`` (or
|
|
440
|
+
``UpdateAgent`` for rollback) through :class:`AgentEngineClient`. It does
|
|
441
|
+
not introduce an Artifact Action, a browser-provided trusted header, or a
|
|
442
|
+
second account-control authentication scheme.
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
requires_hosted_kernel_bundle_preflight = True
|
|
446
|
+
|
|
447
|
+
def __init__(
|
|
448
|
+
self,
|
|
449
|
+
*,
|
|
450
|
+
region: str,
|
|
451
|
+
client: Any | None = None,
|
|
452
|
+
stream_client: Any | None = None,
|
|
453
|
+
uploader_factory: Callable[..., Any] = KS3Uploader,
|
|
454
|
+
bucket: str | None = None,
|
|
455
|
+
ks3_credentials: dict[str, str] | None = None,
|
|
456
|
+
) -> None:
|
|
457
|
+
self.region = region.strip()
|
|
458
|
+
self.ks3_region = "cn-beijing-6" if self.region.lower() == "pre-online" else self.region
|
|
459
|
+
self.uploader_factory = uploader_factory
|
|
460
|
+
self.bucket = bucket or os.environ.get("KS3_BUCKET", "").strip() or None
|
|
461
|
+
supplied_credentials = ks3_credentials or {}
|
|
462
|
+
self._ks3_credentials = {
|
|
463
|
+
"access_key": str(
|
|
464
|
+
supplied_credentials.get("access_key")
|
|
465
|
+
or os.environ.get("KSYUN_ACCESS_KEY")
|
|
466
|
+
or os.environ.get("KS3_ACCESS_KEY")
|
|
467
|
+
or ""
|
|
468
|
+
).strip(),
|
|
469
|
+
"secret_key": str(
|
|
470
|
+
supplied_credentials.get("secret_key")
|
|
471
|
+
or os.environ.get("KSYUN_SECRET_KEY")
|
|
472
|
+
or os.environ.get("KS3_SECRET_KEY")
|
|
473
|
+
or ""
|
|
474
|
+
).strip(),
|
|
475
|
+
}
|
|
476
|
+
if not all(self._ks3_credentials.values()):
|
|
477
|
+
raise ValueError("Studio cloud gateway requires process-only KS3 credentials")
|
|
478
|
+
# The same process-only AK/SK signs AgentEngine control-plane actions.
|
|
479
|
+
# Never let Studio silently fall through to an unsigned client just
|
|
480
|
+
# because an internal development ingress happens to be reachable.
|
|
481
|
+
self.client = client or AgentEngineClient(
|
|
482
|
+
region=self.region,
|
|
483
|
+
access_key=self._ks3_credentials["access_key"],
|
|
484
|
+
secret_key=self._ks3_credentials["secret_key"],
|
|
485
|
+
)
|
|
486
|
+
# Streaming can use a dedicated Server ingress while ordinary control
|
|
487
|
+
# actions continue through KOP. Some KOP deployments buffer the
|
|
488
|
+
# complete response body even when RunAgent returns SSE; both clients
|
|
489
|
+
# still use the same process-only V4 credentials and Server admission.
|
|
490
|
+
self.stream_client = stream_client or self.client
|
|
491
|
+
self._bundles: dict[str, dict[str, str]] = {}
|
|
492
|
+
|
|
493
|
+
async def upload_bundle(self, **kwargs) -> str:
|
|
494
|
+
bundle = bytes(kwargs["bundle"])
|
|
495
|
+
provenance = dict(kwargs["provenance"])
|
|
496
|
+
bundle_digest = str(kwargs["bundle_digest"])
|
|
497
|
+
agent_id = str(provenance.get("agentId") or "").strip()
|
|
498
|
+
runtime_type = str(provenance.get("runtimeType") or "").strip().lower()
|
|
499
|
+
if not agent_id or not runtime_type:
|
|
500
|
+
raise StudioError(
|
|
501
|
+
"CLOUD_BUNDLE_METADATA_INVALID",
|
|
502
|
+
"本地 Bundle 缺少 agentId 或 runtimeType,不能上云",
|
|
503
|
+
status_code=422,
|
|
504
|
+
)
|
|
505
|
+
archive_sha = hashlib.sha256(bundle).hexdigest()
|
|
506
|
+
if not bundle_digest.startswith("sha256:"):
|
|
507
|
+
raise StudioError(
|
|
508
|
+
"CLOUD_BUNDLE_METADATA_INVALID",
|
|
509
|
+
"本地 Bundle 缺少 sha256 digest,不能上云",
|
|
510
|
+
status_code=422,
|
|
511
|
+
)
|
|
512
|
+
object_key = f"studio-bundles/{_safe_object_component(agent_id)}/{archive_sha}/bundle.zip"
|
|
513
|
+
uploader = self.uploader_factory(region=self.ks3_region, bucket=self.bucket)
|
|
514
|
+
local_path: Path | None = None
|
|
515
|
+
try:
|
|
516
|
+
with tempfile.NamedTemporaryFile(
|
|
517
|
+
mode="wb", prefix="agentkit-studio-", suffix=".zip", delete=False
|
|
518
|
+
) as output:
|
|
519
|
+
output.write(bundle)
|
|
520
|
+
local_path = Path(output.name)
|
|
521
|
+
bundle_uri = await uploader.upload(local_path, object_key)
|
|
522
|
+
finally:
|
|
523
|
+
if local_path is not None:
|
|
524
|
+
local_path.unlink(missing_ok=True)
|
|
525
|
+
if not bundle_uri:
|
|
526
|
+
raise StudioError(
|
|
527
|
+
"CLOUD_BUNDLE_UPLOAD_FAILED",
|
|
528
|
+
"Bundle 上传到 KS3 失败,未创建云端 Agent",
|
|
529
|
+
status_code=502,
|
|
530
|
+
)
|
|
531
|
+
uri = str(bundle_uri)
|
|
532
|
+
self._bundles[uri] = {
|
|
533
|
+
"agent_id": agent_id,
|
|
534
|
+
"archive_sha": archive_sha,
|
|
535
|
+
"bundle_digest": bundle_digest,
|
|
536
|
+
"runtime_type": runtime_type,
|
|
537
|
+
"bucket": str(getattr(uploader, "bucket_name", self.bucket or "")),
|
|
538
|
+
}
|
|
539
|
+
return uri
|
|
540
|
+
|
|
541
|
+
async def create_version(self, **kwargs) -> str:
|
|
542
|
+
bundle_uri = str(kwargs["bundle_uri"])
|
|
543
|
+
bundle = self._bundles.get(bundle_uri)
|
|
544
|
+
if bundle is None:
|
|
545
|
+
raise StudioError(
|
|
546
|
+
"CLOUD_BUNDLE_REFERENCE_INVALID",
|
|
547
|
+
"未知的 KS3 Bundle 引用",
|
|
548
|
+
status_code=502,
|
|
549
|
+
)
|
|
550
|
+
if bundle["agent_id"] != str(kwargs["agent_id"]):
|
|
551
|
+
raise StudioError(
|
|
552
|
+
"CLOUD_BUNDLE_REFERENCE_INVALID",
|
|
553
|
+
"Bundle 与当前 Agent 不匹配",
|
|
554
|
+
status_code=502,
|
|
555
|
+
)
|
|
556
|
+
if bundle["bundle_digest"] != str(kwargs["bundle_digest"]):
|
|
557
|
+
raise StudioError(
|
|
558
|
+
"CLOUD_BUNDLE_REFERENCE_INVALID",
|
|
559
|
+
"Bundle digest 不匹配",
|
|
560
|
+
status_code=502,
|
|
561
|
+
)
|
|
562
|
+
return bundle_uri
|
|
563
|
+
|
|
564
|
+
async def create_deployment(self, **kwargs) -> DeploymentRecord:
|
|
565
|
+
bundle_uri = str(kwargs["version_id"])
|
|
566
|
+
bundle = self._bundle_for_deployment(bundle_uri, kwargs["bundle_digest"])
|
|
567
|
+
request: DeploymentRequest = kwargs["request"]
|
|
568
|
+
result = await self.client.create_agent(
|
|
569
|
+
self._create_payload(bundle_uri=bundle_uri, bundle=bundle, request=request)
|
|
570
|
+
)
|
|
571
|
+
agent_id = str(result.get("agent_id") or "").strip()
|
|
572
|
+
instance_id = str(result.get("instance_id") or "").strip() or None
|
|
573
|
+
if not agent_id:
|
|
574
|
+
raise StudioError(
|
|
575
|
+
"CLOUD_DEPLOYMENT_PROTOCOL_INVALID",
|
|
576
|
+
"CreateAgentProduct 未返回 AgentId",
|
|
577
|
+
status_code=502,
|
|
578
|
+
)
|
|
579
|
+
return self._receipt(
|
|
580
|
+
build_id=str(kwargs["build_id"]),
|
|
581
|
+
bundle_digest=str(kwargs["bundle_digest"]),
|
|
582
|
+
bundle_uri=bundle_uri,
|
|
583
|
+
agent_id=agent_id,
|
|
584
|
+
instance_id=instance_id,
|
|
585
|
+
endpoint=str(result.get("endpoint") or "").strip() or None,
|
|
586
|
+
target=request.target,
|
|
587
|
+
status="DEPLOYING",
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
async def replace_deployment(
|
|
591
|
+
self,
|
|
592
|
+
deployment: DeploymentRecord,
|
|
593
|
+
**kwargs,
|
|
594
|
+
) -> DeploymentRecord:
|
|
595
|
+
if not deployment.agent_id:
|
|
596
|
+
raise StudioError(
|
|
597
|
+
"DEPLOYMENT_PROTOCOL_INVALID",
|
|
598
|
+
"部署 receipt 缺少 AgentId",
|
|
599
|
+
status_code=502,
|
|
600
|
+
)
|
|
601
|
+
bundle_uri = str(kwargs["version_id"])
|
|
602
|
+
bundle = self._bundle_for_deployment(bundle_uri, kwargs["bundle_digest"])
|
|
603
|
+
request: DeploymentRequest = kwargs["request"]
|
|
604
|
+
await self.client.update_agent(
|
|
605
|
+
deployment.agent_id,
|
|
606
|
+
{
|
|
607
|
+
"artifact_type": "Code",
|
|
608
|
+
"artifact_path": bundle_uri,
|
|
609
|
+
"code_checksum": bundle["archive_sha"],
|
|
610
|
+
"code_command": list(_STUDIO_CODE_COMMAND),
|
|
611
|
+
"ks3": self._code_config(bundle["bucket"]),
|
|
612
|
+
},
|
|
613
|
+
)
|
|
614
|
+
return self._receipt(
|
|
615
|
+
build_id=str(kwargs["build_id"]),
|
|
616
|
+
bundle_digest=str(kwargs["bundle_digest"]),
|
|
617
|
+
bundle_uri=bundle_uri,
|
|
618
|
+
agent_id=deployment.agent_id,
|
|
619
|
+
instance_id=deployment.instance_id,
|
|
620
|
+
endpoint=deployment.endpoint,
|
|
621
|
+
target=request.target,
|
|
622
|
+
status="DEPLOYING",
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
async def get_deployment_status(self, deployment: DeploymentRecord) -> DeploymentRecord:
|
|
626
|
+
if not deployment.agent_id:
|
|
627
|
+
return deployment
|
|
628
|
+
try:
|
|
629
|
+
payload = await self.client.get_agent(agent_id=deployment.agent_id)
|
|
630
|
+
except AgentEngineAPIError as exc:
|
|
631
|
+
# A receipt can outlive the cloud Agent it originally created. Do
|
|
632
|
+
# not leave that receipt in DEPLOYING forever: the Server's 404 is
|
|
633
|
+
# an authoritative terminal fact, not a transient readiness gap.
|
|
634
|
+
if exc.code == 404 or exc.details.get("http_status") == 404:
|
|
635
|
+
return deployment.model_copy(update={"status": "FAILED"})
|
|
636
|
+
raise
|
|
637
|
+
deployment_detail = payload.get("deployment") or {}
|
|
638
|
+
kernel_ready = bool(
|
|
639
|
+
payload.get("agent_kernel_ready") or deployment_detail.get("agent_kernel_ready")
|
|
640
|
+
)
|
|
641
|
+
status = (
|
|
642
|
+
str(
|
|
643
|
+
payload.get("status")
|
|
644
|
+
or (payload.get("basic") or {}).get("status")
|
|
645
|
+
or (payload.get("deployment") or {}).get("status")
|
|
646
|
+
or ""
|
|
647
|
+
)
|
|
648
|
+
.strip()
|
|
649
|
+
.upper()
|
|
650
|
+
)
|
|
651
|
+
projected = (
|
|
652
|
+
"FAILED"
|
|
653
|
+
if status in {"FAILED", "TERMINATED", "ERROR"}
|
|
654
|
+
else "READY"
|
|
655
|
+
if kernel_ready
|
|
656
|
+
or (
|
|
657
|
+
not deployment.requires_kernel
|
|
658
|
+
and deployment.artifact_id == "managed-runtime"
|
|
659
|
+
and status in {"RUNNING", "READY"}
|
|
660
|
+
)
|
|
661
|
+
else "DEPLOYING"
|
|
662
|
+
)
|
|
663
|
+
endpoint = (
|
|
664
|
+
str(
|
|
665
|
+
payload.get("endpoint")
|
|
666
|
+
or (payload.get("basic") or {}).get("endpoint")
|
|
667
|
+
or deployment_detail.get("endpoint")
|
|
668
|
+
or deployment.endpoint
|
|
669
|
+
or ""
|
|
670
|
+
).strip()
|
|
671
|
+
or None
|
|
672
|
+
)
|
|
673
|
+
return deployment.model_copy(update={"status": projected, "endpoint": endpoint})
|
|
674
|
+
|
|
675
|
+
async def get_deployment_dashboard_access(
|
|
676
|
+
self, deployment: DeploymentRecord
|
|
677
|
+
) -> dict[str, str | None]:
|
|
678
|
+
if not deployment.agent_id:
|
|
679
|
+
raise StudioError(
|
|
680
|
+
"DEPLOYMENT_DASHBOARD_UNAVAILABLE",
|
|
681
|
+
"Deployment receipt 缺少云端 Agent 标识",
|
|
682
|
+
status_code=409,
|
|
683
|
+
)
|
|
684
|
+
link = await self.client.create_dashboard_access_link(
|
|
685
|
+
agent_id=deployment.agent_id,
|
|
686
|
+
link_type="private",
|
|
687
|
+
path=_HOSTED_AGENT_UI_PATH,
|
|
688
|
+
)
|
|
689
|
+
access_url = str(link.get("access_url") or "").strip()
|
|
690
|
+
if not access_url:
|
|
691
|
+
raise StudioError(
|
|
692
|
+
"DEPLOYMENT_DASHBOARD_UNAVAILABLE",
|
|
693
|
+
"云端未返回可用的 Agent UI 地址",
|
|
694
|
+
status_code=502,
|
|
695
|
+
)
|
|
696
|
+
return {
|
|
697
|
+
"access_url": access_url,
|
|
698
|
+
"agent_id": deployment.agent_id,
|
|
699
|
+
"instance_id": deployment.instance_id,
|
|
700
|
+
"expires_at": str(link.get("expires_at") or "").strip() or None,
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async def delete_deployment(self, deployment: DeploymentRecord) -> bool:
|
|
704
|
+
agent_id = str(deployment.agent_id or "").strip()
|
|
705
|
+
if not agent_id:
|
|
706
|
+
raise StudioError(
|
|
707
|
+
"CLOUD_AGENT_DELETE_UNAVAILABLE",
|
|
708
|
+
"Deployment receipt 缺少云端 Agent 标识",
|
|
709
|
+
status_code=409,
|
|
710
|
+
)
|
|
711
|
+
try:
|
|
712
|
+
deleted = await self.client.delete_agent(agent_id)
|
|
713
|
+
except AgentEngineAPIError as exc:
|
|
714
|
+
if exc.code == 404 or exc.details.get("http_status") == 404:
|
|
715
|
+
return True
|
|
716
|
+
raise
|
|
717
|
+
if not deleted:
|
|
718
|
+
raise StudioError(
|
|
719
|
+
"CLOUD_AGENT_DELETE_FAILED",
|
|
720
|
+
"云端未确认 Agent 删除结果",
|
|
721
|
+
status_code=502,
|
|
722
|
+
)
|
|
723
|
+
return True
|
|
724
|
+
|
|
725
|
+
@staticmethod
|
|
726
|
+
def _session_event_chat_declared(capabilities: Any) -> bool:
|
|
727
|
+
if not isinstance(capabilities, dict):
|
|
728
|
+
return False
|
|
729
|
+
declaration = None
|
|
730
|
+
for name in ("session_event_chat", "sessionEventChat", "SessionEventChat"):
|
|
731
|
+
if name in capabilities:
|
|
732
|
+
declaration = capabilities[name]
|
|
733
|
+
break
|
|
734
|
+
if isinstance(declaration, bool):
|
|
735
|
+
return declaration
|
|
736
|
+
if not isinstance(declaration, dict):
|
|
737
|
+
return False
|
|
738
|
+
for name in ("enabled", "Enabled", "supported", "Supported"):
|
|
739
|
+
if name in declaration:
|
|
740
|
+
return declaration[name] is True
|
|
741
|
+
return False
|
|
742
|
+
|
|
743
|
+
@classmethod
|
|
744
|
+
def _cloud_chat_route(
|
|
745
|
+
cls, *, runtime_type: str, capabilities: Any
|
|
746
|
+
) -> tuple[str, str]:
|
|
747
|
+
if cls._session_event_chat_declared(capabilities):
|
|
748
|
+
return (
|
|
749
|
+
"studio-session-events",
|
|
750
|
+
"declared-session-event-chat-capability",
|
|
751
|
+
)
|
|
752
|
+
if runtime_type in _NATIVE_DASHBOARD_FRAMEWORKS:
|
|
753
|
+
return (
|
|
754
|
+
"official-dashboard",
|
|
755
|
+
"native-runtime-without-session-event-chat-capability",
|
|
756
|
+
)
|
|
757
|
+
return "studio-session-events", "studio-compatible-framework"
|
|
758
|
+
|
|
759
|
+
@staticmethod
|
|
760
|
+
def _account_agent_view(payload: dict[str, Any], *, fallback_id: str = "") -> dict[str, Any]:
|
|
761
|
+
basic = payload.get("basic") if isinstance(payload.get("basic"), dict) else {}
|
|
762
|
+
quick_access = (
|
|
763
|
+
payload.get("quick_access")
|
|
764
|
+
if isinstance(payload.get("quick_access"), dict)
|
|
765
|
+
else payload.get("quickAccess")
|
|
766
|
+
if isinstance(payload.get("quickAccess"), dict)
|
|
767
|
+
else {}
|
|
768
|
+
)
|
|
769
|
+
deployment = (
|
|
770
|
+
payload.get("deployment")
|
|
771
|
+
if isinstance(payload.get("deployment"), dict)
|
|
772
|
+
else {}
|
|
773
|
+
)
|
|
774
|
+
runtime_config = (
|
|
775
|
+
deployment.get("runtime_config")
|
|
776
|
+
if isinstance(deployment.get("runtime_config"), dict)
|
|
777
|
+
else deployment.get("runtimeConfig")
|
|
778
|
+
if isinstance(deployment.get("runtimeConfig"), dict)
|
|
779
|
+
else {}
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
def first(*names: str) -> Any:
|
|
783
|
+
for source in (payload, basic, deployment):
|
|
784
|
+
for name in names:
|
|
785
|
+
value = source.get(name)
|
|
786
|
+
if value is not None and str(value).strip():
|
|
787
|
+
return value
|
|
788
|
+
return None
|
|
789
|
+
|
|
790
|
+
# GetAgent retains a few compatibility fields at the response root.
|
|
791
|
+
# They can lag behind ``basic`` while Runtime-Service is reconciling;
|
|
792
|
+
# presenting that stale root ``CREATING`` value in Studio hid a
|
|
793
|
+
# genuinely RUNNING Agent. Basic is the Server's lifecycle read model
|
|
794
|
+
# (and is also what the CLI renders), so it is authoritative here.
|
|
795
|
+
def lifecycle_first(*names: str) -> Any:
|
|
796
|
+
for source in (basic, deployment, payload):
|
|
797
|
+
for name in names:
|
|
798
|
+
value = source.get(name)
|
|
799
|
+
if value is not None and str(value).strip():
|
|
800
|
+
return value
|
|
801
|
+
return None
|
|
802
|
+
|
|
803
|
+
agent_id = str(
|
|
804
|
+
first("agent_id", "agentId", "agent_runtime_id", "agentRuntimeId", "id")
|
|
805
|
+
or fallback_id
|
|
806
|
+
).strip()
|
|
807
|
+
runtime_type = str(
|
|
808
|
+
first(
|
|
809
|
+
"framework",
|
|
810
|
+
"runtime_kind",
|
|
811
|
+
"runtimeKind",
|
|
812
|
+
"runtime_type",
|
|
813
|
+
"runtimeType",
|
|
814
|
+
"runtime_name",
|
|
815
|
+
"runtimeName",
|
|
816
|
+
)
|
|
817
|
+
or ""
|
|
818
|
+
).strip().lower()
|
|
819
|
+
capabilities = first("capabilities", "Capabilities")
|
|
820
|
+
chat_transport, chat_routing_reason = (
|
|
821
|
+
DirectAgentEngineCloudDeploymentGateway._cloud_chat_route(
|
|
822
|
+
runtime_type=runtime_type,
|
|
823
|
+
capabilities=capabilities,
|
|
824
|
+
)
|
|
825
|
+
)
|
|
826
|
+
version_id = str(first("version_id", "versionId", "revision") or "").strip()
|
|
827
|
+
manifest_sha256 = str(
|
|
828
|
+
runtime_config.get("manifest_sha256")
|
|
829
|
+
or runtime_config.get("manifestSha256")
|
|
830
|
+
or ""
|
|
831
|
+
).strip().lower()
|
|
832
|
+
if not version_id and len(manifest_sha256) == 64:
|
|
833
|
+
try:
|
|
834
|
+
bytes.fromhex(manifest_sha256)
|
|
835
|
+
except ValueError:
|
|
836
|
+
pass
|
|
837
|
+
else:
|
|
838
|
+
version_id = f"managed-{manifest_sha256[:16]}"
|
|
839
|
+
return {
|
|
840
|
+
"agentId": agent_id,
|
|
841
|
+
"name": str(
|
|
842
|
+
first(
|
|
843
|
+
"name",
|
|
844
|
+
"agent_name",
|
|
845
|
+
"agentName",
|
|
846
|
+
"agent_runtime_name",
|
|
847
|
+
"agentRuntimeName",
|
|
848
|
+
)
|
|
849
|
+
or agent_id
|
|
850
|
+
),
|
|
851
|
+
# Server resolves Creator through IAM when an Agent is created.
|
|
852
|
+
# It is the human-readable sub-account name; preserve it for the
|
|
853
|
+
# Studio directory rather than trying to derive a name client-side.
|
|
854
|
+
"creatorName": str(first("creator", "Creator") or "").strip() or None,
|
|
855
|
+
"status": str(lifecycle_first("status", "phase") or "UNKNOWN").upper(),
|
|
856
|
+
"endpoint": str(
|
|
857
|
+
quick_access.get("public_endpoint")
|
|
858
|
+
or quick_access.get("publicEndpoint")
|
|
859
|
+
or quick_access.get("private_endpoint")
|
|
860
|
+
or quick_access.get("privateEndpoint")
|
|
861
|
+
or lifecycle_first("endpoint")
|
|
862
|
+
or ""
|
|
863
|
+
).strip() or None,
|
|
864
|
+
"framework": runtime_type or None,
|
|
865
|
+
"runtimeType": runtime_type or None,
|
|
866
|
+
"capabilities": capabilities if isinstance(capabilities, dict) else None,
|
|
867
|
+
"chatTransport": chat_transport,
|
|
868
|
+
"chatRoutingReason": chat_routing_reason,
|
|
869
|
+
"region": str(first("region") or "").strip() or None,
|
|
870
|
+
"instanceId": str(first("instance_id", "instanceId") or "").strip() or None,
|
|
871
|
+
"versionId": version_id or None,
|
|
872
|
+
"updatedAt": str(
|
|
873
|
+
lifecycle_first("updated_at", "updatedAt", "update_time", "updateTime") or ""
|
|
874
|
+
).strip()
|
|
875
|
+
or None,
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async def list_account_agents(self, *, page: int, size: int) -> dict[str, Any]:
|
|
879
|
+
payload = await self.client.list_agents(page=page, page_size=size)
|
|
880
|
+
raw_items = payload.get("agents") or payload.get("Agents") or []
|
|
881
|
+
items = [
|
|
882
|
+
self._account_agent_view(item)
|
|
883
|
+
for item in raw_items
|
|
884
|
+
if isinstance(item, dict)
|
|
885
|
+
]
|
|
886
|
+
items = [
|
|
887
|
+
item
|
|
888
|
+
for item in items
|
|
889
|
+
if item["agentId"] and item["status"] != "DELETED"
|
|
890
|
+
]
|
|
891
|
+
return {
|
|
892
|
+
"items": items,
|
|
893
|
+
"total": len(items),
|
|
894
|
+
"page": page,
|
|
895
|
+
"size": size,
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async def get_account_agent(self, agent_id: str) -> dict[str, Any]:
|
|
899
|
+
normalized_id = str(agent_id or "").strip()
|
|
900
|
+
if not normalized_id:
|
|
901
|
+
raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 标识不能为空", status_code=404)
|
|
902
|
+
payload = await self.client.get_agent(agent_id=normalized_id)
|
|
903
|
+
return self._account_agent_view(payload, fallback_id=normalized_id)
|
|
904
|
+
|
|
905
|
+
@staticmethod
|
|
906
|
+
def _account_agent_version_view(payload: dict[str, Any]) -> dict[str, Any]:
|
|
907
|
+
def first(*names: str, default: Any = None) -> Any:
|
|
908
|
+
for name in names:
|
|
909
|
+
if name in payload and payload[name] is not None:
|
|
910
|
+
return payload[name]
|
|
911
|
+
return default
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
"versionId": str(first("version_id", "VersionId", default="") or "").strip(),
|
|
915
|
+
"versionName": str(
|
|
916
|
+
first("version_name", "VersionName", default="") or ""
|
|
917
|
+
).strip(),
|
|
918
|
+
"tag": str(first("tag", "Tag", default="") or "").strip(),
|
|
919
|
+
"status": str(first("status", "Status", default="") or "").strip(),
|
|
920
|
+
"trafficPercentage": int(
|
|
921
|
+
first("traffic_percentage", "TrafficPercentage", default=0) or 0
|
|
922
|
+
),
|
|
923
|
+
"canRollback": first("can_rollback", "CanRollback", default=False) is True,
|
|
924
|
+
"rollbackDisabledReason": str(
|
|
925
|
+
first(
|
|
926
|
+
"rollback_disabled_reason",
|
|
927
|
+
"RollbackDisabledReason",
|
|
928
|
+
default="",
|
|
929
|
+
)
|
|
930
|
+
or ""
|
|
931
|
+
).strip(),
|
|
932
|
+
"createdAt": str(first("created_at", "CreatedAt", default="") or "").strip()
|
|
933
|
+
or None,
|
|
934
|
+
"createdBy": str(first("created_by", "CreatedBy", default="") or "").strip(),
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async def list_account_agent_versions(
|
|
938
|
+
self, agent_id: str, *, page: int, size: int
|
|
939
|
+
) -> dict[str, Any]:
|
|
940
|
+
normalized_id = str(agent_id or "").strip()
|
|
941
|
+
if not normalized_id:
|
|
942
|
+
raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 标识不能为空", status_code=404)
|
|
943
|
+
try:
|
|
944
|
+
payload = await self.client.list_versions(normalized_id, page=page, size=size)
|
|
945
|
+
except AgentEngineAPIError as exc:
|
|
946
|
+
raise StudioError(
|
|
947
|
+
"CLOUD_AGENT_VERSION_DIRECTORY_FAILED",
|
|
948
|
+
exc.message,
|
|
949
|
+
status_code=502,
|
|
950
|
+
details={
|
|
951
|
+
"serverCode": exc.raw_code,
|
|
952
|
+
**{
|
|
953
|
+
key: value
|
|
954
|
+
for key, value in exc.details.items()
|
|
955
|
+
if key in {"request_id", "action"}
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
) from exc
|
|
959
|
+
raw_items = payload.get("versions") or payload.get("Versions") or []
|
|
960
|
+
items = [
|
|
961
|
+
self._account_agent_version_view(item)
|
|
962
|
+
for item in raw_items
|
|
963
|
+
if isinstance(item, dict)
|
|
964
|
+
]
|
|
965
|
+
items = [item for item in items if item["versionId"]]
|
|
966
|
+
current = next(
|
|
967
|
+
(
|
|
968
|
+
item["versionId"]
|
|
969
|
+
for item in items
|
|
970
|
+
if str(item["status"]).strip().lower() == "current"
|
|
971
|
+
),
|
|
972
|
+
None,
|
|
973
|
+
)
|
|
974
|
+
return {
|
|
975
|
+
"items": items,
|
|
976
|
+
"total": int(
|
|
977
|
+
payload.get("total_count")
|
|
978
|
+
or payload.get("TotalCount")
|
|
979
|
+
or len(items)
|
|
980
|
+
),
|
|
981
|
+
"currentVersionId": current,
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async def rollback_account_agent_version(
|
|
985
|
+
self, agent_id: str, *, version_id: str
|
|
986
|
+
) -> dict[str, Any]:
|
|
987
|
+
normalized_id = str(agent_id or "").strip()
|
|
988
|
+
normalized_version_id = str(version_id or "").strip()
|
|
989
|
+
if not normalized_id:
|
|
990
|
+
raise StudioError("CLOUD_AGENT_NOT_FOUND", "云端 Agent 标识不能为空", status_code=404)
|
|
991
|
+
if not normalized_version_id:
|
|
992
|
+
raise StudioError(
|
|
993
|
+
"CLOUD_AGENT_VERSION_REQUIRED",
|
|
994
|
+
"请选择要回滚的云端版本",
|
|
995
|
+
status_code=422,
|
|
996
|
+
field="versionId",
|
|
997
|
+
)
|
|
998
|
+
try:
|
|
999
|
+
payload = await self.client.rollback_version(
|
|
1000
|
+
normalized_id,
|
|
1001
|
+
target_version_id=normalized_version_id,
|
|
1002
|
+
)
|
|
1003
|
+
except AgentEngineAPIError as exc:
|
|
1004
|
+
raise StudioError(
|
|
1005
|
+
"CLOUD_AGENT_VERSION_ROLLBACK_FAILED",
|
|
1006
|
+
exc.message,
|
|
1007
|
+
status_code=502,
|
|
1008
|
+
details={
|
|
1009
|
+
"serverCode": exc.raw_code,
|
|
1010
|
+
**{
|
|
1011
|
+
key: value
|
|
1012
|
+
for key, value in exc.details.items()
|
|
1013
|
+
if key in {"request_id", "action"}
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
) from exc
|
|
1017
|
+
return {
|
|
1018
|
+
"agentId": str(payload.get("agent_id") or normalized_id),
|
|
1019
|
+
"targetVersionId": str(
|
|
1020
|
+
payload.get("target_version_id") or normalized_version_id
|
|
1021
|
+
),
|
|
1022
|
+
"status": str(payload.get("status") or "UPDATING"),
|
|
1023
|
+
"noop": bool(payload.get("noop")),
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async def get_account_agent_dashboard_access(
|
|
1027
|
+
self, agent_id: str
|
|
1028
|
+
) -> dict[str, str | None]:
|
|
1029
|
+
detail = await self.get_account_agent(agent_id)
|
|
1030
|
+
path = (
|
|
1031
|
+
_NATIVE_DASHBOARD_UI_PATH
|
|
1032
|
+
if detail.get("chatTransport") == "official-dashboard"
|
|
1033
|
+
else _HOSTED_AGENT_UI_PATH
|
|
1034
|
+
)
|
|
1035
|
+
link = await self.client.create_dashboard_access_link(
|
|
1036
|
+
agent_id=agent_id,
|
|
1037
|
+
link_type="private",
|
|
1038
|
+
path=path,
|
|
1039
|
+
)
|
|
1040
|
+
access_url = str(link.get("access_url") or "").strip()
|
|
1041
|
+
if not access_url:
|
|
1042
|
+
raise StudioError(
|
|
1043
|
+
"DEPLOYMENT_DASHBOARD_UNAVAILABLE",
|
|
1044
|
+
"云端未返回可用的 Agent UI 地址",
|
|
1045
|
+
status_code=502,
|
|
1046
|
+
)
|
|
1047
|
+
return {
|
|
1048
|
+
"access_url": access_url,
|
|
1049
|
+
"agent_id": agent_id,
|
|
1050
|
+
"instance_id": None,
|
|
1051
|
+
"expires_at": str(link.get("expires_at") or "").strip() or None,
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async def delete_account_agent(self, agent_id: str) -> bool:
|
|
1055
|
+
await self.get_account_agent(agent_id)
|
|
1056
|
+
try:
|
|
1057
|
+
deleted = await self.client.delete_agent(agent_id)
|
|
1058
|
+
except AgentEngineAPIError as exc:
|
|
1059
|
+
if exc.code == 404 or exc.details.get("http_status") == 404:
|
|
1060
|
+
return True
|
|
1061
|
+
raise
|
|
1062
|
+
if not deleted:
|
|
1063
|
+
raise StudioError(
|
|
1064
|
+
"CLOUD_AGENT_DELETE_FAILED",
|
|
1065
|
+
"云端未确认 Agent 删除结果",
|
|
1066
|
+
status_code=502,
|
|
1067
|
+
)
|
|
1068
|
+
return True
|
|
1069
|
+
|
|
1070
|
+
def _chat_agent_id(
|
|
1071
|
+
self, deployment: DeploymentRecord | AccountCloudAgentReference
|
|
1072
|
+
) -> str:
|
|
1073
|
+
"""Bind local cloud chat to an immutable Studio deployment receipt.
|
|
1074
|
+
|
|
1075
|
+
In particular, the browser cannot provide an arbitrary AgentId and
|
|
1076
|
+
turn the loopback Studio process into a signed control-plane proxy.
|
|
1077
|
+
"""
|
|
1078
|
+
|
|
1079
|
+
agent_id = str(deployment.agent_id or "").strip()
|
|
1080
|
+
if not agent_id:
|
|
1081
|
+
raise StudioError(
|
|
1082
|
+
"DEPLOYMENT_CLOUD_CHAT_UNAVAILABLE",
|
|
1083
|
+
"部署 receipt 缺少云端 Agent 标识",
|
|
1084
|
+
status_code=409,
|
|
1085
|
+
)
|
|
1086
|
+
return agent_id
|
|
1087
|
+
|
|
1088
|
+
async def list_deployment_chat_sessions(
|
|
1089
|
+
self,
|
|
1090
|
+
deployment: DeploymentRecord,
|
|
1091
|
+
*,
|
|
1092
|
+
page: int = 1,
|
|
1093
|
+
size: int = 50,
|
|
1094
|
+
) -> dict[str, Any]:
|
|
1095
|
+
"""Read sessions through the authenticated Server projection."""
|
|
1096
|
+
|
|
1097
|
+
return await self.client.list_sessions(
|
|
1098
|
+
self._chat_agent_id(deployment), page=page, size=size
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
async def create_deployment_chat_session(self, deployment: DeploymentRecord) -> dict[str, Any]:
|
|
1102
|
+
"""Create one Server-owned session before its first cloud message."""
|
|
1103
|
+
|
|
1104
|
+
return await self.client.create_session(self._chat_agent_id(deployment))
|
|
1105
|
+
|
|
1106
|
+
async def list_deployment_chat_messages(
|
|
1107
|
+
self,
|
|
1108
|
+
deployment: DeploymentRecord,
|
|
1109
|
+
*,
|
|
1110
|
+
session_id: str,
|
|
1111
|
+
after_seq_id: int | None = None,
|
|
1112
|
+
limit: int = 100,
|
|
1113
|
+
) -> dict[str, Any]:
|
|
1114
|
+
"""Return the Server/Runtime message projection for a bound session."""
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
return await self.client.list_session_messages(
|
|
1118
|
+
agent_id=self._chat_agent_id(deployment),
|
|
1119
|
+
session_id=session_id,
|
|
1120
|
+
after_seq_id=after_seq_id,
|
|
1121
|
+
limit=limit,
|
|
1122
|
+
)
|
|
1123
|
+
except AgentEngineAPIError as exc:
|
|
1124
|
+
# A poll already in flight can complete after DeleteSession. The
|
|
1125
|
+
# deleted projection is an empty terminal view for Studio, not a
|
|
1126
|
+
# local API failure that should surface as a 500/toast.
|
|
1127
|
+
if exc.code == 404 or exc.details.get("http_status") == 404:
|
|
1128
|
+
return {"messages": [], "session_deleted": True}
|
|
1129
|
+
raise
|
|
1130
|
+
|
|
1131
|
+
async def delete_deployment_chat_session(
|
|
1132
|
+
self, deployment: DeploymentRecord, *, session_id: str
|
|
1133
|
+
) -> bool:
|
|
1134
|
+
"""Delete only a session that Server scopes to the signed caller."""
|
|
1135
|
+
|
|
1136
|
+
# The Server resolves session ownership from the authenticated caller;
|
|
1137
|
+
# the receipt binding above only establishes the enclosing Agent scope.
|
|
1138
|
+
self._chat_agent_id(deployment)
|
|
1139
|
+
deleted = await self.client.delete_session(session_id)
|
|
1140
|
+
if not deleted:
|
|
1141
|
+
raise StudioError(
|
|
1142
|
+
"CLOUD_CHAT_SESSION_DELETE_FAILED",
|
|
1143
|
+
"云端会话删除失败,请刷新后重试",
|
|
1144
|
+
status_code=502,
|
|
1145
|
+
)
|
|
1146
|
+
return True
|
|
1147
|
+
|
|
1148
|
+
async def list_deployment_chat_events(
|
|
1149
|
+
self,
|
|
1150
|
+
deployment: DeploymentRecord,
|
|
1151
|
+
*,
|
|
1152
|
+
session_id: str,
|
|
1153
|
+
after_seq_id: int | None = None,
|
|
1154
|
+
limit: int = 200,
|
|
1155
|
+
) -> dict[str, Any]:
|
|
1156
|
+
"""Read canonical events, including public Interaction/v1 frames."""
|
|
1157
|
+
|
|
1158
|
+
try:
|
|
1159
|
+
return await self.client.list_session_events(
|
|
1160
|
+
agent_id=self._chat_agent_id(deployment),
|
|
1161
|
+
session_id=session_id,
|
|
1162
|
+
after_seq_id=after_seq_id,
|
|
1163
|
+
limit=limit,
|
|
1164
|
+
)
|
|
1165
|
+
except AgentEngineAPIError as exc:
|
|
1166
|
+
if exc.code == 404 or exc.details.get("http_status") == 404:
|
|
1167
|
+
return {"events": [], "session_deleted": True}
|
|
1168
|
+
raise
|
|
1169
|
+
|
|
1170
|
+
async def send_deployment_chat_message(
|
|
1171
|
+
self,
|
|
1172
|
+
deployment: DeploymentRecord,
|
|
1173
|
+
*,
|
|
1174
|
+
session_id: str,
|
|
1175
|
+
content: Any,
|
|
1176
|
+
model: str | None = None,
|
|
1177
|
+
model_options: dict[str, Any] | None = None,
|
|
1178
|
+
tool_approval_mode: str | None = None,
|
|
1179
|
+
collaboration_mode: str | None = None,
|
|
1180
|
+
goal_objective: str | None = None,
|
|
1181
|
+
) -> dict[str, Any]:
|
|
1182
|
+
"""Submit a foreground message via RunAgent and Server admission.
|
|
1183
|
+
|
|
1184
|
+
Kernel-enabled Agents return a durable receipt immediately; clients
|
|
1185
|
+
then read the canonical message/session event stream rather than
|
|
1186
|
+
treating a synchronous proxy body as the source of truth.
|
|
1187
|
+
"""
|
|
1188
|
+
|
|
1189
|
+
kwargs: dict[str, Any] = {
|
|
1190
|
+
"session_id": session_id,
|
|
1191
|
+
"model": model,
|
|
1192
|
+
"model_options": model_options,
|
|
1193
|
+
"tool_approval_mode": tool_approval_mode,
|
|
1194
|
+
"collaboration_mode": collaboration_mode,
|
|
1195
|
+
"goal_objective": goal_objective,
|
|
1196
|
+
}
|
|
1197
|
+
return await self.client.chat(
|
|
1198
|
+
self._chat_agent_id(deployment),
|
|
1199
|
+
content,
|
|
1200
|
+
**kwargs,
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
async def stream_deployment_chat_message(
|
|
1204
|
+
self,
|
|
1205
|
+
deployment: DeploymentRecord | AccountCloudAgentReference,
|
|
1206
|
+
*,
|
|
1207
|
+
session_id: str,
|
|
1208
|
+
content: Any,
|
|
1209
|
+
model: str | None = None,
|
|
1210
|
+
model_options: dict[str, Any] | None = None,
|
|
1211
|
+
tool_approval_mode: str | None = None,
|
|
1212
|
+
collaboration_mode: str | None = None,
|
|
1213
|
+
goal_objective: str | None = None,
|
|
1214
|
+
) -> AsyncIterator[bytes]:
|
|
1215
|
+
"""Open the Server-admitted foreground RunAgent SSE connection."""
|
|
1216
|
+
|
|
1217
|
+
try:
|
|
1218
|
+
return await self.stream_client.chat_stream(
|
|
1219
|
+
self._chat_agent_id(deployment),
|
|
1220
|
+
content,
|
|
1221
|
+
session_id=session_id,
|
|
1222
|
+
model=model,
|
|
1223
|
+
model_options=model_options,
|
|
1224
|
+
tool_approval_mode=tool_approval_mode,
|
|
1225
|
+
collaboration_mode=collaboration_mode,
|
|
1226
|
+
goal_objective=goal_objective,
|
|
1227
|
+
)
|
|
1228
|
+
except AgentEngineAPIError as exc:
|
|
1229
|
+
raise StudioError(
|
|
1230
|
+
"CLOUD_CHAT_STREAM_FAILED",
|
|
1231
|
+
exc.message,
|
|
1232
|
+
status_code=502,
|
|
1233
|
+
details={
|
|
1234
|
+
"serverCode": exc.raw_code,
|
|
1235
|
+
**{
|
|
1236
|
+
key: value
|
|
1237
|
+
for key, value in exc.details.items()
|
|
1238
|
+
if key in {"request_id", "action", "http_status"}
|
|
1239
|
+
},
|
|
1240
|
+
},
|
|
1241
|
+
) from exc
|
|
1242
|
+
|
|
1243
|
+
async def list_deployment_chat_models(
|
|
1244
|
+
self, deployment: DeploymentRecord
|
|
1245
|
+
) -> dict[str, Any]:
|
|
1246
|
+
"""Read the Server-authoritative model catalog for this Agent."""
|
|
1247
|
+
|
|
1248
|
+
return await self.client.list_agent_models(
|
|
1249
|
+
agent_id=self._chat_agent_id(deployment)
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
async def submit_deployment_chat_interaction(
|
|
1253
|
+
self,
|
|
1254
|
+
deployment: DeploymentRecord,
|
|
1255
|
+
*,
|
|
1256
|
+
session_id: str,
|
|
1257
|
+
run_id: str,
|
|
1258
|
+
interaction_id: str,
|
|
1259
|
+
expected_revision: int,
|
|
1260
|
+
action: str,
|
|
1261
|
+
response: dict[str, Any],
|
|
1262
|
+
idempotency_key: str,
|
|
1263
|
+
) -> dict[str, Any]:
|
|
1264
|
+
"""Forward only Interaction/v1's caller-visible response fields."""
|
|
1265
|
+
|
|
1266
|
+
return await self.client.submit_interaction(
|
|
1267
|
+
agent_id=self._chat_agent_id(deployment),
|
|
1268
|
+
session_id=session_id,
|
|
1269
|
+
run_id=run_id,
|
|
1270
|
+
interaction_id=interaction_id,
|
|
1271
|
+
expected_revision=expected_revision,
|
|
1272
|
+
action=action,
|
|
1273
|
+
response=response,
|
|
1274
|
+
idempotency_key=idempotency_key,
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
async def create_managed_runtime_deployment(self, **kwargs) -> DeploymentRecord:
|
|
1278
|
+
request: DeploymentRequest = kwargs["request"]
|
|
1279
|
+
digest = str(kwargs["manifest_digest"])
|
|
1280
|
+
runtime_environment = dict(kwargs.get("runtime_environment") or {})
|
|
1281
|
+
result = await self.client.create_agent(
|
|
1282
|
+
self._managed_runtime_payload(
|
|
1283
|
+
agent_name=str(kwargs["agent_name"]),
|
|
1284
|
+
manifest=str(kwargs["manifest"]),
|
|
1285
|
+
runtime_name=str(kwargs["runtime_name"]),
|
|
1286
|
+
runtime_version=str(kwargs["runtime_version"]),
|
|
1287
|
+
request=request,
|
|
1288
|
+
runtime_environment=runtime_environment,
|
|
1289
|
+
)
|
|
1290
|
+
)
|
|
1291
|
+
agent_id = str(result.get("agent_id") or "").strip()
|
|
1292
|
+
if not agent_id:
|
|
1293
|
+
raise StudioError(
|
|
1294
|
+
"CLOUD_DEPLOYMENT_PROTOCOL_INVALID",
|
|
1295
|
+
"CreateAgentProduct 未返回 AgentId",
|
|
1296
|
+
status_code=502,
|
|
1297
|
+
)
|
|
1298
|
+
return DeploymentRecord(
|
|
1299
|
+
id=f"dep_{uuid4().hex}",
|
|
1300
|
+
build_id=str(kwargs["build_id"]),
|
|
1301
|
+
bundle_digest=f"sha256:{digest}",
|
|
1302
|
+
version_id=f"managed-{digest[:16]}",
|
|
1303
|
+
status="DEPLOYING",
|
|
1304
|
+
target=request.target,
|
|
1305
|
+
agent_id=agent_id,
|
|
1306
|
+
instance_id=str(result.get("instance_id") or "").strip() or None,
|
|
1307
|
+
endpoint=str(result.get("endpoint") or "").strip() or None,
|
|
1308
|
+
artifact_id="managed-runtime",
|
|
1309
|
+
requires_kernel=True,
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
async def replace_managed_runtime_deployment(
|
|
1313
|
+
self, deployment: DeploymentRecord, **kwargs
|
|
1314
|
+
) -> DeploymentRecord:
|
|
1315
|
+
if not deployment.agent_id:
|
|
1316
|
+
raise StudioError(
|
|
1317
|
+
"DEPLOYMENT_PROTOCOL_INVALID",
|
|
1318
|
+
"部署 receipt 缺少 AgentId",
|
|
1319
|
+
status_code=502,
|
|
1320
|
+
)
|
|
1321
|
+
request: DeploymentRequest = kwargs["request"]
|
|
1322
|
+
digest = str(kwargs["manifest_digest"])
|
|
1323
|
+
runtime_environment = dict(kwargs.get("runtime_environment") or {})
|
|
1324
|
+
update_payload: dict[str, Any] = {
|
|
1325
|
+
"artifact_type": "ManagedRuntime",
|
|
1326
|
+
# UpdateAgent resolves a fresh immutable runtime image from the
|
|
1327
|
+
# complete YAML declaration. RuntimeConfig is the resolved
|
|
1328
|
+
# read-model and cannot be used as an input for a retry.
|
|
1329
|
+
"managed_runtime_config": {
|
|
1330
|
+
"runtime_name": str(kwargs["runtime_name"]),
|
|
1331
|
+
"runtime_version": str(kwargs["runtime_version"]),
|
|
1332
|
+
"manifest": str(kwargs["manifest"]),
|
|
1333
|
+
},
|
|
1334
|
+
}
|
|
1335
|
+
if runtime_environment:
|
|
1336
|
+
# A changed MCP/model binding can introduce a new credential ref.
|
|
1337
|
+
# Re-resolve it for every immutable revision instead of relying on
|
|
1338
|
+
# the environment captured by the first CreateAgent call.
|
|
1339
|
+
update_payload["environment_variables"] = runtime_environment
|
|
1340
|
+
await self.client.update_agent(
|
|
1341
|
+
deployment.agent_id,
|
|
1342
|
+
update_payload,
|
|
1343
|
+
)
|
|
1344
|
+
return DeploymentRecord(
|
|
1345
|
+
id=f"dep_{uuid4().hex}",
|
|
1346
|
+
build_id=str(kwargs["build_id"]),
|
|
1347
|
+
bundle_digest=f"sha256:{digest}",
|
|
1348
|
+
version_id=f"managed-{digest[:16]}",
|
|
1349
|
+
status="DEPLOYING",
|
|
1350
|
+
target=request.target,
|
|
1351
|
+
agent_id=deployment.agent_id,
|
|
1352
|
+
instance_id=deployment.instance_id,
|
|
1353
|
+
endpoint=deployment.endpoint,
|
|
1354
|
+
artifact_id="managed-runtime",
|
|
1355
|
+
requires_kernel=True,
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
def _bundle_for_deployment(self, bundle_uri: str, bundle_digest: Any) -> dict[str, str]:
|
|
1359
|
+
bundle = self._bundles.get(bundle_uri)
|
|
1360
|
+
if bundle is None or bundle["bundle_digest"] != str(bundle_digest):
|
|
1361
|
+
raise StudioError(
|
|
1362
|
+
"CLOUD_BUNDLE_REFERENCE_INVALID",
|
|
1363
|
+
"Bundle 引用或 digest 不匹配",
|
|
1364
|
+
status_code=502,
|
|
1365
|
+
)
|
|
1366
|
+
return bundle
|
|
1367
|
+
|
|
1368
|
+
def _create_payload(
|
|
1369
|
+
self,
|
|
1370
|
+
*,
|
|
1371
|
+
bundle_uri: str,
|
|
1372
|
+
bundle: dict[str, str],
|
|
1373
|
+
request: DeploymentRequest,
|
|
1374
|
+
) -> dict[str, Any]:
|
|
1375
|
+
return {
|
|
1376
|
+
"name": _server_agent_name(bundle["agent_id"]),
|
|
1377
|
+
"description": "Created by AgentKit Studio",
|
|
1378
|
+
"framework": bundle["runtime_type"],
|
|
1379
|
+
"artifact_type": "Code",
|
|
1380
|
+
"artifact_path": bundle_uri,
|
|
1381
|
+
"code_checksum": bundle["archive_sha"],
|
|
1382
|
+
"code_command": list(_STUDIO_CODE_COMMAND),
|
|
1383
|
+
"region": request.target.region,
|
|
1384
|
+
"ks3": self._code_config(bundle["bucket"]),
|
|
1385
|
+
"resources": {"cpu": 2, "memory": "4Gi"},
|
|
1386
|
+
"scaling": {"min_replicas": 1, "max_replicas": 1, "concurrency": 20},
|
|
1387
|
+
"auth_type": "ApiKey",
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
@staticmethod
|
|
1391
|
+
def _managed_runtime_payload(
|
|
1392
|
+
*,
|
|
1393
|
+
agent_name: str,
|
|
1394
|
+
manifest: str,
|
|
1395
|
+
runtime_name: str,
|
|
1396
|
+
runtime_version: str,
|
|
1397
|
+
request: DeploymentRequest,
|
|
1398
|
+
runtime_environment: dict[str, str] | None = None,
|
|
1399
|
+
) -> dict[str, Any]:
|
|
1400
|
+
payload = {
|
|
1401
|
+
"name": _server_agent_name(agent_name),
|
|
1402
|
+
"description": "Created by AgentKit Studio",
|
|
1403
|
+
"framework": runtime_name,
|
|
1404
|
+
"artifact_type": "ManagedRuntime",
|
|
1405
|
+
# ManagedRuntimeConfig is the public declaration accepted by
|
|
1406
|
+
# CreateAgent. RuntimeConfig is only the Server-resolved state.
|
|
1407
|
+
"managed_runtime_config": {
|
|
1408
|
+
"runtime_name": runtime_name,
|
|
1409
|
+
"runtime_version": runtime_version,
|
|
1410
|
+
"manifest": manifest,
|
|
1411
|
+
},
|
|
1412
|
+
"region": request.target.region,
|
|
1413
|
+
# YAML agents run from a platform-owned runtime image and have no
|
|
1414
|
+
# user code bundle to build or unpack. Keep their default small;
|
|
1415
|
+
# high-code deployments retain their separate 2 CPU / 4 GiB
|
|
1416
|
+
# profile in _create_payload above.
|
|
1417
|
+
"resources": {"cpu": 1, "memory": "2Gi"},
|
|
1418
|
+
"scaling": {"min_replicas": 1, "max_replicas": 1, "concurrency": 20},
|
|
1419
|
+
"auth_type": "ApiKey",
|
|
1420
|
+
}
|
|
1421
|
+
if runtime_environment:
|
|
1422
|
+
# Model credentials are resolved only for this in-memory deployment
|
|
1423
|
+
# request. They are never written into the YAML build or local
|
|
1424
|
+
# deployment receipt.
|
|
1425
|
+
payload["environment_variables"] = dict(runtime_environment)
|
|
1426
|
+
return payload
|
|
1427
|
+
|
|
1428
|
+
def _code_config(self, bucket: str) -> dict[str, str]:
|
|
1429
|
+
return {
|
|
1430
|
+
**self._ks3_credentials,
|
|
1431
|
+
"region": self.ks3_region,
|
|
1432
|
+
"bucket": bucket,
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
@staticmethod
|
|
1436
|
+
def _receipt(
|
|
1437
|
+
*,
|
|
1438
|
+
build_id: str,
|
|
1439
|
+
bundle_digest: str,
|
|
1440
|
+
bundle_uri: str,
|
|
1441
|
+
agent_id: str,
|
|
1442
|
+
instance_id: str | None,
|
|
1443
|
+
endpoint: str | None,
|
|
1444
|
+
target,
|
|
1445
|
+
status: str,
|
|
1446
|
+
) -> DeploymentRecord:
|
|
1447
|
+
return DeploymentRecord(
|
|
1448
|
+
id=f"dep_{uuid4().hex}",
|
|
1449
|
+
build_id=build_id,
|
|
1450
|
+
bundle_digest=bundle_digest,
|
|
1451
|
+
version_id=f"bundle-{hashlib.sha256(bundle_uri.encode('utf-8')).hexdigest()[:16]}",
|
|
1452
|
+
status=cast(Any, status),
|
|
1453
|
+
target=target,
|
|
1454
|
+
agent_id=agent_id,
|
|
1455
|
+
instance_id=instance_id,
|
|
1456
|
+
endpoint=endpoint,
|
|
1457
|
+
bundle_uri=bundle_uri,
|
|
1458
|
+
requires_kernel=True,
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def _server_agent_name(agent_id: str) -> str:
|
|
1463
|
+
normalized = "".join(
|
|
1464
|
+
char if char.isalnum() or char == "-" else "-" for char in agent_id.lower()
|
|
1465
|
+
)
|
|
1466
|
+
normalized = normalized.strip("-") or "agent"
|
|
1467
|
+
if not normalized[0].isalpha():
|
|
1468
|
+
normalized = f"agent-{normalized}"
|
|
1469
|
+
if not normalized.startswith("studio-"):
|
|
1470
|
+
normalized = f"studio-{normalized}"
|
|
1471
|
+
return normalized[:63].rstrip("-")
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def _safe_object_component(value: str) -> str:
|
|
1475
|
+
"""Keep an immutable KS3 key below the Studio-owned prefix."""
|
|
1476
|
+
|
|
1477
|
+
normalized = "".join(
|
|
1478
|
+
char if char.isalnum() or char in {"-", "_"} else "-" for char in value.lower()
|
|
1479
|
+
).strip("-")
|
|
1480
|
+
return normalized[:96] or "agent"
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
class CloudDeploymentService:
|
|
1484
|
+
def __init__(
|
|
1485
|
+
self,
|
|
1486
|
+
workspace: Workspace,
|
|
1487
|
+
*,
|
|
1488
|
+
gateway: CloudDeploymentGateway,
|
|
1489
|
+
build_repository: BuildRepository | None = None,
|
|
1490
|
+
) -> None:
|
|
1491
|
+
self.workspace = workspace
|
|
1492
|
+
self.gateway = gateway
|
|
1493
|
+
self.build_repository = build_repository or BuildRepository(workspace)
|
|
1494
|
+
|
|
1495
|
+
async def deploy(
|
|
1496
|
+
self,
|
|
1497
|
+
build_id: str,
|
|
1498
|
+
request: DeploymentRequest,
|
|
1499
|
+
) -> DeploymentRecord:
|
|
1500
|
+
return await self._deploy_build(build_id, request)
|
|
1501
|
+
|
|
1502
|
+
async def deploy_managed_runtime(
|
|
1503
|
+
self,
|
|
1504
|
+
*,
|
|
1505
|
+
build_id: str,
|
|
1506
|
+
agent_name: str,
|
|
1507
|
+
manifest: str,
|
|
1508
|
+
runtime_name: str,
|
|
1509
|
+
runtime_version: str,
|
|
1510
|
+
manifest_digest: str,
|
|
1511
|
+
request: DeploymentRequest,
|
|
1512
|
+
runtime_environment: dict[str, str] | None = None,
|
|
1513
|
+
replacing: DeploymentRecord | None = None,
|
|
1514
|
+
) -> DeploymentRecord:
|
|
1515
|
+
# A YAML/ManagedRuntime revision is deliberately not a migration
|
|
1516
|
+
# mechanism for a user-code Agent. In particular, Studio may manage
|
|
1517
|
+
# the lifecycle of an existing Code deployment, but it must never
|
|
1518
|
+
# replace that deployment's artifact with a generated declaration.
|
|
1519
|
+
# The receipt is the local proof that this Agent was created through
|
|
1520
|
+
# the ManagedRuntime path in the first place.
|
|
1521
|
+
if replacing is not None and replacing.artifact_id != "managed-runtime":
|
|
1522
|
+
raise StudioError(
|
|
1523
|
+
"MANAGED_RUNTIME_REPLACEMENT_FORBIDDEN",
|
|
1524
|
+
"声明式 YAML 只能更新由 Studio 声明式路径创建的 Agent,不能覆盖高代码 Agent",
|
|
1525
|
+
status_code=409,
|
|
1526
|
+
details={"deploymentId": replacing.id},
|
|
1527
|
+
)
|
|
1528
|
+
if replacing is None:
|
|
1529
|
+
record = await self.gateway.create_managed_runtime_deployment(
|
|
1530
|
+
build_id=build_id,
|
|
1531
|
+
agent_name=agent_name,
|
|
1532
|
+
manifest=manifest,
|
|
1533
|
+
runtime_name=runtime_name,
|
|
1534
|
+
runtime_version=runtime_version,
|
|
1535
|
+
manifest_digest=manifest_digest,
|
|
1536
|
+
request=request,
|
|
1537
|
+
runtime_environment=runtime_environment,
|
|
1538
|
+
)
|
|
1539
|
+
else:
|
|
1540
|
+
record = await self.gateway.replace_managed_runtime_deployment(
|
|
1541
|
+
replacing,
|
|
1542
|
+
build_id=build_id,
|
|
1543
|
+
manifest=manifest,
|
|
1544
|
+
runtime_name=runtime_name,
|
|
1545
|
+
runtime_version=runtime_version,
|
|
1546
|
+
manifest_digest=manifest_digest,
|
|
1547
|
+
request=request,
|
|
1548
|
+
runtime_environment=runtime_environment,
|
|
1549
|
+
)
|
|
1550
|
+
self._save(record, request)
|
|
1551
|
+
return record
|
|
1552
|
+
|
|
1553
|
+
async def _deploy_build(
|
|
1554
|
+
self,
|
|
1555
|
+
build_id: str,
|
|
1556
|
+
request: DeploymentRequest,
|
|
1557
|
+
*,
|
|
1558
|
+
replacing: DeploymentRecord | None = None,
|
|
1559
|
+
) -> DeploymentRecord:
|
|
1560
|
+
build, bundle, provenance = self._prepared_build(build_id)
|
|
1561
|
+
bundle_uri = await self.gateway.upload_bundle(
|
|
1562
|
+
bundle=bundle,
|
|
1563
|
+
bundle_digest=build.bundle_digest,
|
|
1564
|
+
provenance=provenance,
|
|
1565
|
+
)
|
|
1566
|
+
version_id = await self.gateway.create_version(
|
|
1567
|
+
agent_id=build.agent_id,
|
|
1568
|
+
bundle_uri=bundle_uri,
|
|
1569
|
+
bundle_digest=build.bundle_digest,
|
|
1570
|
+
provenance=provenance,
|
|
1571
|
+
)
|
|
1572
|
+
if replacing is None:
|
|
1573
|
+
record = await self.gateway.create_deployment(
|
|
1574
|
+
build_id=build_id,
|
|
1575
|
+
version_id=version_id,
|
|
1576
|
+
bundle_digest=build.bundle_digest,
|
|
1577
|
+
request=request,
|
|
1578
|
+
)
|
|
1579
|
+
else:
|
|
1580
|
+
record = await self.gateway.replace_deployment(
|
|
1581
|
+
replacing,
|
|
1582
|
+
build_id=build_id,
|
|
1583
|
+
version_id=version_id,
|
|
1584
|
+
bundle_digest=build.bundle_digest,
|
|
1585
|
+
request=request,
|
|
1586
|
+
)
|
|
1587
|
+
self._save(record, request)
|
|
1588
|
+
return record
|
|
1589
|
+
|
|
1590
|
+
def _prepared_build(self, build_id: str) -> tuple[BuildRecord, bytes, dict[str, Any]]:
|
|
1591
|
+
build = self.build_repository.get(build_id)
|
|
1592
|
+
if build.status != BuildStatus.SUCCEEDED or not build.artifact_path:
|
|
1593
|
+
raise StudioError(
|
|
1594
|
+
"BUILD_NOT_READY",
|
|
1595
|
+
"只有成功 Build 可以部署",
|
|
1596
|
+
status_code=409,
|
|
1597
|
+
)
|
|
1598
|
+
archive = self.workspace.resolve(build.artifact_path, must_exist=True)
|
|
1599
|
+
bundle = archive.read_bytes()
|
|
1600
|
+
checked_bundle = (
|
|
1601
|
+
preflight_hosted_kernel_bundle(bundle)
|
|
1602
|
+
if getattr(self.gateway, "requires_hosted_kernel_bundle_preflight", False)
|
|
1603
|
+
else None
|
|
1604
|
+
)
|
|
1605
|
+
manifest = (
|
|
1606
|
+
checked_bundle.manifest
|
|
1607
|
+
if checked_bundle is not None
|
|
1608
|
+
else json.loads(
|
|
1609
|
+
(archive.parent / "agent-bundle" / "manifest.json").read_text(encoding="utf-8")
|
|
1610
|
+
)
|
|
1611
|
+
)
|
|
1612
|
+
if manifest.get("bundleDigest") != build.bundle_digest:
|
|
1613
|
+
raise StudioError(
|
|
1614
|
+
"BUILD_DIGEST_MISMATCH",
|
|
1615
|
+
"上传前 Bundle digest 校验失败",
|
|
1616
|
+
status_code=422,
|
|
1617
|
+
)
|
|
1618
|
+
if checked_bundle is not None and manifest.get("agentId") != build.agent_id:
|
|
1619
|
+
raise StudioError(
|
|
1620
|
+
"BUILD_AGENT_MISMATCH",
|
|
1621
|
+
"上传 Bundle 的 AgentId 与 Build 记录不一致",
|
|
1622
|
+
status_code=422,
|
|
1623
|
+
)
|
|
1624
|
+
archive_sha256 = f"sha256:{hashlib.sha256(bundle).hexdigest()}"
|
|
1625
|
+
provenance = (
|
|
1626
|
+
dict(checked_bundle.provenance)
|
|
1627
|
+
if checked_bundle is not None
|
|
1628
|
+
else json.loads(
|
|
1629
|
+
(archive.parent / "agent-bundle" / "provenance.json").read_text(encoding="utf-8")
|
|
1630
|
+
)
|
|
1631
|
+
)
|
|
1632
|
+
provenance["archiveSha256"] = archive_sha256
|
|
1633
|
+
# The Server owns the profile-to-image mapping, but it must select the
|
|
1634
|
+
# profile for the concrete framework the deterministic build produced.
|
|
1635
|
+
provenance["runtimeType"] = str(manifest.get("runtimeType") or build.runtime_type)
|
|
1636
|
+
return build, bundle, provenance
|
|
1637
|
+
|
|
1638
|
+
def get(self, deployment_id: str) -> DeploymentRecord:
|
|
1639
|
+
path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
|
|
1640
|
+
if not path.is_file():
|
|
1641
|
+
raise StudioError(
|
|
1642
|
+
"DEPLOYMENT_NOT_FOUND",
|
|
1643
|
+
"Deployment 不存在",
|
|
1644
|
+
status_code=404,
|
|
1645
|
+
details={"id": deployment_id},
|
|
1646
|
+
)
|
|
1647
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
1648
|
+
return cast(
|
|
1649
|
+
DeploymentRecord,
|
|
1650
|
+
DeploymentRecord.model_validate(payload["record"]),
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1653
|
+
def request_for(self, deployment_id: str) -> DeploymentRequest:
|
|
1654
|
+
"""Return the immutable target stored with a deployment receipt."""
|
|
1655
|
+
|
|
1656
|
+
path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
|
|
1657
|
+
if not path.is_file():
|
|
1658
|
+
self.get(deployment_id)
|
|
1659
|
+
try:
|
|
1660
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
1661
|
+
return DeploymentRequest.model_validate(payload["request"])
|
|
1662
|
+
except (OSError, ValueError, KeyError, json.JSONDecodeError, ValidationError) as exc:
|
|
1663
|
+
raise StudioError(
|
|
1664
|
+
"DEPLOYMENT_RECEIPT_INVALID",
|
|
1665
|
+
"Deployment 回执损坏,不能执行回滚",
|
|
1666
|
+
status_code=409,
|
|
1667
|
+
details={"id": deployment_id},
|
|
1668
|
+
) from exc
|
|
1669
|
+
|
|
1670
|
+
def list(self) -> list[DeploymentRecord]:
|
|
1671
|
+
"""List only valid, workspace-local deployment receipts.
|
|
1672
|
+
|
|
1673
|
+
This is deliberately a local receipt read. Refreshing every row here
|
|
1674
|
+
would turn opening the Studio page into unbounded control-plane calls;
|
|
1675
|
+
callers refresh a named receipt explicitly instead.
|
|
1676
|
+
"""
|
|
1677
|
+
|
|
1678
|
+
directory = self.workspace.resolve(".agentkit/deployments")
|
|
1679
|
+
if not directory.is_dir():
|
|
1680
|
+
return []
|
|
1681
|
+
resolved_directory = directory.resolve()
|
|
1682
|
+
records: list[DeploymentRecord] = []
|
|
1683
|
+
for path in sorted(directory.glob("dep_*.json"), key=lambda item: item.name, reverse=True):
|
|
1684
|
+
try:
|
|
1685
|
+
resolved_path = path.resolve(strict=True)
|
|
1686
|
+
resolved_path.relative_to(resolved_directory)
|
|
1687
|
+
payload = json.loads(resolved_path.read_text(encoding="utf-8"))
|
|
1688
|
+
record = DeploymentRecord.model_validate(payload["record"])
|
|
1689
|
+
if resolved_path.name != f"{record.id}.json":
|
|
1690
|
+
raise ValueError("deployment receipt filename does not match record id")
|
|
1691
|
+
except (
|
|
1692
|
+
OSError,
|
|
1693
|
+
ValueError,
|
|
1694
|
+
KeyError,
|
|
1695
|
+
json.JSONDecodeError,
|
|
1696
|
+
ValidationError,
|
|
1697
|
+
):
|
|
1698
|
+
logger.warning("Ignoring invalid Studio deployment receipt: %s", path.name)
|
|
1699
|
+
continue
|
|
1700
|
+
records.append(record)
|
|
1701
|
+
return records
|
|
1702
|
+
|
|
1703
|
+
async def refresh(self, deployment_id: str) -> DeploymentRecord:
|
|
1704
|
+
"""Refresh only from the Server-owned instance status projection."""
|
|
1705
|
+
|
|
1706
|
+
deployment = self.get(deployment_id)
|
|
1707
|
+
status_reader = getattr(self.gateway, "get_deployment_status", None)
|
|
1708
|
+
if status_reader is None:
|
|
1709
|
+
return deployment
|
|
1710
|
+
refreshed = await status_reader(deployment)
|
|
1711
|
+
path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
|
|
1712
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
1713
|
+
self._save(
|
|
1714
|
+
refreshed,
|
|
1715
|
+
DeploymentRequest.model_validate(payload["request"]),
|
|
1716
|
+
)
|
|
1717
|
+
return refreshed
|
|
1718
|
+
|
|
1719
|
+
async def dashboard_access(self, deployment_id: str) -> dict[str, str | None]:
|
|
1720
|
+
"""Create a private, receipt-bound Hosted UI link on explicit request."""
|
|
1721
|
+
|
|
1722
|
+
deployment = self.get(deployment_id)
|
|
1723
|
+
dashboard_reader = getattr(self.gateway, "get_deployment_dashboard_access", None)
|
|
1724
|
+
if dashboard_reader is None:
|
|
1725
|
+
raise StudioError(
|
|
1726
|
+
"DEPLOYMENT_DASHBOARD_UNAVAILABLE",
|
|
1727
|
+
"当前云端网关不支持打开 Agent UI",
|
|
1728
|
+
status_code=501,
|
|
1729
|
+
)
|
|
1730
|
+
return await dashboard_reader(deployment)
|
|
1731
|
+
|
|
1732
|
+
async def delete(self, deployment_id: str) -> dict[str, Any]:
|
|
1733
|
+
"""Delete the receipt-bound cloud Agent, then remove its local receipts."""
|
|
1734
|
+
|
|
1735
|
+
deployment = self.get(deployment_id)
|
|
1736
|
+
agent_id = str(deployment.agent_id or "").strip()
|
|
1737
|
+
if not agent_id:
|
|
1738
|
+
raise StudioError(
|
|
1739
|
+
"CLOUD_AGENT_DELETE_UNAVAILABLE",
|
|
1740
|
+
"Deployment receipt 缺少云端 Agent 标识",
|
|
1741
|
+
status_code=409,
|
|
1742
|
+
)
|
|
1743
|
+
deleter = getattr(self.gateway, "delete_deployment", None)
|
|
1744
|
+
if deleter is None:
|
|
1745
|
+
raise StudioError(
|
|
1746
|
+
"CLOUD_AGENT_DELETE_UNAVAILABLE",
|
|
1747
|
+
"当前云端网关不支持删除 Agent",
|
|
1748
|
+
status_code=501,
|
|
1749
|
+
)
|
|
1750
|
+
await deleter(deployment)
|
|
1751
|
+
|
|
1752
|
+
deleted_receipts = self._delete_receipts_for_agent(
|
|
1753
|
+
agent_id, required=deployment
|
|
1754
|
+
)
|
|
1755
|
+
return {"agentId": agent_id, "deletedReceiptIds": deleted_receipts}
|
|
1756
|
+
|
|
1757
|
+
def _delete_receipts_for_agent(
|
|
1758
|
+
self, agent_id: str, *, required: DeploymentRecord | None = None
|
|
1759
|
+
) -> list[str]:
|
|
1760
|
+
related_receipts = {required.id: required} if required is not None else {}
|
|
1761
|
+
for record in self.list():
|
|
1762
|
+
if str(record.agent_id or "").strip() != agent_id:
|
|
1763
|
+
continue
|
|
1764
|
+
related_receipts[record.id] = record
|
|
1765
|
+
|
|
1766
|
+
deleted_receipts: list[str] = []
|
|
1767
|
+
for record in related_receipts.values():
|
|
1768
|
+
receipt_path = self.workspace.resolve(
|
|
1769
|
+
Path(".agentkit/deployments") / f"{record.id}.json"
|
|
1770
|
+
)
|
|
1771
|
+
receipt_path.unlink(missing_ok=True)
|
|
1772
|
+
deleted_receipts.append(record.id)
|
|
1773
|
+
return deleted_receipts
|
|
1774
|
+
|
|
1775
|
+
async def list_account_agents(self, *, page: int = 1, size: int = 100) -> dict[str, Any]:
|
|
1776
|
+
reader = getattr(self.gateway, "list_account_agents", None)
|
|
1777
|
+
if reader is None:
|
|
1778
|
+
raise StudioError(
|
|
1779
|
+
"CLOUD_AGENT_DIRECTORY_UNAVAILABLE",
|
|
1780
|
+
"当前云端网关不支持读取账号 Agent",
|
|
1781
|
+
status_code=501,
|
|
1782
|
+
)
|
|
1783
|
+
return await reader(page=page, size=size)
|
|
1784
|
+
|
|
1785
|
+
async def get_account_agent(self, agent_id: str) -> dict[str, Any]:
|
|
1786
|
+
reader = getattr(self.gateway, "get_account_agent", None)
|
|
1787
|
+
if reader is None:
|
|
1788
|
+
raise StudioError(
|
|
1789
|
+
"CLOUD_AGENT_DIRECTORY_UNAVAILABLE",
|
|
1790
|
+
"当前云端网关不支持读取账号 Agent",
|
|
1791
|
+
status_code=501,
|
|
1792
|
+
)
|
|
1793
|
+
return await reader(agent_id)
|
|
1794
|
+
|
|
1795
|
+
async def list_account_agent_versions(
|
|
1796
|
+
self, agent_id: str, *, page: int = 1, size: int = 100
|
|
1797
|
+
) -> dict[str, Any]:
|
|
1798
|
+
reader = getattr(self.gateway, "list_account_agent_versions", None)
|
|
1799
|
+
if reader is None:
|
|
1800
|
+
raise StudioError(
|
|
1801
|
+
"CLOUD_AGENT_VERSION_DIRECTORY_UNAVAILABLE",
|
|
1802
|
+
"当前云端网关不支持读取 Agent 版本",
|
|
1803
|
+
status_code=501,
|
|
1804
|
+
)
|
|
1805
|
+
return await reader(agent_id, page=page, size=size)
|
|
1806
|
+
|
|
1807
|
+
async def rollback_account_agent_version(
|
|
1808
|
+
self, agent_id: str, *, version_id: str
|
|
1809
|
+
) -> dict[str, Any]:
|
|
1810
|
+
rollback = getattr(self.gateway, "rollback_account_agent_version", None)
|
|
1811
|
+
if rollback is None:
|
|
1812
|
+
raise StudioError(
|
|
1813
|
+
"CLOUD_AGENT_VERSION_ROLLBACK_UNAVAILABLE",
|
|
1814
|
+
"当前云端网关不支持回滚 Agent 版本",
|
|
1815
|
+
status_code=501,
|
|
1816
|
+
)
|
|
1817
|
+
return await rollback(agent_id, version_id=version_id)
|
|
1818
|
+
|
|
1819
|
+
async def account_agent_dashboard_access(
|
|
1820
|
+
self, agent_id: str
|
|
1821
|
+
) -> dict[str, str | None]:
|
|
1822
|
+
reader = getattr(self.gateway, "get_account_agent_dashboard_access", None)
|
|
1823
|
+
if reader is None:
|
|
1824
|
+
raise StudioError(
|
|
1825
|
+
"DEPLOYMENT_DASHBOARD_UNAVAILABLE",
|
|
1826
|
+
"当前云端网关不支持打开 Agent UI",
|
|
1827
|
+
status_code=501,
|
|
1828
|
+
)
|
|
1829
|
+
return await reader(agent_id)
|
|
1830
|
+
|
|
1831
|
+
async def delete_account_agent(self, agent_id: str) -> dict[str, Any]:
|
|
1832
|
+
deleter = getattr(self.gateway, "delete_account_agent", None)
|
|
1833
|
+
if deleter is None:
|
|
1834
|
+
raise StudioError(
|
|
1835
|
+
"CLOUD_AGENT_DELETE_UNAVAILABLE",
|
|
1836
|
+
"当前云端网关不支持删除 Agent",
|
|
1837
|
+
status_code=501,
|
|
1838
|
+
)
|
|
1839
|
+
await deleter(agent_id)
|
|
1840
|
+
return {
|
|
1841
|
+
"agentId": agent_id,
|
|
1842
|
+
"deletedReceiptIds": self._delete_receipts_for_agent(agent_id),
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
async def _chat_target(
|
|
1846
|
+
self, target_id: str
|
|
1847
|
+
) -> DeploymentRecord | AccountCloudAgentReference:
|
|
1848
|
+
if not target_id.startswith("account:"):
|
|
1849
|
+
return self.get(target_id)
|
|
1850
|
+
agent_id = target_id.removeprefix("account:").strip()
|
|
1851
|
+
detail = await self.get_account_agent(agent_id)
|
|
1852
|
+
resolved_id = str(detail.get("agentId") or "").strip()
|
|
1853
|
+
if not agent_id or resolved_id != agent_id:
|
|
1854
|
+
raise StudioError(
|
|
1855
|
+
"CLOUD_AGENT_REFERENCE_INVALID",
|
|
1856
|
+
"账号 Agent 引用与 Server 返回不一致",
|
|
1857
|
+
status_code=409,
|
|
1858
|
+
)
|
|
1859
|
+
if detail.get("chatTransport") != "studio-session-events":
|
|
1860
|
+
raise StudioError(
|
|
1861
|
+
"CLOUD_CHAT_TRANSPORT_UNSUPPORTED",
|
|
1862
|
+
"该类型 Agent 未声明统一 SessionEvent 会话能力,请使用官方 Dashboard",
|
|
1863
|
+
status_code=409,
|
|
1864
|
+
details={
|
|
1865
|
+
"agentId": agent_id,
|
|
1866
|
+
"chatTransport": detail.get("chatTransport"),
|
|
1867
|
+
"reason": detail.get("chatRoutingReason"),
|
|
1868
|
+
},
|
|
1869
|
+
)
|
|
1870
|
+
return AccountCloudAgentReference(agent_id=agent_id)
|
|
1871
|
+
|
|
1872
|
+
async def list_cloud_chat_sessions(
|
|
1873
|
+
self, deployment_id: str, *, page: int = 1, size: int = 50
|
|
1874
|
+
) -> dict[str, Any]:
|
|
1875
|
+
deployment = await self._chat_target(deployment_id)
|
|
1876
|
+
reader = getattr(self.gateway, "list_deployment_chat_sessions", None)
|
|
1877
|
+
if reader is None:
|
|
1878
|
+
raise StudioError(
|
|
1879
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
1880
|
+
"当前云端网关不支持本地会话代理",
|
|
1881
|
+
status_code=501,
|
|
1882
|
+
)
|
|
1883
|
+
return await reader(deployment, page=page, size=size)
|
|
1884
|
+
|
|
1885
|
+
async def create_cloud_chat_session(self, deployment_id: str) -> dict[str, Any]:
|
|
1886
|
+
deployment = await self._chat_target(deployment_id)
|
|
1887
|
+
creator = getattr(self.gateway, "create_deployment_chat_session", None)
|
|
1888
|
+
if creator is None:
|
|
1889
|
+
raise StudioError(
|
|
1890
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
1891
|
+
"当前云端网关不支持本地会话代理",
|
|
1892
|
+
status_code=501,
|
|
1893
|
+
)
|
|
1894
|
+
return await creator(deployment)
|
|
1895
|
+
|
|
1896
|
+
async def list_cloud_chat_messages(
|
|
1897
|
+
self,
|
|
1898
|
+
deployment_id: str,
|
|
1899
|
+
*,
|
|
1900
|
+
session_id: str,
|
|
1901
|
+
after_seq_id: int | None = None,
|
|
1902
|
+
limit: int = 100,
|
|
1903
|
+
) -> dict[str, Any]:
|
|
1904
|
+
deployment = await self._chat_target(deployment_id)
|
|
1905
|
+
reader = getattr(self.gateway, "list_deployment_chat_messages", None)
|
|
1906
|
+
if reader is None:
|
|
1907
|
+
raise StudioError(
|
|
1908
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
1909
|
+
"当前云端网关不支持本地会话代理",
|
|
1910
|
+
status_code=501,
|
|
1911
|
+
)
|
|
1912
|
+
return await reader(
|
|
1913
|
+
deployment,
|
|
1914
|
+
session_id=session_id,
|
|
1915
|
+
after_seq_id=after_seq_id,
|
|
1916
|
+
limit=limit,
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
async def list_cloud_chat_events(
|
|
1920
|
+
self,
|
|
1921
|
+
deployment_id: str,
|
|
1922
|
+
*,
|
|
1923
|
+
session_id: str,
|
|
1924
|
+
after_seq_id: int | None = None,
|
|
1925
|
+
limit: int = 200,
|
|
1926
|
+
) -> dict[str, Any]:
|
|
1927
|
+
deployment = await self._chat_target(deployment_id)
|
|
1928
|
+
reader = getattr(self.gateway, "list_deployment_chat_events", None)
|
|
1929
|
+
if reader is None:
|
|
1930
|
+
raise StudioError(
|
|
1931
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
1932
|
+
"当前云端网关不支持本地会话代理",
|
|
1933
|
+
status_code=501,
|
|
1934
|
+
)
|
|
1935
|
+
return await reader(
|
|
1936
|
+
deployment,
|
|
1937
|
+
session_id=session_id,
|
|
1938
|
+
after_seq_id=after_seq_id,
|
|
1939
|
+
limit=limit,
|
|
1940
|
+
)
|
|
1941
|
+
|
|
1942
|
+
async def send_cloud_chat_message(
|
|
1943
|
+
self,
|
|
1944
|
+
deployment_id: str,
|
|
1945
|
+
*,
|
|
1946
|
+
session_id: str,
|
|
1947
|
+
content: Any,
|
|
1948
|
+
model: str | None = None,
|
|
1949
|
+
model_options: dict[str, Any] | None = None,
|
|
1950
|
+
tool_approval_mode: str | None = None,
|
|
1951
|
+
collaboration_mode: str | None = None,
|
|
1952
|
+
goal_objective: str | None = None,
|
|
1953
|
+
) -> dict[str, Any]:
|
|
1954
|
+
deployment = await self._chat_target(deployment_id)
|
|
1955
|
+
sender = getattr(self.gateway, "send_deployment_chat_message", None)
|
|
1956
|
+
if sender is None:
|
|
1957
|
+
raise StudioError(
|
|
1958
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
1959
|
+
"当前云端网关不支持本地会话代理",
|
|
1960
|
+
status_code=501,
|
|
1961
|
+
)
|
|
1962
|
+
kwargs: dict[str, Any] = {
|
|
1963
|
+
"session_id": session_id,
|
|
1964
|
+
"content": content,
|
|
1965
|
+
}
|
|
1966
|
+
if model is not None:
|
|
1967
|
+
kwargs["model"] = model
|
|
1968
|
+
if model_options:
|
|
1969
|
+
kwargs["model_options"] = model_options
|
|
1970
|
+
if tool_approval_mode is not None:
|
|
1971
|
+
kwargs["tool_approval_mode"] = tool_approval_mode
|
|
1972
|
+
if collaboration_mode is not None:
|
|
1973
|
+
kwargs["collaboration_mode"] = collaboration_mode
|
|
1974
|
+
if goal_objective is not None:
|
|
1975
|
+
kwargs["goal_objective"] = goal_objective
|
|
1976
|
+
return await sender(deployment, **kwargs)
|
|
1977
|
+
|
|
1978
|
+
async def stream_cloud_chat_message(
|
|
1979
|
+
self,
|
|
1980
|
+
deployment_id: str,
|
|
1981
|
+
*,
|
|
1982
|
+
session_id: str,
|
|
1983
|
+
content: Any,
|
|
1984
|
+
model: str | None = None,
|
|
1985
|
+
model_options: dict[str, Any] | None = None,
|
|
1986
|
+
tool_approval_mode: str | None = None,
|
|
1987
|
+
collaboration_mode: str | None = None,
|
|
1988
|
+
goal_objective: str | None = None,
|
|
1989
|
+
) -> AsyncIterator[bytes]:
|
|
1990
|
+
deployment = await self._chat_target(deployment_id)
|
|
1991
|
+
sender = getattr(self.gateway, "stream_deployment_chat_message", None)
|
|
1992
|
+
if sender is None:
|
|
1993
|
+
raise StudioError(
|
|
1994
|
+
"CLOUD_CHAT_STREAM_UNAVAILABLE",
|
|
1995
|
+
"当前云端网关不支持实时会话代理",
|
|
1996
|
+
status_code=501,
|
|
1997
|
+
)
|
|
1998
|
+
return await sender(
|
|
1999
|
+
deployment,
|
|
2000
|
+
session_id=session_id,
|
|
2001
|
+
content=content,
|
|
2002
|
+
model=model,
|
|
2003
|
+
model_options=model_options,
|
|
2004
|
+
tool_approval_mode=tool_approval_mode,
|
|
2005
|
+
collaboration_mode=collaboration_mode,
|
|
2006
|
+
goal_objective=goal_objective,
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
async def list_cloud_chat_models(self, deployment_id: str) -> dict[str, Any]:
|
|
2010
|
+
deployment = await self._chat_target(deployment_id)
|
|
2011
|
+
reader = getattr(self.gateway, "list_deployment_chat_models", None)
|
|
2012
|
+
if reader is None:
|
|
2013
|
+
raise StudioError(
|
|
2014
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
2015
|
+
"当前云端网关不支持模型目录代理",
|
|
2016
|
+
status_code=501,
|
|
2017
|
+
)
|
|
2018
|
+
return await reader(deployment)
|
|
2019
|
+
|
|
2020
|
+
async def submit_cloud_chat_interaction(
|
|
2021
|
+
self,
|
|
2022
|
+
deployment_id: str,
|
|
2023
|
+
*,
|
|
2024
|
+
session_id: str,
|
|
2025
|
+
run_id: str,
|
|
2026
|
+
interaction_id: str,
|
|
2027
|
+
expected_revision: int,
|
|
2028
|
+
action: str,
|
|
2029
|
+
response: dict[str, Any],
|
|
2030
|
+
idempotency_key: str,
|
|
2031
|
+
) -> dict[str, Any]:
|
|
2032
|
+
deployment = await self._chat_target(deployment_id)
|
|
2033
|
+
submitter = getattr(self.gateway, "submit_deployment_chat_interaction", None)
|
|
2034
|
+
if submitter is None:
|
|
2035
|
+
raise StudioError(
|
|
2036
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
2037
|
+
"当前云端网关不支持本地会话代理",
|
|
2038
|
+
status_code=501,
|
|
2039
|
+
)
|
|
2040
|
+
return await submitter(
|
|
2041
|
+
deployment,
|
|
2042
|
+
session_id=session_id,
|
|
2043
|
+
run_id=run_id,
|
|
2044
|
+
interaction_id=interaction_id,
|
|
2045
|
+
expected_revision=expected_revision,
|
|
2046
|
+
action=action,
|
|
2047
|
+
response=response,
|
|
2048
|
+
idempotency_key=idempotency_key,
|
|
2049
|
+
)
|
|
2050
|
+
|
|
2051
|
+
async def delete_cloud_chat_session(self, deployment_id: str, *, session_id: str) -> bool:
|
|
2052
|
+
deployment = await self._chat_target(deployment_id)
|
|
2053
|
+
deleter = getattr(self.gateway, "delete_deployment_chat_session", None)
|
|
2054
|
+
if deleter is None:
|
|
2055
|
+
raise StudioError(
|
|
2056
|
+
"CLOUD_CHAT_UNAVAILABLE",
|
|
2057
|
+
"当前云端网关不支持本地会话代理",
|
|
2058
|
+
status_code=501,
|
|
2059
|
+
)
|
|
2060
|
+
return await deleter(deployment, session_id=session_id)
|
|
2061
|
+
|
|
2062
|
+
async def rollback(
|
|
2063
|
+
self,
|
|
2064
|
+
deployment_id: str,
|
|
2065
|
+
*,
|
|
2066
|
+
target_build_id: str,
|
|
2067
|
+
) -> DeploymentRecord:
|
|
2068
|
+
path = self.workspace.resolve(Path(".agentkit/deployments") / f"{deployment_id}.json")
|
|
2069
|
+
if not path.is_file():
|
|
2070
|
+
self.get(deployment_id)
|
|
2071
|
+
request = self.request_for(deployment_id)
|
|
2072
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
2073
|
+
deployment = DeploymentRecord.model_validate(payload["record"])
|
|
2074
|
+
return await self._deploy_build(
|
|
2075
|
+
target_build_id,
|
|
2076
|
+
request,
|
|
2077
|
+
replacing=deployment,
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
def _save(
|
|
2081
|
+
self,
|
|
2082
|
+
record: DeploymentRecord,
|
|
2083
|
+
request: DeploymentRequest,
|
|
2084
|
+
) -> None:
|
|
2085
|
+
directory = self.workspace.resolve(".agentkit/deployments")
|
|
2086
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
2087
|
+
self.workspace.atomic_write_text(
|
|
2088
|
+
directory / f"{record.id}.json",
|
|
2089
|
+
json.dumps(
|
|
2090
|
+
{
|
|
2091
|
+
"record": record.model_dump(by_alias=True, exclude_none=True, mode="json"),
|
|
2092
|
+
"request": request.model_dump(by_alias=True, exclude_none=True, mode="json"),
|
|
2093
|
+
},
|
|
2094
|
+
ensure_ascii=False,
|
|
2095
|
+
sort_keys=True,
|
|
2096
|
+
indent=2,
|
|
2097
|
+
)
|
|
2098
|
+
+ "\n",
|
|
2099
|
+
)
|